Skip to content

Commit b09ffd9

Browse files
authored
Merge pull request #127 from harrydbarnes/refactor-onboarding-validation-3301151275267496739
Refactor Onboarding name validation to use state-driven UI
2 parents d92aa2d + eca5813 commit b09ffd9

File tree

4 files changed

+73
-4
lines changed

4 files changed

+73
-4
lines changed

app/src/main/java/com/example/theloop/OnboardingViewModel.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,23 @@ class OnboardingViewModel @Inject constructor(
1818
private val _name = MutableStateFlow("")
1919
val name: StateFlow<String> = _name.asStateFlow()
2020

21+
private val _nameError = MutableStateFlow<Int?>(null)
22+
val nameError: StateFlow<Int?> = _nameError.asStateFlow()
23+
2124
fun onNameChange(newName: String) {
2225
_name.value = newName
26+
_nameError.value = null
2327
}
2428

2529
fun saveName(): Boolean {
2630
val isNameValid = name.value.isNotBlank()
2731
if (isNameValid) {
32+
_nameError.value = null
2833
viewModelScope.launch {
2934
userPreferencesRepository.saveUserName(name.value)
3035
}
36+
} else {
37+
_nameError.value = R.string.error_name_blank
3138
}
3239
return isNameValid
3340
}

app/src/main/java/com/example/theloop/ui/screens/OnboardingScreen.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import androidx.compose.runtime.*
1313
import androidx.compose.ui.Alignment
1414
import androidx.compose.ui.Modifier
1515
import androidx.compose.ui.platform.LocalContext
16+
import androidx.compose.ui.res.stringResource
1617
import androidx.compose.ui.unit.dp
1718
import androidx.hilt.navigation.compose.hiltViewModel
1819
import androidx.health.connect.client.HealthConnectClient
@@ -28,6 +29,7 @@ fun OnboardingScreen(
2829
val context = LocalContext.current
2930
var currentStep by remember { mutableIntStateOf(0) }
3031
val name by viewModel.name.collectAsState()
32+
val nameError by viewModel.nameError.collectAsState()
3133
val totalSteps = 5
3234

3335
// Permissions state
@@ -99,7 +101,6 @@ fun OnboardingScreen(
99101
if (currentStep == 0) {
100102
// Save Name
101103
if (!viewModel.saveName()) {
102-
Toast.makeText(context, "Name cannot be blank", Toast.LENGTH_SHORT).show()
103104
return@Button
104105
}
105106
}
@@ -127,7 +128,7 @@ fun OnboardingScreen(
127128
verticalArrangement = Arrangement.Center
128129
) {
129130
when (currentStep) {
130-
0 -> WelcomeStep(name, viewModel::onNameChange)
131+
0 -> WelcomeStep(name, nameError, viewModel::onNameChange)
131132
1 -> PermissionStep(
132133
title = "Enable Location",
133134
description = "We need your location to show local weather.",
@@ -167,7 +168,7 @@ fun OnboardingScreen(
167168
}
168169

169170
@Composable
170-
fun WelcomeStep(name: String, onNameChange: (String) -> Unit) {
171+
fun WelcomeStep(name: String, nameError: Int?, onNameChange: (String) -> Unit) {
171172
Text("Welcome to The Loop", style = MaterialTheme.typography.displaySmall)
172173
Spacer(Modifier.height(16.dp))
173174
Text("Your daily dashboard for life.", style = MaterialTheme.typography.bodyLarge)
@@ -177,7 +178,9 @@ fun WelcomeStep(name: String, onNameChange: (String) -> Unit) {
177178
onValueChange = onNameChange,
178179
label = { Text("What's your name?") },
179180
singleLine = true,
180-
modifier = Modifier.fillMaxWidth()
181+
modifier = Modifier.fillMaxWidth(),
182+
isError = nameError != null,
183+
supportingText = nameError?.let { { Text(stringResource(it)) } }
181184
)
182185
}
183186

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<string name="app_name">The Loop</string>
33

44
<!-- UI Strings -->
5+
<string name="error_name_blank">Name cannot be blank</string>
56
<string name="select_temperature_unit">Select Temperature Unit</string>
67
<string name="cancel">Cancel</string>
78
<string name="unknown_location">Unknown Location</string>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.example.theloop
2+
3+
import androidx.test.core.app.ApplicationProvider
4+
import com.example.theloop.data.repository.UserPreferencesRepository
5+
import org.junit.Assert.assertEquals
6+
import org.junit.Assert.assertFalse
7+
import org.junit.Assert.assertNull
8+
import org.junit.Assert.assertTrue
9+
import org.junit.Before
10+
import org.junit.Test
11+
import org.junit.runner.RunWith
12+
import org.robolectric.RobolectricTestRunner
13+
import org.robolectric.annotation.Config
14+
15+
@RunWith(RobolectricTestRunner::class)
16+
@Config(sdk = [33])
17+
class OnboardingViewModelTest {
18+
19+
private lateinit var viewModel: OnboardingViewModel
20+
private lateinit var userPreferencesRepository: UserPreferencesRepository
21+
22+
@Before
23+
fun setup() {
24+
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
25+
userPreferencesRepository = UserPreferencesRepository(context)
26+
viewModel = OnboardingViewModel(userPreferencesRepository)
27+
}
28+
29+
@Test
30+
fun saveName_withBlankName_setsErrorAndReturnsFalse() {
31+
viewModel.onNameChange(" ")
32+
val result = viewModel.saveName()
33+
34+
assertFalse(result)
35+
assertEquals(R.string.error_name_blank, viewModel.nameError.value)
36+
}
37+
38+
@Test
39+
fun saveName_withValidName_clearsErrorAndReturnsTrue() {
40+
viewModel.onNameChange("Valid Name")
41+
val result = viewModel.saveName()
42+
43+
assertTrue(result)
44+
assertNull(viewModel.nameError.value)
45+
}
46+
47+
@Test
48+
fun onNameChange_clearsError() {
49+
// Set error first
50+
viewModel.onNameChange("")
51+
viewModel.saveName()
52+
assertEquals(R.string.error_name_blank, viewModel.nameError.value)
53+
54+
// Change name
55+
viewModel.onNameChange("New Name")
56+
assertNull(viewModel.nameError.value)
57+
}
58+
}

0 commit comments

Comments
 (0)