Skip to content

Commit db0c639

Browse files
committed
feat: Add pull-to-refresh and improve navigation in settings
- **Settings Categories**: - Added `Refresh` and `NavBackDetail` actions to `SettingsCategoriesViewModel`. - Implemented `navBackDetail` to clear the selected category and navigate back via the router. - Updated `SettingsCategoriesResult` to include a `loading` state. - Wrapped `SettingsMasterScreenBody` content in a `PullToRefreshBox`. - **Settings Detail**: - Added a `Refresh` action to `SettingsViewModel` to trigger switch updates. - Integrated `PullToRefreshBox` into `SettingsDetailScreen` for manual updates. - Optimized state collection using `derivedStateOf` for refresh and loading indicators. - **Adaptive UI & Navigation**: - Updated `AdaptiveSettingsScreen` to use the view model's `NavBackDetail` action for back navigation, ensuring state synchronization. - Replaced local coroutine-based navigation in the UI with view model actions. - **Testing**: - Enabled the navigation back test case in `AdaptiveInteractorTest`.
1 parent b1c1731 commit db0c639

File tree

11 files changed

+59
-46
lines changed

11 files changed

+59
-46
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ private fun ScreenContent(
225225
4. **Preview functions**: Add `@Preview` for visual components
226226
5. **Remember wisely**: Use `remember` for expensive calculations
227227
6. **Keys in lists**: Provide stable keys for LazyColumn/LazyRow
228+
7. **Adaptive drag handle**: Prefer method references for pane expansion, e.g. `paneExpansionDragHandle = ThreePaneScaffoldScope::VerticalPaneExpansionDragHandle` (use a lambda in Kotlin/Wasm actuals to avoid the current composable function reference compiler issue).
228229

229230
### ViewModel Style
230231

core/presentation/src/androidHostTest/kotlin/com/softartdev/notedelight/presentation/adaptive/AdaptiveInteractorTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.softartdev.notedelight.presentation.note.NoteAction
2121
import com.softartdev.notedelight.presentation.note.NoteResult
2222
import com.softartdev.notedelight.presentation.note.NoteViewModel
2323
import com.softartdev.notedelight.presentation.settings.SecurityResult
24+
import com.softartdev.notedelight.presentation.settings.SettingsAction
2425
import com.softartdev.notedelight.presentation.settings.SettingsCategoriesAction
2526
import com.softartdev.notedelight.presentation.settings.SettingsCategoriesViewModel
2627
import com.softartdev.notedelight.presentation.settings.SettingsViewModel
@@ -209,7 +210,7 @@ class AdaptiveInteractorTest {
209210
}
210211

211212
@Test
212-
fun `when SettingsCategoriesViewModel navigates back then selection cleared`() = runTest {
213+
fun `when SettingsViewModel navigates back then selection cleared`() = runTest {
213214
settingsCategoriesViewModel.launchCategories()
214215
settingsViewModel.launchCollectingSelectedCategoryId()
215216

@@ -221,7 +222,7 @@ class AdaptiveInteractorTest {
221222
val selected: SecurityResult = awaitItem()
222223
assertEquals(SettingsCategory.Theme, selected.selectedCategory)
223224

224-
settingsCategoriesViewModel.onAction(SettingsCategoriesAction.NavBackDetail)
225+
settingsViewModel.onAction(SettingsAction.NavBack)
225226

226227
val cleared: SecurityResult = awaitItem()
227228
assertNull(cleared.selectedCategory)

core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/settings/SettingsCategoriesResult.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,5 @@ data class SettingsCategoriesResult(
1313
sealed interface SettingsCategoriesAction {
1414
data class SelectCategory(val category: SettingsCategory) : SettingsCategoriesAction
1515
data object Refresh : SettingsCategoriesAction
16-
data object NavBackDetail : SettingsCategoriesAction
1716
data object NavBack : SettingsCategoriesAction
1817
}

core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/settings/SettingsCategoriesViewModel.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,12 @@ class SettingsCategoriesViewModel(
2525

2626
fun launchCategories() {
2727
if (job != null) return
28-
job = viewModelScope.launch {
29-
adaptiveInteractor.selectedSettingsCategoryIdStateFlow.collect { selectedId: Long? ->
30-
mutableStateFlow.update { result -> result.copy(selectedCategoryId = selectedId) }
31-
}
32-
}
28+
startCollectingSelection()
3329
}
3430

3531
fun onAction(action: SettingsCategoriesAction) = when (action) {
3632
is SettingsCategoriesAction.SelectCategory -> selectCategory(action.category)
3733
is SettingsCategoriesAction.Refresh -> refresh()
38-
is SettingsCategoriesAction.NavBackDetail -> navBackDetail()
3934
is SettingsCategoriesAction.NavBack -> navBack()
4035
}
4136

@@ -46,15 +41,20 @@ class SettingsCategoriesViewModel(
4641

4742
private fun refresh() = viewModelScope.launch {
4843
mutableStateFlow.update(SettingsCategoriesResult::showLoading)
44+
startCollectingSelection()
4945
mutableStateFlow.update { result ->
5046
result.copy(selectedCategoryId = adaptiveInteractor.selectedSettingsCategoryIdStateFlow.value)
5147
}
5248
mutableStateFlow.update(SettingsCategoriesResult::hideLoading)
5349
}
5450

55-
private fun navBackDetail() = viewModelScope.launch {
56-
adaptiveInteractor.selectedSettingsCategoryIdStateFlow.value = null
57-
router.adaptiveNavigateBack()
51+
private fun startCollectingSelection() {
52+
job?.cancel()
53+
job = viewModelScope.launch {
54+
adaptiveInteractor.selectedSettingsCategoryIdStateFlow.collect { selectedId: Long? ->
55+
mutableStateFlow.update { result -> result.copy(selectedCategoryId = selectedId) }
56+
}
57+
}
5858
}
5959

6060
private fun navBack() {

core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/settings/SettingsViewModel.kt

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,12 @@ class SettingsViewModel(
4444

4545
fun launchCollectingSelectedCategoryId() {
4646
if (selectedCategoryJob != null) return
47-
selectedCategoryJob = viewModelScope.launch {
48-
adaptiveInteractor.selectedSettingsCategoryIdStateFlow.collect { selectedId: Long? ->
49-
mutableStateFlow.update { result ->
50-
result.copy(selectedCategory = SettingsCategory.fromId(selectedId))
51-
}
52-
}
53-
}
47+
startCollectingSelectedCategory()
5448
}
5549

5650
fun onAction(action: SettingsAction) = when (action) {
5751
is SettingsAction.NavBack -> navBack()
58-
is SettingsAction.Refresh -> updateSwitches()
52+
is SettingsAction.Refresh -> refresh()
5953
is SettingsAction.ChangeTheme -> changeTheme()
6054
is SettingsAction.ChangeLanguage -> changeLanguage()
6155
is SettingsAction.ChangeEncryption -> changeEncryption(action.checked)
@@ -84,8 +78,25 @@ class SettingsViewModel(
8478
}
8579
}
8680

87-
private fun navBack() {
88-
if (!router.popBackStack()) router.navigate(route = AppNavGraph.Splash)
81+
private fun navBack() = viewModelScope.launch {
82+
adaptiveInteractor.selectedSettingsCategoryIdStateFlow.value = null
83+
router.adaptiveNavigateBack()
84+
}
85+
86+
private fun refresh() {
87+
startCollectingSelectedCategory()
88+
updateSwitches()
89+
}
90+
91+
private fun startCollectingSelectedCategory() {
92+
selectedCategoryJob?.cancel()
93+
selectedCategoryJob = viewModelScope.launch {
94+
adaptiveInteractor.selectedSettingsCategoryIdStateFlow.collect { selectedId: Long? ->
95+
mutableStateFlow.update { result ->
96+
result.copy(selectedCategory = SettingsCategory.fromId(selectedId))
97+
}
98+
}
99+
}
89100
}
90101

91102
private fun changeTheme() = router.navigate(route = AppNavGraph.ThemeDialog)

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ fun NoteDetailBody(
237237
Use Action interfaces when you have **3+ different user actions** that need to be passed down through multiple Composable layers. Examples:
238238
-`NoteAction` (Save, Edit, Delete, CheckSaveChange) - 4 actions
239239
-`MainAction` (OnNoteClick, OnSettingsClick, OnRefresh) - 3 actions
240-
-`SettingsAction` (NavBack, ChangeTheme, CheckEncryption, etc.) - 7 actions
240+
-`SettingsAction` (NavBack, Refresh, ChangeTheme, ChangeLanguage, ChangeEncryption, ChangePassword, ShowCipherVersion, ShowDatabasePath, ShowFileList, RevealFileList) - 10 actions
241241
-`ChangeAction` (OnEditOldPassword, OnEditNewPassword, OnEditRepeatPassword, etc.) - 5 actions
242242

243243
**When NOT to Use Action Interfaces**:

ui/shared/src/commonMain/kotlin/com/softartdev/notedelight/ui/Composables.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,10 @@ fun PasswordSaveButton(
143143
@Composable
144144
fun ThreePaneScaffoldScope.VerticalPaneExpansionDragHandle(
145145
paneExpansionState: PaneExpansionState,
146-
modifier: Modifier = Modifier,
147146
) {
148147
val mutableInteractionSource = remember { MutableInteractionSource() }
149148
VerticalDragHandle(
150-
modifier = modifier.paneExpansionDraggable(
149+
modifier = Modifier.paneExpansionDraggable(
151150
state = paneExpansionState,
152151
minTouchTargetSize = LocalMinimumInteractiveComponentSize.current,
153152
interactionSource = mutableInteractionSource,
@@ -188,4 +187,4 @@ fun PreviewError() = Error(err = "Mock error")
188187

189188
@Preview(showBackground = true)
190189
@Composable
191-
fun PreviewPasswordSaveButton() = PasswordSaveButton(tag = "PASSWORD_SAVE_BUTTON_TAG") {}
190+
fun PreviewPasswordSaveButton() = PasswordSaveButton(tag = "PASSWORD_SAVE_BUTTON_TAG") {}

ui/shared/src/commonMain/kotlin/com/softartdev/notedelight/ui/main/AdaptiveMainScreen.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import androidx.compose.material3.SnackbarHostState
99
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
1010
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
1111
import androidx.compose.material3.adaptive.layout.PaneExpansionState
12-
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope
1312
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
1413
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
1514
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
@@ -49,7 +48,9 @@ fun AdaptiveMainScreen(
4948
value = navigator.scaffoldValue,
5049
listPane = { MainScreen(mainViewModel, snackbarHostState) },
5150
detailPane = { NoteDetail(noteViewModel) },
52-
paneExpansionDragHandle = ThreePaneScaffoldScope::VerticalPaneExpansionDragHandle,
51+
paneExpansionDragHandle = { paneExpansionState: PaneExpansionState ->
52+
VerticalPaneExpansionDragHandle(paneExpansionState)
53+
},
5354
paneExpansionState = paneExpansionState
5455
)
5556
BackHandler(navigator.canNavigateBack()) { coroutineScope.launch { navigator.navigateBack() } }

ui/shared/src/commonMain/kotlin/com/softartdev/notedelight/ui/settings/AdaptiveSettingsScreen.kt

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import androidx.compose.material3.MaterialTheme
77
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
88
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
99
import androidx.compose.material3.adaptive.layout.PaneExpansionState
10-
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope
1110
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
1211
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
1312
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
@@ -17,10 +16,8 @@ import androidx.compose.ui.Modifier
1716
import androidx.compose.ui.tooling.preview.Preview
1817
import com.softartdev.notedelight.di.PreviewKoin
1918
import com.softartdev.notedelight.navigation.Router
20-
import com.softartdev.notedelight.presentation.settings.SettingsCategoriesAction
2119
import com.softartdev.notedelight.presentation.settings.SettingsCategoriesViewModel
2220
import com.softartdev.notedelight.presentation.settings.SettingsViewModel
23-
import com.softartdev.notedelight.ui.BackHandler
2421
import com.softartdev.notedelight.ui.VerticalPaneExpansionDragHandle
2522
import com.softartdev.notedelight.ui.settings.detail.SettingsDetailScreen
2623
import com.softartdev.notedelight.ui.settings.master.SettingsMasterScreen
@@ -48,16 +45,13 @@ fun AdaptiveSettingsScreen(
4845
SettingsMasterScreen(categoriesViewModel = categoriesViewModel)
4946
},
5047
detailPane = {
51-
SettingsDetailScreen(
52-
settingsViewModel = settingsViewModel,
53-
onBackClick = { categoriesViewModel.onAction(SettingsCategoriesAction.NavBackDetail) },
54-
)
48+
SettingsDetailScreen(settingsViewModel = settingsViewModel)
49+
},
50+
paneExpansionDragHandle = { paneExpansionState: PaneExpansionState ->
51+
VerticalPaneExpansionDragHandle(paneExpansionState)
5552
},
56-
paneExpansionDragHandle = ThreePaneScaffoldScope::VerticalPaneExpansionDragHandle,
5753
paneExpansionState = paneExpansionState
5854
)
59-
BackHandler(enabled = navigator.canNavigateBack()) { categoriesViewModel.onAction(SettingsCategoriesAction.NavBackDetail) }
60-
BackHandler(enabled = !navigator.canNavigateBack()) { categoriesViewModel.onAction(SettingsCategoriesAction.NavBack) }
6155
}
6256

6357
@Preview

ui/shared/src/commonMain/kotlin/com/softartdev/notedelight/ui/settings/detail/SettingsDetailScreen.kt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import com.softartdev.notedelight.model.SettingsCategory
5454
import com.softartdev.notedelight.presentation.settings.SecurityResult
5555
import com.softartdev.notedelight.presentation.settings.SettingsAction
5656
import com.softartdev.notedelight.presentation.settings.SettingsViewModel
57+
import com.softartdev.notedelight.ui.BackHandler
5758
import com.softartdev.notedelight.ui.SettingsDetailPanePlaceholder
5859
import com.softartdev.notedelight.ui.icon.FileLock
5960
import com.softartdev.notedelight.util.ENABLE_ENCRYPTION_SWITCH_TAG
@@ -79,7 +80,6 @@ import androidx.compose.ui.semantics.testTag as semanticsTestTag
7980
@Composable
8081
fun SettingsDetailScreen(
8182
settingsViewModel: SettingsViewModel,
82-
onBackClick: () -> Unit,
8383
) {
8484
LaunchedEffect(settingsViewModel) {
8585
settingsViewModel.launchCollectingSelectedCategoryId()
@@ -95,13 +95,16 @@ fun SettingsDetailScreen(
9595
}
9696
when (result.selectedCategory) {
9797
null -> SettingsDetailPanePlaceholder()
98-
else -> SettingsDetailScreenBody(
99-
result = result,
100-
onBackClick = onBackClick,
101-
onAction = settingsViewModel::onAction,
102-
onRefresh = { settingsViewModel.onAction(SettingsAction.Refresh) },
103-
refreshState = refreshState,
104-
)
98+
else -> {
99+
SettingsDetailScreenBody(
100+
result = result,
101+
onBackClick = { settingsViewModel.onAction(SettingsAction.NavBack) },
102+
onAction = settingsViewModel::onAction,
103+
onRefresh = { settingsViewModel.onAction(SettingsAction.Refresh) },
104+
refreshState = refreshState,
105+
)
106+
BackHandler { settingsViewModel.onAction(SettingsAction.NavBack) }
107+
}
105108
}
106109
}
107110

0 commit comments

Comments
 (0)