NetworkOnMainThreadException when used as a LaunchedEffect key - android

When I use
LaunchedEffect(Dispatchers.IO)
I get,
NetworkOnMainThreadException
How should I use this function to run on background thread?
this is my code:
LaunchedEffect(Dispatchers.IO) {
val input = URL("https://rezaapp.downloadseriesmovie.ir/maintxt.php").readText()
println(input)
}
I'm using it inside my jetpack compose project

LaunchedEffect is one of the many Side Effects in Jetpack Compose, but instead of just explaining, it would be better for us to just have very simple compose use-case. Though I'm expecting that you already know what is re-composition and how a MutableState triggers it.
What we'll have:
a screen with a button in the middle
a MutableState increment-able integer value
a Log statement inside LaunchedEffect
What we'll do
click the button and increment the MutableState integer value
print the incremented value
What we'll expect
Logcat will display the value coming from LaunchedEffect, even before clicking the button
Our simple Composable
#Composable
fun ComposeSample() {
var intNum by remember {
mutableStateOf(0)
}
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button(onClick = {
intNum++
}) {
Text(
text = "Increment the integer"
)
}
}
LaunchedEffect(Unit) {
Log.e("IntNumber", "Current value: $intNum")
}
}
At this point, pay attention to the key I supplied with the LaunchedEffect.
When the screen is rendered for the first time, LaunchedEffect will trigger and we'll see a logcat print.
E: Current value: 0
But when I click the button it doesn't show the incremented value. Because LaunchedEffect needs a key that will change if you intend to trigger it every re-composition.
Now I changed the key I supplied to LaunchedEffect using the MutableState intNum variable,
LaunchedEffect(intNum) {
Log.e("IntNumber", "Current value: $intNum")
}
every click the logcat prints, because every time the intNum changes, the LaunchedEffect is triggered and triggers the Logcat statement.
E: Current value: 0
E: Current value: 1
E: Current value: 2
E: Current value: 3
When a key of a LaunchedEffect has changed and a composition happens, it will trigger anything inside its block.
I'm not sure what you are trying to achieve with your posted code, I don't even know how did it happen, but I suppose there aren't any use-case (to the best of my knowledge) where you will use a specific coroutine Dispatcher as a LaunchedEffect key.

Let me give it to you straight and clear,
lifecyclescope, or any coroutine scope in compose ui is on main
thread by default.
Especially for the compose coroutine scope you can not change their
dispatcher,
I strongly suggest that you call ViewModel method and there you launch the coroutine in side viewmodelSope, also keep in mind that the compose coroutine scopes are for light suspending operation, do not perform any heavy lift in those scopes.

Related

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.

Run ViewModel method as a reaction on composition

I have a top-level composable representing some app screen, which uses 2 view-models (for clarity). ViewModel_B starts some network activity once created:
Once network request finishes, it updates state_B: MutableStateFlow inside ViewModel_B which is observed from our composable. And that state is, for example, Loading | Error | Success enum, and let's say Success contains some payload:
When it's Success, I want my ViewModel_A to start another activity, which would in turn affect another state_A: MutableStateFlow, which we also observe and show UI based on it:
#Composable
fun MainScreen(
viewModel_A = viewModel { factory_A },
viewModel_B = viewModel { factory_B }.apply { networkRequest_B() }
) {
val state_B by viewModel_B.state_B.collectAsState()
when (val sB = state_B) {
Loading -> LoadingScreen() // Composable
Error -> ErrorScreen() // Composable
is Success -> {
val state_A by viewModel_A.state_A.collectAsState()
viewModel_A.doAction(sB.payload) // changes state_A internally
when (val sA = state_A) {
// other composables based on state_A values
}
}
}
}
When I run this code, the whole MainScreen() method gets called again and again in an infinite loop. Hence, two questions:
When should I start observing state_A - as close as possible to the place, where it is relevant? I mean, there is no sense to expect any values from it if state_B was Loading | Error, since there is no screen for state_A. Should I put viewModel_A.state_A.collectAsState() where it is now or I should hoist it top?
I believe, infinite loop is due to improper usage of Jetpack Compose here. It recomposes and runs viewModel_A.doAction() every recomposition, which will affect inner composables and everything would repeat. I believe, I should use rememberCoroutineScope() here OR LaunchedEffect {}, but I'm not sure how to do this properly.
What I see from LauchedEffect docs is that it's recomposed when key1 changes. I actually want to depend on sB.payload when running doAction(), but docs discourages me from passing sB.payload for key1.
For rememberCoroutineScope() - not sure, how to use it in my case, and also - where should I declare it - top or inside is Success -> {} block?

How to auto-scroll to the bottom of a Scrolling Log-Type Text composable

I'm trying to implement a Text view for log-type messages where every change to the text would auto-scroll the Text to the last/newly-added line of text. For example if my text size only fits 4 lines:
log 1
log 2
log 3
log 4
and a new line is added, then the output should be:
log 2
log 3
log 4
log 5
The specific problem I'd like to find a solution for is how to scroll down to the bottom every time there's a change in the text value. I tried the accepted solution for TextField, where a LaunchedEffect is triggered during composition phase, but it didn't work in my case:
#Composable
fun LogText(log: State<String>) {
var logState = rememberScrollState(0)
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(coroutineScope) {
logState.scrollTo(logState.maxValue)
}
Text(
text = log.value,
modifier = Modifier.verticalScroll(logState),
)
}
The LaunchedEffect did not change the Text to scroll down to the newest line.
Your usage of LaunchedEffect is wrong. The lambda you are passing to the LaunchedEffect is executed every time the LaunchedEffect's key(s) are changed. You are using remembered coroutine scope as a key, which never changes, therefore your effect is only executed once.
Also, the LaunchedEffect lambda is launched in coroutine scope by default, so you don't need your own.
Try it like this:
#Composable
fun LogText(log: State<String>) {
var logState = rememberScrollState(0)
LaunchedEffect(log.value) {
logState.scrollTo(logState.maxValue)
}
...
}
this means: everytime the log.value changes, execute the lambda.
And one more thing: if your text gets really long, drawing it like that will be quite heavy. I would consider splitting it by lines to List<String> and using LazyColumn with multiple Text items...

Why is LaunchedEffect(true) suspicious?

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.

What does Jetpack Compose remember actually do, how does it work under the hood?

