What is the exact meaning of 'emit' in Android Jetpack Compose? - android

The word emit is often used in Jetpack Compose's documentation or codelabs, as follows:
The function doesn't return anything. Compose functions that "emit" UI do not need to return anything, because they describe the desired screen state instead of constructing UI widgets.
What is the exact meaning of emit in Android Jetpack Compose?
Who handles the UI emitted by the Compose function? Does the Compose framework detect and process the emitted UI?
Is there documentation with information on how and by whom the emitted UI is handled?

"Emit" means that Compose inserts a new group into the current composition.
See the source code:
#Suppress("NONREADONLY_CALL_IN_READONLY_COMPOSABLE", "UnnecessaryLambdaCreation")
#Composable inline fun <T : Any, reified E : Applier<*>> ReusableComposeNode(
noinline factory: () -> T,
update: #DisallowComposableCalls Updater<T>.() -> Unit
) {
if (currentComposer.applier !is E) invalidApplier()
currentComposer.startReusableNode() // <--- EMITTING THE NODE
if (currentComposer.inserting) {
currentComposer.createNode { factory() }
} else {
currentComposer.useNode()
}
currentComposer.disableReusing()
Updater<T>(currentComposer).update()
currentComposer.enableReusing()
currentComposer.endNode()
}

Related

Manage condition logic in stateless compose in jetpack compose

I am learning State hosting in jetpack compose. I have created two separated function ContentStateful and ContentStateLess. In my ContentStateLess there is a lot of view inside them and I am checking some condition and change view accordingly. I am guessing that there is no condition/business logic inside Stateful compose. So what is the proper way of doing this kind of logic in here.
ContentStateful
#Composable
fun ContentStateful(
viewModel: PairViewModel = getViewModel()
) {
ContentStateLess(viewModel)
}
ContentStateLess
#Composable
fun ContentStateLess(
viewModel: PairViewModel
) {
Text()
Text()
Image()
if (viewModel.isTrue) {
Image()
// more item here
} else {
Text()
// more item here
}
Image()
}
So what is the best recommendation for this if - else logic in ContentStateLess(). Many Thanks
If you are building stateless Composables it's better not to pass anything like ViewModel. You can pass Boolean parameter instead. When you wish to move your custom Composable to another screen or another project you will need to move ViewModel either.
The reason Google recommends stateless Composables is it's difficult to test, you can easily test a Composable with inputs only.
Another thing you experience the more states inner composables have to more exposure you create for your composable being in a State that you might not anticipate.
When you build simple Composables with one, two, three layers might not be an issue but with more states and layers state management becomes a serious issue. And if you somehow forget or miss a state inside a Composable you might end up with a behavior that's not expected. So to minimize risks and make your Composables testable you should aim to manage your states in one place and possible in a state holder class that wraps multiple states.
#Composable
fun ContentStateLess(
firstOneTrue: Boolean
) {
Text()
Text()
Image()
if (firstOneTrue) {
Image()
// more item here
} else {
Text()
// more item here
}
Image()
}

Use of LaunchedEffect vs SideEffect in jetpack compose

