Skip to content

Commit 38e351d

Browse files
committed
Metro: the four SavedStateHandle dialogs via assisted view model factories
1 parent f604494 commit 38e351d

9 files changed

Lines changed: 142 additions & 28 deletions

File tree

app/src/test/kotlin/app/aaps/di/metro/ContributedViewModelsTest.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import app.aaps.plugins.sync.smsCommunicator.compose.SmsCommunicatorViewModel
88
import app.aaps.plugins.sync.tidepool.compose.TidepoolViewModel
99
import app.aaps.plugins.sync.wear.compose.WearViewModel
1010
import app.aaps.plugins.sync.xdrip.compose.XdripViewModel
11+
import app.aaps.ui.compose.careDialog.CareDialogViewModel
12+
import app.aaps.ui.compose.fillDialog.FillDialogViewModel
13+
import app.aaps.ui.compose.scenes.wizard.SceneWizardViewModel
14+
import app.aaps.ui.compose.wizardDialog.WizardDialogViewModel
1115
import com.google.common.truth.Truth.assertThat
1216
import org.junit.jupiter.api.Test
1317

@@ -44,6 +48,28 @@ class ContributedViewModelsTest {
4448
)
4549
}
4650

51+
@Test
52+
fun `the four dialogs that need SavedStateHandle are in the assisted map`() {
53+
// They cannot be in the plain map: that one builds from the graph alone, and these four take a
54+
// SavedStateHandle that only the caller has. `MetroViewModelFactory` checks the assisted map
55+
// first, so being in the wrong one is not a compile error - it is "no binding for view model"
56+
// when the dialog opens.
57+
val root = testRoot()
58+
59+
assertThat(root.assistedFactoryProviders.keys).containsAtLeast(
60+
CareDialogViewModel::class,
61+
FillDialogViewModel::class,
62+
SceneWizardViewModel::class,
63+
WizardDialogViewModel::class
64+
)
65+
assertThat(root.viewModelProviders.keys).containsNoneOf(
66+
CareDialogViewModel::class,
67+
FillDialogViewModel::class,
68+
SceneWizardViewModel::class,
69+
WizardDialogViewModel::class
70+
)
71+
}
72+
4773
@Test
4874
fun `a view model is NOT scoped - each screen gets its own`() {
4975
// A view model holds one screen's state. Scoping it would hand the next screen the previous

ui/src/main/kotlin/app/aaps/ui/compose/careDialog/CareDialogScreen.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ import androidx.compose.ui.platform.LocalFocusManager
4242
import androidx.compose.ui.res.stringResource
4343
import androidx.compose.ui.semantics.Role
4444
import androidx.compose.ui.unit.dp
45-
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
4645
import androidx.lifecycle.compose.collectAsStateWithLifecycle
46+
import app.aaps.core.ui.compose.metroViewModel
4747
import app.aaps.core.data.configuration.Constants
4848
import app.aaps.core.data.format.NumberFormat
4949
import app.aaps.core.data.model.GlucoseUnit
@@ -65,7 +65,7 @@ import app.aaps.core.ui.R as CoreUiR
6565

6666
@Composable
6767
fun CareDialogScreen(
68-
viewModel: CareDialogViewModel = hiltViewModel(),
68+
viewModel: CareDialogViewModel = metroViewModel(),
6969
onNavigateBack: () -> Unit,
7070
onPickSiteLocation: () -> Unit = {},
7171
siteLocationResult: Pair<String?, String?>? = null

ui/src/main/kotlin/app/aaps/ui/compose/careDialog/CareDialogViewModel.kt

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package app.aaps.ui.compose.careDialog
22

33
import androidx.compose.runtime.Stable
44
import androidx.lifecycle.SavedStateHandle
5+
import androidx.lifecycle.createSavedStateHandle
6+
import androidx.lifecycle.viewmodel.CreationExtras
57
import androidx.lifecycle.ViewModel
68
import androidx.lifecycle.viewModelScope
79
import app.aaps.core.data.model.GlucoseUnit
@@ -29,19 +31,23 @@ import app.aaps.core.keys.IntKey
2931
import app.aaps.core.keys.interfaces.Preferences
3032
import app.aaps.core.ui.compose.siteRotation.BodyType
3133
import app.aaps.ui.R
32-
import dagger.hilt.android.lifecycle.HiltViewModel
34+
import dev.zacsweers.metro.AppScope
35+
import dev.zacsweers.metro.Assisted
36+
import dev.zacsweers.metro.AssistedFactory
37+
import dev.zacsweers.metro.AssistedInject
38+
import dev.zacsweers.metro.ContributesIntoMap
39+
import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactory
40+
import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactoryKey
3341
import kotlinx.coroutines.CoroutineScope
3442
import kotlinx.coroutines.flow.MutableStateFlow
3543
import kotlinx.coroutines.flow.StateFlow
3644
import kotlinx.coroutines.flow.asStateFlow
3745
import kotlinx.coroutines.flow.update
3846
import kotlinx.coroutines.launch
39-
import javax.inject.Inject
4047

41-
@HiltViewModel
4248
@Stable
43-
class CareDialogViewModel @Inject constructor(
44-
savedStateHandle: SavedStateHandle,
49+
class CareDialogViewModel @AssistedInject constructor(
50+
@Assisted savedStateHandle: SavedStateHandle,
4551
private val persistenceLayer: PersistenceLayer,
4652
private val batchExecutor: BatchExecutor,
4753
private val profileFunction: ProfileFunction,
@@ -266,4 +272,20 @@ class CareDialogViewModel @Inject constructor(
266272
CareportalEventType.QUESTION -> Sources.Question
267273
CareportalEventType.ANNOUNCEMENT -> Sources.Announcement
268274
}
275+
276+
/**
277+
* The same split assisted injection expresses everywhere: [SavedStateHandle] comes from the
278+
* caller, everything else from the graph. `MetroViewModelFactory` tries the assisted map before
279+
* the plain one, so the screen still calls `metroViewModel()` and nothing at the call site says
280+
* this view model is built differently.
281+
*/
282+
@ContributesIntoMap(AppScope::class)
283+
@ViewModelAssistedFactoryKey(CareDialogViewModel::class)
284+
@AssistedFactory
285+
fun interface Factory : ViewModelAssistedFactory {
286+
287+
override fun create(extras: CreationExtras): CareDialogViewModel = create(extras.createSavedStateHandle())
288+
289+
fun create(@Assisted savedStateHandle: SavedStateHandle): CareDialogViewModel
290+
}
269291
}

ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogScreen.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ import androidx.compose.ui.platform.LocalFocusManager
4848
import androidx.compose.ui.res.stringResource
4949
import androidx.compose.ui.text.style.TextAlign
5050
import androidx.compose.ui.unit.dp
51-
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
5251
import androidx.lifecycle.compose.collectAsStateWithLifecycle
52+
import app.aaps.core.ui.compose.metroViewModel
5353
import app.aaps.core.data.format.NumberFormat
5454
import app.aaps.core.data.model.ICfg
5555
import app.aaps.core.data.model.TE
@@ -83,7 +83,7 @@ import app.aaps.core.ui.R as CoreUiR
8383
*/
8484
@Composable
8585
fun FillDialogScreen(
86-
viewModel: FillDialogViewModel = hiltViewModel(),
86+
viewModel: FillDialogViewModel = metroViewModel(),
8787
fillButtonsDef: PreferenceSubScreenDef,
8888
onNavigateBack: () -> Unit,
8989
onPickSiteLocation: () -> Unit = {},

ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogViewModel.kt

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package app.aaps.ui.compose.fillDialog
22

33
import androidx.compose.runtime.Stable
44
import androidx.lifecycle.SavedStateHandle
5+
import androidx.lifecycle.createSavedStateHandle
6+
import androidx.lifecycle.viewmodel.CreationExtras
57
import androidx.lifecycle.ViewModel
68
import androidx.lifecycle.viewModelScope
79
import app.aaps.core.data.format.NumberFormat
@@ -38,7 +40,13 @@ import app.aaps.core.objects.constraints.ConstraintObject
3840
import app.aaps.core.objects.extensions.observeChange
3941
import app.aaps.core.ui.clientcontrol.failText
4042
import app.aaps.ui.R
41-
import dagger.hilt.android.lifecycle.HiltViewModel
43+
import dev.zacsweers.metro.AppScope
44+
import dev.zacsweers.metro.Assisted
45+
import dev.zacsweers.metro.AssistedFactory
46+
import dev.zacsweers.metro.AssistedInject
47+
import dev.zacsweers.metro.ContributesIntoMap
48+
import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactory
49+
import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactoryKey
4250
import kotlinx.coroutines.CoroutineScope
4351
import kotlinx.coroutines.channels.BufferOverflow
4452
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -51,15 +59,13 @@ import kotlinx.coroutines.flow.launchIn
5159
import kotlinx.coroutines.flow.onEach
5260
import kotlinx.coroutines.flow.update
5361
import kotlinx.coroutines.launch
54-
import javax.inject.Inject
5562
import kotlin.math.abs
5663
import app.aaps.core.interfaces.R as InterfacesR
5764
import app.aaps.core.ui.R as CoreUiR
5865

59-
@HiltViewModel
6066
@Stable
61-
class FillDialogViewModel @Inject constructor(
62-
savedStateHandle: SavedStateHandle,
67+
class FillDialogViewModel @AssistedInject constructor(
68+
@Assisted savedStateHandle: SavedStateHandle,
6369
private val constraintChecker: ConstraintsChecker,
6470
activePlugin: ActivePlugin,
6571
private val persistenceLayer: PersistenceLayer,
@@ -451,4 +457,20 @@ class FillDialogViewModel @Inject constructor(
451457
fun decimalFormat(): NumberFormat =
452458
decimalFormatter.pumpSupportedBolusFormat(uiState.value.bolusStep)
453459

460+
461+
/**
462+
* The same split assisted injection expresses everywhere: [SavedStateHandle] comes from the
463+
* caller, everything else from the graph. MetroViewModelFactory tries the assisted map before the
464+
* plain one, so the screen still calls metroViewModel() and nothing at the call site says this
465+
* view model is built differently.
466+
*/
467+
@ContributesIntoMap(AppScope::class)
468+
@ViewModelAssistedFactoryKey(FillDialogViewModel::class)
469+
@AssistedFactory
470+
fun interface Factory : ViewModelAssistedFactory {
471+
472+
override fun create(extras: CreationExtras): FillDialogViewModel = create(extras.createSavedStateHandle())
473+
474+
fun create(@Assisted savedStateHandle: SavedStateHandle): FillDialogViewModel
475+
}
454476
}

ui/src/main/kotlin/app/aaps/ui/compose/scenes/wizard/SceneWizardScreen.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import androidx.compose.runtime.getValue
2020
import androidx.compose.ui.Modifier
2121
import androidx.compose.ui.platform.LocalFocusManager
2222
import androidx.compose.ui.res.stringResource
23-
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
2423
import androidx.lifecycle.compose.collectAsStateWithLifecycle
24+
import app.aaps.core.ui.compose.metroViewModel
2525
import app.aaps.core.ui.R
2626
import app.aaps.core.ui.compose.AapsSpacing
2727
import app.aaps.core.ui.compose.AapsTopAppBar
@@ -31,7 +31,7 @@ import app.aaps.core.ui.compose.pump.StepProgressIndicator
3131
fun SceneWizardScreen(
3232
onFinished: () -> Unit,
3333
onCancel: () -> Unit,
34-
viewModel: SceneWizardViewModel = hiltViewModel()
34+
viewModel: SceneWizardViewModel = metroViewModel()
3535
) {
3636
val state by viewModel.state.collectAsStateWithLifecycle()
3737
val chainTargets by viewModel.availableChainTargets.collectAsStateWithLifecycle()

ui/src/main/kotlin/app/aaps/ui/compose/scenes/wizard/SceneWizardViewModel.kt

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package app.aaps.ui.compose.scenes.wizard
22

33
import androidx.compose.runtime.Stable
44
import androidx.lifecycle.SavedStateHandle
5+
import androidx.lifecycle.createSavedStateHandle
6+
import androidx.lifecycle.viewmodel.CreationExtras
57
import androidx.lifecycle.ViewModel
68
import androidx.lifecycle.viewModelScope
79
import app.aaps.core.data.model.RM
@@ -22,7 +24,13 @@ import app.aaps.core.keys.interfaces.Preferences
2224
import app.aaps.core.objects.extensions.profileNames
2325
import app.aaps.core.objects.extensions.toScenes
2426
import app.aaps.ui.compose.scenes.SceneTemplate
25-
import dagger.hilt.android.lifecycle.HiltViewModel
27+
import dev.zacsweers.metro.AppScope
28+
import dev.zacsweers.metro.Assisted
29+
import dev.zacsweers.metro.AssistedFactory
30+
import dev.zacsweers.metro.AssistedInject
31+
import dev.zacsweers.metro.ContributesIntoMap
32+
import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactory
33+
import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactoryKey
2634
import kotlinx.coroutines.flow.MutableStateFlow
2735
import kotlinx.coroutines.flow.SharingStarted
2836
import kotlinx.coroutines.flow.StateFlow
@@ -31,12 +39,10 @@ import kotlinx.coroutines.flow.map
3139
import kotlinx.coroutines.flow.stateIn
3240
import kotlinx.coroutines.flow.update
3341
import java.util.UUID
34-
import javax.inject.Inject
3542

36-
@HiltViewModel
3743
@Stable
38-
class SceneWizardViewModel @Inject constructor(
39-
savedStateHandle: SavedStateHandle,
44+
class SceneWizardViewModel @AssistedInject constructor(
45+
@Assisted savedStateHandle: SavedStateHandle,
4046
private val sceneRepository: SceneStore,
4147
private val profileRepository: ProfileRepository,
4248
private val profileUtil: ProfileUtil,
@@ -290,4 +296,20 @@ class SceneWizardViewModel @Inject constructor(
290296
sceneRepository.saveScene(scene)
291297
return true
292298
}
299+
300+
/**
301+
* The same split assisted injection expresses everywhere: [SavedStateHandle] comes from the
302+
* caller, everything else from the graph. MetroViewModelFactory tries the assisted map before the
303+
* plain one, so the screen still calls metroViewModel() and nothing at the call site says this
304+
* view model is built differently.
305+
*/
306+
@ContributesIntoMap(AppScope::class)
307+
@ViewModelAssistedFactoryKey(SceneWizardViewModel::class)
308+
@AssistedFactory
309+
fun interface Factory : ViewModelAssistedFactory {
310+
311+
override fun create(extras: CreationExtras): SceneWizardViewModel = create(extras.createSavedStateHandle())
312+
313+
fun create(@Assisted savedStateHandle: SavedStateHandle): SceneWizardViewModel
314+
}
293315
}

ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogScreen.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ import androidx.compose.ui.platform.LocalFocusManager
7070
import androidx.compose.ui.res.stringResource
7171
import androidx.compose.ui.text.font.FontWeight
7272
import androidx.compose.ui.unit.dp
73-
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
7473
import androidx.lifecycle.compose.collectAsStateWithLifecycle
74+
import app.aaps.core.ui.compose.metroViewModel
7575
import app.aaps.core.data.configuration.Constants
7676
import app.aaps.core.data.format.NumberFormat
7777
import app.aaps.core.interfaces.navigation.ElementType
@@ -104,7 +104,7 @@ import app.aaps.core.ui.R as CoreUiR
104104

105105
@Composable
106106
fun WizardDialogScreen(
107-
viewModel: WizardDialogViewModel = hiltViewModel(),
107+
viewModel: WizardDialogViewModel = metroViewModel(),
108108
wizardSettingsDef: PreferenceSubScreenDef,
109109
onNavigateBack: () -> Unit,
110110
onShowDeliveryError: (String) -> Unit

ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogViewModel.kt

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package app.aaps.ui.compose.wizardDialog
22

33
import androidx.compose.runtime.Stable
44
import androidx.lifecycle.SavedStateHandle
5+
import androidx.lifecycle.createSavedStateHandle
6+
import androidx.lifecycle.viewmodel.CreationExtras
57
import androidx.lifecycle.ViewModel
68
import androidx.lifecycle.viewModelScope
79
import app.aaps.core.data.time.T
@@ -41,7 +43,13 @@ import app.aaps.core.objects.runningMode.RunningModeGuard
4143
import app.aaps.core.objects.wizard.BolusWizard
4244
import app.aaps.core.ui.clientcontrol.failText
4345
import app.aaps.core.ui.compose.icons.IcCalculator
44-
import dagger.hilt.android.lifecycle.HiltViewModel
46+
import dev.zacsweers.metro.AppScope
47+
import dev.zacsweers.metro.Assisted
48+
import dev.zacsweers.metro.AssistedFactory
49+
import dev.zacsweers.metro.AssistedInject
50+
import dev.zacsweers.metro.ContributesIntoMap
51+
import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactory
52+
import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactoryKey
4553
import kotlinx.coroutines.CoroutineScope
4654
import kotlinx.coroutines.channels.BufferOverflow
4755
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -52,14 +60,12 @@ import kotlinx.coroutines.flow.asSharedFlow
5260
import kotlinx.coroutines.flow.asStateFlow
5361
import kotlinx.coroutines.flow.update
5462
import kotlinx.coroutines.launch
55-
import javax.inject.Inject
5663
import javax.inject.Provider
5764
import kotlin.math.abs
5865

59-
@HiltViewModel
6066
@Stable
61-
class WizardDialogViewModel @Inject constructor(
62-
private val savedStateHandle: SavedStateHandle,
67+
class WizardDialogViewModel @AssistedInject constructor(
68+
@Assisted private val savedStateHandle: SavedStateHandle,
6369
private val bolusWizardProvider: Provider<BolusWizard>,
6470
private val constraintChecker: ConstraintsChecker,
6571
private val profileFunction: ProfileFunction,
@@ -540,4 +546,20 @@ class WizardDialogViewModel @Inject constructor(
540546
}
541547
}
542548
}
549+
550+
/**
551+
* The same split assisted injection expresses everywhere: [SavedStateHandle] comes from the
552+
* caller, everything else from the graph. MetroViewModelFactory tries the assisted map before the
553+
* plain one, so the screen still calls metroViewModel() and nothing at the call site says this
554+
* view model is built differently.
555+
*/
556+
@ContributesIntoMap(AppScope::class)
557+
@ViewModelAssistedFactoryKey(WizardDialogViewModel::class)
558+
@AssistedFactory
559+
fun interface Factory : ViewModelAssistedFactory {
560+
561+
override fun create(extras: CreationExtras): WizardDialogViewModel = create(extras.createSavedStateHandle())
562+
563+
fun create(@Assisted savedStateHandle: SavedStateHandle): WizardDialogViewModel
564+
}
543565
}

0 commit comments

Comments
 (0)