Checking out codelab's basic tutorial there is a snippet to increase counter on button when clicked
#Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
val counterState = remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxHeight()) {
Column(modifier = Modifier.weight(1f)) {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
}
Counter(
count = counterState.value,
updateCount = { newCount ->
counterState.value = newCount
}
)
}
}
#Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button(
onClick = { updateCount(count + 1) },
colors = ButtonConstants.defaultButtonColors(
backgroundColor = if (count > 5) Color.Green else Color.White
)
) {
Text("I've been clicked $count times")
}
}
It is clear that remember { mutableStateOf(0) } stores the state/value. My question is what remember does under the hood. Using var count = remember { 0 } or mutableStateOf(0) without remember does not increase the value.
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
var count = remember { 0 }
Column(modifier = Modifier.fillMaxHeight()) {
Column(modifier = Modifier.weight(1f)) {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
}
Counter(
count = count,
updateCount = { newCount ->
count = newCount
}
)
}
}
Snippet above does not update the value printed on Text, does remember only work with MutableState?
remember - allows you to remember state from previous recompose invocation and just this.
So if you for instance randomize color at initial run. The randomized color will going to be calculated once and reused whenever re-compose is necessary.
so ...
remember = store value just in case recompose will be called.
Now the second thing is knowing when re-compose should be actually triggered.
and there mutable states comes to help.
mutablestate = store the value AND in case i update value trigger recompose for all elements using this data.
To learn how composition and recomposition works you can check out Under the hood of Jetpack Compose article by Leland Richardson, which describes inner works very well, also youtube video here. And most of this answer uses article as reference and quoted most from it.
The implementation of the Composer contains a data structure that is closely related to a Gap Buffer. This data structure is commonly used in text editors.
A gap buffer represents a collection with a current index or cursor. It is implemented in memory with a flat array. That flat array is larger than the collection of data that it represents, with the unused space referred to as the gap.
Basically adding space near your Composable function slot table to be able to update UI dynamically with high costs since get, move, insert, and delete — are constant time operations, except for moving the gap. Moving the gap is O(n) but this does not happen often which you need to change all UI structure, on average, UIs don’t change structure very much.
#Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(
text="Count: $count",
onPress={ count += 1 }
)
}
When the compiler sees the Composable annotation, it inserts additional parameters and calls into the body of the function.
First, the compiler adds a call to the composer.start method and passes it a compile time generated key integer.
fun Counter($composer: Composer) {
$composer.start(123)
var count by remember($composer) { mutableStateOf(0) }
Button(
$composer,
text="Count: $count",
onPress={ count += 1 },
)
$composer.end()
}
When this composer executes it does the following:
Composer.start gets called and stores a group object
remember inserts a group object
the value that mutableStateOf returns, the state instance, is stored.
Button stores a group, followed by each of its parameters.
And then finally we arrive at composer.end.
The data structure now holds all of the objects from the composition, the entire tree in execution order, effectively a depth first traversal of the tree.
So remember needed to store a mutableState() to get value from previous composition and mutableState() is required to trigger one.
And MutableState interface uses #Stable annotation
#Stable
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
Stable is used to communicate some guarantees to the compose compiler about how a certain type or function will behave.
When applied to a class or an interface, Stable indicates that the following must be true:
The result of equals will always return the same result for the same two instances.
When a public property of the type changes, composition will be notified.
All public property types are stable.
When applied to a function or a property, the Stable]annotation indicates that the function will return the same result if the same
parameters are passed in. This is only meaningful if the parameters
and results are themselves Stable, Immutable, or primitive.
The invariants that this annotation implies are used for optimizations by the compose compiler, and have undefined behavior if
the above assumptions are not met. As a result, one should not use this annotation unless they are certain that these conditions are satisfied.
Another source with a Video that describes how Compose works.
Codelab example mentions about remember and mutableState as
Reacting to state changes is at the very heart of Compose. Compose
apps transform data into UI by calling Composable functions. If your
data changes, you recall these functions with the new data, creating
an updated UI. Compose offers tools for observing changes in your
app's data, which will automatically recall your functions—this is
called recomposing. Compose also looks at what data is needed by an
individual composable so that it only needs to recompose components
whose data has changed and can skip composing those that are not
affected.
Under the hood, Compose uses a custom Kotlin compiler plugin so when
the underlying data changes, the composable functions can be
re-invoked to update the UI hierarchy.
To add internal state to a composable, use the mutableStateOf
function, which gives a composable mutable memory. To not have a
different state for every recomposition, remember the mutable state
using remember. And, if there are multiple instances of the composable
at different places on the screen, each copy will get its own version
of the state. You can think of internal state as a private variable in
a class.
remember{X} and remember{mutableStateOf(X)} are useful in different scenarios.
First one is required when your object doesn't need to be instantiated at each recomposition, and there is another trigger that triggers composition.
An example for this is remember{Paint()}, any object that doesn't need to be instantiated more than once or memory heavy to instantiate. If a Composable that possesses this object is recomposed, properties of your object don't change thanks to remember, if you don't use remember your object is instantiated on each recomposition and all the properties previously set are reset.
If you need a trigger(mutableStateOf) and need to have the latest value(remember) like in question choose remember{mutableStateOf()}
Variables are cleared on every compositon.
Using remember will get the previous value.
I think its equivalent to declare a mutableState in ViewModel.

Categories

Resources