With state flow I can use
val items by myViewModel.items.collectAsState()
I suppose shared flow cannot be used this way. Are shared flows even applicable for Compose?
SharedFlow should be used for one-shot events(navigation, toast, etc... ).
So this is the way to collect a SharedFlow:
#Composable
fun <T> Flow<T>.collectAsEffect(
context: CoroutineContext = EmptyCoroutineContext,
block: (T) -> Unit
) {
LaunchedEffect(key1 = Unit) {
onEach(block).flowOn(context).launchIn(this)
}
}
Technically you can collect it as state as any other Flow - with an initial value:
flow.collectAsState(initial = 0)
This state will have the last value emitted by the flow during the time of view being presented, or the initial value. I'm not sure this makes much sense, though.
But you can also use it as a way to deliver events that require a one-time response, as shown in this answer.
Related
In the following code, which shows two different public uiState handler, uiState1 and uiState, what is the difference between the two scenarios?
// UI state exposed to the UI
// Scenario 1
private val _uiState1: MutableStateFlow<InboxUiState> = MutableStateFlow(InboxUiState.default)
val uiState1 = _uiState1.asStateFlow()
// Scenario 2
private val _uiState: MutableStateFlow<InboxUiState> = MutableStateFlow(InboxUiState.default)
val uiState = _uiState
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
InboxUiState.default
)
where asStateFlow() represents a modifiable state flow as a read-only stateflow, stateIn() him converts a cold Flow into a hot StateFlow that is started in the given coroutine scope.
The stateIn operator is useful in situations when there is a cold flow that provides updates to the value of some state and is expensive to create and/or to maintain, but there are multiple subscribers that need to collect the most recent state value.
stateIn : https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html
asStateFlow : https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/as-state-flow.html
I'm working on implementing MVI using compose. In order for me to follow the proper event loop, I need to propagate clicks events through my view model and then observe side effects. I have looked at a few implementations and they all use LaunchedEffect(true) to observe side effects and take actions.
I have a similar setup for example:
#Composable
fun HelloComposeScreen(
viewModel: MyViewModel = hiltViewModel(),
onClickedNext: () -> Unit
) {
LaunchedEffect(true) {
viewModel.sideEffect.collectLatest { sideEffect ->
when (sideEffect) {
DashboardSideEffect.CreateParty -> onClickedNext()
}
}
}
Button(
onClick = { viewModel.onEvent(UserEvent.ClickedButton)},
) {
Text("Click Me")
}
}
This results in me using LaunchedEffect(true) for any screen that has navigation or one time events but the official documentation has this warning
Warning: LaunchedEffect(true) is as suspicious as a while(true). Even though there are valid use cases for it, always pause and make sure that's what you need.
My questions are:
When exactly does the LaunchedEffect get canceled? The documentation says that it matches the lifecycle of the call site. Is that the composition in this case?
Considering that the official documentation has a warning there? Should I not be using this LaunchedEffect(true) setup for observing side effects through my project? What would be an alternative?
The LaunchedEffect is canceled along with its coroutine in two variants:
The passed key argument(s) is changed - in this case the current LaunchedEffect will be cancelled and a new one will be created.
LaunchedEffect is removed from the life tree, for example, in case you put it (or its parent at any level) in an if block and the condition becomes false.
If you do not need to pass any key that should restart LaunchedEffect, you can pass Unit. Any other constant, like true in your case, is considered suspect because it cannot be changed at runtime and yet may look like complex logic to any coder.
The LacunchedEffect is a Composable function and it runs coroutines in a coroutineScope.
The coroutineScope will be canceled and restarted in two cases:
When the passed keys to LaunchedEffect gets changed. Changing a passed key from value x to y cancels the current coroutineScope, and then launching the block of code inside LaunchedEffect again with a new passed keys.
When the LaucnhedEffect exits the composition. That means in a later composition if the LaucnhedEffect does not recompose. For example, because it's inside an if statement that gets evaluated as false or if one of the parent composables in the composition tree exits the composition.
Example:
#Composable
fun MyComposable(authorId: Int, showReadMore: Boolean) {
// ... logic....
// When showReadMore is false, the latest LaunchedEffect composable exits the composition (the coroutineScope will be cancelled)
if (showReadMore) {
// Changing the value of authorId when showReadMore is true, cancels the coroutineScope and launch the block again.
LaunchedEffect(authorId) {
// Get more info of the author using suspend function
// Since we use LaunchedEffect to run suspend function(s) inside
}
}
}
For the second question: Passing any value like false, true, 1, 2, and Unit gives the same result. Passing Unit makes the code more sense and easier to read because it indicates void which means that we don't care about restarting the coroutineScope in the first case (when keys changes) because the keys are void.
I have read the article,
It seems that State<T> is designed for #Composable.
Is it better to use State<T> in other classes such as ViewModel?
Yes, being part of the androidx.compose.runtime package State<T> was indeed intended as a value holder for composables.
If you want to publish/emit and consume "states" within ViewModels or Composables you might want to take a look at StateFlow and SharedFlow
You can either collect those as you would with any kotlin Flow<T> and use collectAsState within compose functions.
#Composable
fun YourComposable() {
val myState by viewModel.stats.collectAsState()
}
States trigger recomposable, for each screen I've always used custom data class (if it's necessary) and wrap it inside mutableStateOf(YourDataClass()) and place it in ViewModel just like we always use LiveData. And in your screen (composable) you can just val yourState = viewModel.yourState.value.
For a complete example
// ViewModel
private val _yourState: MutableState<AnimeTopState> = mutableStateOf(YourState())
val yourState: State<YourState> = _yourState
// ViewModel
// Composable
val yourState = viewModel.yourState.value
// Composable
So, state is like the way to trigger view changes on #Composable function, we cant just trigger view change with LiveData or normal value like the way we used to with XML view.
I just started learning jetpack compose. I have a very basic question. My ViewModel has a SingleLiveEvent that I use to navigate to another screen.
private val _navigateToDetails: SingleLiveEvent<Movie> = SingleLiveEvent()
val navigateToDetails: MutableLiveData<Movie> = _navigateToDetails
I know that I can use Livedata as state to emit UI but how to use it to trigger some action within composable.
Previously I had used viewLifecycleOwner to observer the state as anyone would do like this.
viewModel.navigateToDetails.observe(viewLifecycleOwner) {
// navigate to details
}
How do I achieve the same thing in compose. I don't know if that's possible or not. Maybe I am not thinking this in compose way. Any help would be appreciated.
I would do something like to make sure I'm only doing it once:
#Composable
fun LoginScreen(viewModel: LoginViewModel) {
val loginState by viewModel.loginState.observeAsState(null)
val hasHandledNavigation = remember { mutableStateOf(false)}
if (loginState != null && !hasHandledNavigation.value ) {
navigateToWelcomeScreen()
else {
// rest of the Compose UI
}
}
UPDATE:
Option two, you can also just pass the action of going to next screen to viewmodel and fire it up there.
Actually, in compose we use mutableStateOf() over LiveData. In your viewmodel, you can change the type of the data holder from LiveData to mutableStateOf(...) which will allow you to directly use it in your Composables without explicitly calling observe()
Let's say you wish to store an integer of any kind in your viewmodel and update the state of your Composable based on that.
In your viewmodel,
var mInteger by mutableStateOf (0) //'by' helps treat state as data
fun updateMInteger(newValue: Int){
mInteger = newValue
}
In your Composable, directly call viewmodel.mInteger and Compose being built like that, automatically updates the UI, given that mInteger is being read from the Composable
Like
Text(viewModel.mInteger)
My particular implementation concerns the use of kotlin flows in Android, but I guess this is applicable to kotlin in general.
What I would like to do is to set my SharedFlow to be started according to the SharingStarted.WhileSubscribed() started policy, so that the flow materializes only when the number of subscribers is greater than zero.
The recommended way to setup such a flow from the android official guide is to use the shareIn operator:
val latestNews: Flow<List<ArticleHeadline>> = flow {
...
// emit() here
}.shareIn(
externalScope,
replay = 1,
started = SharingStarted.WhileSubscribed()
)
In my case, I want to emit only under specific conditions that are independent from the flow itself, so it is unpractical to emit inside the flow{ ... } body. As a consequence, I created a MutableSharedFlow and use tryEmit to emit whenever I need to, for example upon a method call:
// Backing property to avoid flow emissions from other classes
private val _tickFlow = MutableSharedFlow<Int>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val tickFlow: SharedFlow<Int> = _tickFlow
// called after domain logic deciding which number to emit
fun timeToEmit(num:Int){
_tickerFlow.tryEmit(num)
}
What is the SharingStarted policy (if any) of a flow created through the MutableSharedFlow<>() constructor?
How can I set this flow SharingStarted property to be started (materialized) only when the number of subscribers is greater than 0?