-
Notifications
You must be signed in to change notification settings - Fork 105
Open
Description
(Apologies if this should be an issue for Turbine)
I have the following BaseViewModel class which is a lot like the class in the sample-viewmodel folder:
abstract class BaseViewModel<Event, Model>(
private val backgroundScope: CoroutineScope,
private val recompositionMode: RecompositionMode,
)
{
private val events = MutableSharedFlow<Event>(extraBufferCapacity = 20)
val models: StateFlow<Model> by lazy(LazyThreadSafetyMode.NONE) {
println("starting compose runtime")
backgroundScope.launchMolecule(mode = recompositionMode) {
models(events)
}
}
fun take(event: Event) {
println("Taking event $event")
if(!events.tryEmit(event)) {
error("Event buffer overflow")
}
}
@Composable
protected abstract fun models(events: Flow<Event>): Model
}Note that BaseViewModel does not extend from the AAC ViewModel.
Here is an implementation of BaseViewModel+ composable presenter:
class PetListViewModel @AssistedInject constructor(
private val petDb: PetDb,
private val ioDispatcher: CoroutineDispatcher,
@Assisted scope: CoroutineScope,
@Assisted recompositionMode: RecompositionMode,
): BaseViewModel<PetListEvent, PetListUiState>(scope, recompositionMode)
{
private val viewModelState = MutableStateFlow(PetListUiState())
init {
viewModelState.update {
it.copy(
pets = petDb.petQueries.selectAll().executeAsList().map { Pet(id = it._id, name = it.name) }
)
}
}
@Composable
override fun models(events: Flow<PetListEvent>): PetListUiState {
return PetListPresenter(events = events, petDb, ioDispatcher)
}
@AssistedFactory
interface Factory
{
fun create(scope: CoroutineScope, recompositionMode: RecompositionMode): PetListViewModel
}
}
@Composable
fun PetListPresenter(events: Flow<PetListEvent>, petDb: PetDb, ioDispatcher: CoroutineDispatcher): PetListUiState {
var counter by remember { mutableStateOf(0) }
var selectedPet: Pet? by remember { mutableStateOf(null) }
val pets by remember {
petDb.petQueries.selectAll().asFlow()
.mapToList(ioDispatcher)
.map {
it.map { Pet(id = it._id, name = it.name) }
}
}.collectAsState(initial = emptyList())
LaunchedEffect(events) {
events.collect { event ->
print("Received $event")
when(event) {
is PetListEvent.PetSelected -> {
println("Changing selected pet")
selectedPet = event.pet
}
}
}
}
return PetListUiState(
pets = pets,
selectedPet = selectedPet,
)
}This works great inside an actual Android app, but testing it is proving to be a pain with the following failing test:
@Test
fun `selectedPet gets updated after selecting a pet from the list using viewmodel`() = runTest(timeout = 500.milliseconds) {
val testDispatcher = StandardTestDispatcher(testScheduler)
val testScope = TestScope(testScheduler)
val viewModel = PetListViewModel(petDb, testDispatcher, testScope, RecompositionMode.Immediate)
viewModel.models.test {
println("marker 1")
assertEquals(null, awaitItem().selectedPet)
viewModel.take(PetListEvent.PetSelected(Pet(0, "Sparky")))
println("marker 2")
assertEquals(Pet(0, "Sparky"), awaitItem().selectedPet)
println("marker 3")
}
}Which fails with this message:
Expected :Pet(id=0, name=Sparky)
Actual :null
Here's a test that succeeds, skipping the PetListViewModel entirely and using the composable function directly:
@Test
fun `selectedPet gets updated after selecting a pet from the list`() = runTest {
val testDispatcher = StandardTestDispatcher(testScheduler)
val testScope = TestScope(testScheduler)
val events = Channel<PetListEvent>()
testScope.launchMolecule(RecompositionMode.Immediate) {
PetListPresenter(events = events.receiveAsFlow(), petDb = petDb, testDispatcher)
}.test {
println("marker 1")
assertEquals(null, awaitItem().selectedPet)
events.send(PetListEvent.PetSelected(Pet(0, "Sparky")))
println("marker 2")
assertEquals(Pet(0, "Sparky"), awaitItem().selectedPet)
println("marker 3")
}
}Am I using a wrong CoroutineScope here? I've tried using TestScope and CoroutineScope but no luck. I should note that the PetListViewModel works great inside an Android app with the following instantiation:
private val viewModel: PetListViewModel by retain { entry ->
petListViewModelFactory.get().create(CoroutineScope(entry.scope.coroutineContext + AndroidUiDispatcher.Main), RecompositionMode.ContextClock)
// PetListViewModel(petDb, Dispatchers.IO, entry.scope.coroutineContext + AndroidUiDispatcher.Main)
}Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels