escodro
02/26/2021, 1:20 PMFlow
inside a ViewModel
that is notified in every time a change is made in my database and reflect the state via sealed class
to my composable. This Flow
is attached to the viewModelScope
.
Basically I’m facing two problems:
1. Every time the screen is recomposed, the function to load the data is called and a new flow is registered and listening to changes.
2. When I’m navigating in another composables that handles the same data, the Flow
keeps emitting data changes for the composable that is no longer visible.
Is there a better way to implement it? Should I use my own scope instead the one from ViewModel
? If so, how can I “dispose” the scope when the Composable is no longer visible?
@Composable
private fun TaskListLoader(viewModel: TaskListViewModel = viewModel()) {
viewModel.loadTasks()
val viewState by viewModel.state.collectAsState()
TaskScaffold(...)
}
fun loadTasks() = viewModelScope.launch {
loadAllTasksUseCase().collect { tasks ->
// emit via StateFlow
}
}
Thanks a lot in advance! ❤️allan.conda
02/26/2021, 1:24 PMviewModel.loadTasks()
every recompose so it repeats infinitelyViewModel.init {}
escodro
02/26/2021, 1:28 PMviewModel.loadTasks()
and TaskScaffold
in different functions?allan.conda
02/26/2021, 1:28 PMescodro
02/26/2021, 1:29 PMinit{}
but there are some functions that needs params.BenjO
02/26/2021, 1:30 PMLaunchedEffect
escodro
02/26/2021, 1:45 PM@Composable
private fun TaskListLoader(viewModel: TaskListViewModel = viewModel()) {
LaunchedEffect(viewModel) {
viewModel.loadTasks()
}
val viewState by viewModel.state.collectAsState()
TaskScaffold(...)
}
I made the ViewModel
function suspend and only loading it again if the ViewModel changes.allan.conda
02/26/2021, 2:08 PMAdam Powell
02/26/2021, 3:32 PMviewModel.state
emit when needed and handle the cold subscription properties here?viewModel.loadTasks()
seems like it should be redundant with viewModel.state.collectAsState()
escodro
02/26/2021, 4:42 PMloadTasks
inside init{}
and only expose the State
?Adam Powell
02/26/2021, 4:48 PMstate
Flow
would handle it as a result of handling the collect operation. ViewModels that launch in their init blocks are creepy for a whole host of other reasons before Compose even enters the pictureMutableStateFlow
that other things write into arbitrarily, use a standard flow
or channelFlow
combined with .stateIn(viewModelScope, SharingStarted.WhileSubscribed, initialValue)
flow
or channelFlow
will start lazily when at least one subscriber is present and stop when all subscribers go away. Launching in a ViewModel's init block will stay running for a very long time, whether or not the app is even in the foregroundMutable[State/Shared]Flow
now, and it's going to result in the same kind of pushback 🙂BenjO
02/26/2021, 5:04 PMAdam Powell
02/26/2021, 5:13 PMescodro
02/26/2021, 5:50 PMval state: Flow<TaskListViewState> = flow {
loadAllTasksUseCase()
.map { task -> taskWithCategoryMapper.toView(task) }
.catch { error -> emit(TaskListViewState.Error(error)) }
.collect { tasks ->
val result = if (tasks.isNotEmpty()) {
TaskListViewState.Loaded(tasks)
} else {
TaskListViewState.Empty
}
emit(result)
}
}
@Composable
private fun TaskListLoader(viewModel: TaskListViewModel = viewModel()) {
val viewState by viewModel.state.collectAsState(initial = TaskListViewState.Empty)
TaskListScaffold(...)
}
Flow<Task>
and a Composable
. I want the Composable
to observe the Flow
, without duplication or observing it forever… It shouldn’t be so complex. However we receive so much missed signals, that this simple implementation becomes a nightmare.State
, other uses StateFlow
, now you introduced me to Flow
for this scenario (if my implementation is correct). I’m sure if I search hard enough I will find some examples with LiveData
.Adam Powell
02/26/2021, 7:21 PM.getValue()
always returns null
if no one ever subscribes, and the entire series of hacks and kludges around people trying to use LiveData for events.escodro
02/26/2021, 7:27 PMsealed class
representing all the possible states from my View. It may or may not receive a parameter (like load all tasks with category id) and may or may not be a Flow in the database.Adam Powell
02/26/2021, 7:29 PM@Composable fun MyWidget(value: ValueType, onValueChange: (ValueType) -> Unit)
then great; if it starts getting more complicated then I tend to start grouping those considerations into a hoisted state object of sortsby mutableStateOf(...)
properties. If the data source needs to know when something is observing it because it may be expensive to maintain ongoing updates otherwise, I tend to use Flow<T>
suspend fun
- stay away from anything that is fire and forget, since it's hard to scope or cancel.escodro
02/26/2021, 7:49 PMTony Kazanjian
02/26/2021, 7:50 PMinit{}
. If I simply just need to fetch data and return a result without the need for any hot data, is it overkill to use the
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Unit)
operator for a flow in my viewmodel (which would then be collected in LaunchedEffect
? Seems like that way is still expecting hot data. Previously I was just listening to a StateFlow
for the result and then collectAsState
for the value in the composable. That way seemed a lot cleaner, and as long as I use LaunchedEffect
, I wouldn't have to worry about the flow still running, right?allan.conda
02/26/2021, 7:56 PMAdam Powell
02/26/2021, 8:42 PMStateFlow
you listen to? If there's a cold data source backing it you have to scope that somehowTony Kazanjian
02/26/2021, 8:48 PMAdam Powell
02/26/2021, 8:52 PMTony Kazanjian
02/26/2021, 9:20 PMflow
instead of a StateFlow
, correct?Adam Powell
02/26/2021, 10:03 PMBenjO
02/27/2021, 5:27 PMescodro
03/01/2021, 2:10 PMStateFlow
and State
to cold Flow
and I’d like your opinions. 😊
A example of a simplified ViewModel:
fun setTaskInfo(taskId: TaskId): Flow<TaskDetailState> = flow {
val task = loadTaskUseCase(taskId = taskId.value)
if (task != null) {
emit(TaskDetailState.Loaded(task))
} else {
emit(TaskDetailState.Error)
}
}
fun updateTitle(taskId: TaskId, title: String) {
updateTaskTitle(taskId.value, title)
}
In the composable I simply:
val detailViewState by detailViewModel
.setTaskInfo(taskId = id)
.collectAsState(initial = TaskDetailState.Loading)
Now basically all my “load data” functions returns a Flow
that is only observed during the Composable lifecycle. If something needs to be updated in the ViewModel, it passes the id again (onde it already has the reference).
What do you think? 😬Adam Powell
03/01/2021, 2:36 PMsetFoo
is a bit strange, I'd try to find another name for itval detailViewState by remember(detailViewModel, id) { detailViewModel.newName(id) }
.collectAsState(initial = ...)
allan.conda
03/01/2021, 2:41 PMescodro
03/01/2021, 2:42 PMAdam Powell
03/01/2021, 2:45 PMescodro
03/01/2021, 2:54 PMremember
function for the Flow
, now the application seems great!
Everything is called once and the state are working smoothly! 😍allan.conda
03/01/2021, 7:29 PMescodro
03/01/2021, 7:38 PMStylianos Gakis
03/03/2021, 3:09 PMinit {}
of my ViewModels too. Is there no central source where all of this is a bit better explained just like we have this . I feel like so much good information gets hidden away inside these slack threads that 99.99% of the android devs won't ever see. (And I get FOMO and scroll slack too often too 😂)Adam Powell
03/03/2021, 3:15 PMStylianos Gakis
03/03/2021, 3:19 PMJason Ankers
03/04/2021, 10:07 AMval detailViewState by remember(detailViewModel, id) { detailViewModel.newName(id) }
.collectAsState(initial = ...)
Only collect once?
Wouldn’t the collectAsState
get called on every recompose since it’s outside the remember block?allan.conda
03/04/2021, 10:13 AMThere are some newer android+coroutines docs in the pipeline that should be published soon-ish@Adam Powell were you talking about this article? https://developer.android.com/kotlin/coroutines/coroutines-best-practices It’s still using MutableStateFlows in the examples, and somehow manage to avoid talking about where or how the
loadNews()
is supposed to be called 😄. It’s not specific to Compose and only in Compose it’s a bit vague how we’re supposed to execute the use cases.Adam Powell
03/04/2021, 4:17 PMViewModel
class shouldn't expose suspend functions" 🙂uiState
because the operation completed successfully but the result was the same, then you're asking your test to prove a negative, that an unwanted change will never come. How long do you wait to determine, "good enough" for that?allan.conda
03/04/2021, 4:48 PMAdam Powell
03/04/2021, 6:00 PMManuel Vivo
03/25/2021, 12:26 PMUiState
that’s exposed from the ViewModel comes from the very need of surviving configuration changes and avoid inconsistent UI states. Most experienced developers have been fighting with this for a long time already. This is why patterns like MVI have gained so much weight over the past years.
Obviously, we’ll need to see how all of this evolves with Compose and whether the current practices make sense anymore.
Also, it’s difficult to come up with recommendations because which kind of developer should we target? Beginners? The average developer? or people working on the Android Toolkit developers? IMO, having an init
block inside a ViewModel whose result you expose using a StateFlow
to the UI is safer than exposing a suspend function to load data and let the UI decide what to do with it because sooo many things can go wrong.tasksFlow
function looks great. But we’d also need to consider how that pattern scales when you have a much more complicated UIescodro
03/25/2021, 12:36 PMManuel Vivo
03/25/2021, 12:49 PM