Hi guys I am learning side-effect in my project. I want to know when should I use LaunchedEffect and SideEffect in which scenario. I am adding some piece of code using both effect. Please lemme know if I am doing wrong here.
1st using LaunchedEffect, please guide me if we need effect on this function or not.
#Composable
fun BluetoothRequestContinue(multiplePermissionsState: MultiplePermissionsState) {
var launchPermission by remember { mutableStateOf(false) }
if (launchPermission) {
LaunchedEffect(Unit) {
multiplePermissionsState.launchMultiplePermissionRequest()
}
}
AbcMaterialButton(
text = stringResource(R.string.continue_text),
spacerHeight = 10.dp
) {
launchPermission = true
}
}
2nd using SideEffect to open setting using intent
#Composable
fun OpenPermissionSetting(router: Router = get()) {
val activity = LocalContext.current as Activity
var launchSetting by remember { mutableStateOf(false) }
if (launchSetting) {
SideEffect {
activity.startActivity(router.permission.getPermissionSettingsIntent(activity))
}
}
AbcMaterialButton(
text = stringResource(R.string.open_settings),
spacerHeight = 10.dp
) {
launchSetting = true
}
}
Please let me know if we need Effect or not. Also guide me if we need different effect as well. Thanks
There difference between
if (launchSetting) {
SideEffect {
// Do something
}
}
and
if (launchPermission) {
LaunchedEffect(Unit) {
multiplePermissionsState.launchMultiplePermissionRequest()
}
}
both enters recomposition when conditions are true but LaunchedEffect is only invoked once because its key is Unit. SideEffect is invoked on each recomposition as long as condition is true.
SideEffect function can be used for operations that should be invoked only when a successful recomposition happens
Recomposition starts whenever Compose thinks that the parameters of a
composable might have changed. Recomposition is optimistic, which
means Compose expects to finish recomposition before the parameters
change again. If a parameter does change before recomposition
finishes, Compose might cancel the recomposition and restart it with
the new parameter.
When recomposition is canceled, Compose discards the UI tree from the
recomposition. If you have any side-effects that depend on the UI
being displayed, the side-effect will be applied even if composition
is canceled. This can lead to inconsistent app state.
Ensure that all composable functions and lambdas are idempotent and
side-effect free to handle optimistic recomposition.
Sample from official docs
To share Compose state with objects not managed by compose, use the
SideEffect composable, as it's invoked on every successful
recomposition.
#Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
/* ... */
}
// On every successful composition, update FirebaseAnalytics with
// the userType from the current User, ensuring that future analytics
// events have this metadata attached
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
LaunchedEffect is for calling a function on composition or on recomposition if keys are changed.
If you write your LaunchedEffect as
LaunchedEffect(key1= launchPermission) {
if(launchPermission) {
// Do something
}
}
code block inside if will not be called in composition if key is not true but whenever it changes from false to true code block will be invoked. This is useful for one-shot operations that are not fired by user interaction directly or when an operation requires a CoroutineScope invoked after user interaction, animations or calling suspend functions such as lazyListState.animateScrollToItem()
Definition of concept of side-effect from Wikipedia
In computer science, an operation, function or expression is said to
have a side effect if it modifies some state variable value(s) outside
its local environment, which is to say if it has any observable effect
other than its primary effect of returning a value to the invoker of
the operation. Example side effects include modifying a non-local
variable, modifying a static local variable, modifying a mutable
argument passed by reference, performing I/O or calling other
functions with side-effects. In the presence of side effects, a
program's behaviour may depend on history; that is, the order of
evaluation matters. Understanding and debugging a function with side
effects requires knowledge about the context and its possible
histories.

How to use SharedFlow in Jetpack Compose

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.

How to use viewModel and compose navigation correctly together?

In the case of using Di, the way it is written on the official Android website is as follows
// import androidx.hilt.navigation.compose.hiltViewModel
#Composable
fun MyApp() {
NavHost(navController, startDestination = startRoute) {
composable("example") { backStackEntry ->
// Creates a ViewModel from the current BackStackEntry
// Available in the androidx.hilt:hilt-navigation-compose artifact
val exampleViewModel = hiltViewModel<ExampleViewModel>()
ExampleScreen(exampleViewModel)
}
/* ... */
}
}
Then if there are a lot of other #Composable functions in the ExampleScreen, like this
ExampleScreen() {
A()
B()
}
A() {
TopBar()
BottomBar()
....
}
B() ...
If both A() and its sub-functions need to use things in vm, don't you have to pass the vm parameters one by one? Because if vm is created in these functions, it is not a singleton(Because navigation compose affects the viewModel, each time you switch the page, these viewModels will be recreated as a new one). When I was puzzled, I saw this design idea on the official website again:
Pass explicit parameters
The general idea is that I should pass the logic code of the child function in the parent function, e.g. in ExampleScreen write:
ExampleScreen() {
val vm = hilt<VM>()
A(onClick = vm.onClick, ...)
B(...)
}
So my question is, if I have a lot of nested functions, don't I need to write a logical parameter in each function? So if I want to create a vm directly in each function, but it is not a singleton, what should I do? Im confused
You've done the right thing by injecting the viewmodel at the top-level. It's now up to you to decide how you want to pass it down. They're just functions in the end.
You can pass the viewmodel down everywhere, pass down only specific members or pass nothing down.
Do what makes sense and iterate if it doesn't work.

Observe livedata and navigate in jetpack compose

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)

Categories

Resources