I'm using a service locator (as advised in https://developer.android.com/training/dependency-injection#di-alternatives, but I'll switch to proper DI later I promise) to handle auth in my app. I have an authentication service that has a user property that I set and unset using logIn and logOut methods
I'd like my ContentView to react to changes in auth.user but I can't quite figure out how. I've tried wrapping it into by remember { mutableStateOf() } but I don't see any update upon login.. any idea what I am missing?
Thanks in advance! (snippets below)
#Composable
fn ContentView() {
val auth = ServiceLocator.auth
var loggedInUser: User? by remember { mutableStateOf(auth.user) } // <-- I would like my composable to react to changes to auth.user
if (loggedInUser) {
ViewA()
} else {
ViewB()
}
}
object ServiceLocator {
val auth = AuthenticationService()
}
class AuthenticationService {
var user: User? = null
fun logIn() {
// sets user...
}
fun logOut() {
// undefs user...
}
In your code snippet on this line
var loggedInUser: User? by remember { mutableStateOf(auth.user) }
you are creating an instance of MutableState<User?> with an initial value of the value that is at that time referenced by auth.user.
Due to remember { } this initialization happens only when the composable ContentView enters composition and then the MutableState instance is remembered across recompositions and reused.
If you later change the variable auth.user no recomposition will happen, because the value stored in loggedInUser (in the mutable state) has not changed.
The documentation for mutableStateOf explains what this call actually does behind the scenes
Return a new MutableState initialized with the passed in value.
The MutableState class is a single value holder whose reads and writes are observed by Compose. Additionally, writes to it are transacted as part of the Snapshot system.
Let's dissect this piece of information.
Return a new MutableState initialized with the passed in value.
Calling mutableStateOf returns a MutableState instance that is initialized with the value that was passed as the parameter.
The MutableState class is a single value holder
Every instance of this class stores a single value of state. It might store other values for the implementation purposes, but it exposes only a single value of state.
whose reads and writes are observed by Compose
Compose observes reads and writes that happen to instances of MutableState
This is the piece of information that you have missed.
The writes need to happen to the instance of the MutableState (loggedInUser in your case), not to the variable that has been passed in as the initial value (auth.user in your case).
If you really think about it, there is no built-in mechanism in Kotlin to observe changes to a variable, so it is understandable that there has to be a wrapper for Compose to be able to observe the changes. And that we have to change the state through the wrapper instead of changing the variable directly.
Knowing all that you could just move the mutable state into AuthenticationService and things would work
import androidx.compose.runtime.mutableStateOf
class AuthenticationService {
var user: User? by mutableStateOf(null)
private set
// ... rest of the service
}
#Composable
fun ContentView() {
val auth = ServiceLocator.auth
// no remember { } block this time because now the MutableState reference is being kept by
// the AuthenticationService so it won't reset on recomposition
val loggedInUser = auth.user
if (loggedInUser != null) {
ViewA()
} else {
ViewB()
}
}
However now your AuthenticationService depends on mutableStateOf and thus on the Composable runtime which you might want to avoid. A "Service" (or Repository) should not need to know details about the UI implementation.
There are other options to track state changes and not depend on Compose runtime. From the documentation section Compose and other libraries
Compose comes with extensions for Android's most popular stream-based
solutions. Each of these extensions is provided by a different
artifact:
Flow.collectAsState() doesn't require extra dependencies. (because it is part of kotlinx-coroutines-core)
LiveData.observeAsState() included in the androidx.compose.runtime:runtime-livedata:$composeVersion artifact.
Observable.subscribeAsState() included in the androidx.compose.runtime:runtime-rxjava2:$composeVersion or
androidx.compose.runtime:runtime-rxjava3:$composeVersion artifact.
These artifacts register as a listener and represent the values as a
State. Whenever a new value is emitted, Compose recomposes those parts
of the UI where that state.value is used.
Example using a Kotlin MutableStateFlow
// No androidx.compose.* dependencies anymore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class AuthenticationService {
private val user = MutableStateFlow<User?>(null)
val userFlow = user.asStateFlow()
fun logIn() {
user.value = User(/* potential parameters */)
}
fun logOut() {
user.value = null
}
}
And then in the composable we collect the flow as state.
import androidx.compose.runtime.collectAsState
#Composable
fun ContentView() {
val auth = ServiceLocator.auth
val loggedInUser = auth.userFlow.collectAsState().value
if (loggedInUser != null) {
ViewA()
} else {
ViewB()
}
}
To learn more about working with state in Compose see the documentation section on Managing State. This is fundamental information to be able to work with state in Compose and trigger recompositions efficiently. It also covers the fundamentals of state hoisting. If you prefer a coding tutorial here is the code lab for State in Jetpack Compose.
Eventually (when you app grows in complexity) you might want to put another layer between your Service/Repository layer and your UI layer (the composables). A layer that will hold and manage the UI state so you will be able to cover both positive outcomes (a successful login) and negative outcomes (a failed login).
If you are going the MVVM (Model-View-ViewModel) way or the MVI (Model-View-Intent) way, that layer would be covered by ViewModels. In that case the composables manage only some transient UI state themselves, while they get (or observe) the rest of the UI state from the VMs and call the VMs to perform actions. The VMs then interact with the Service/Repository layer and update the UI state accordingly. An introduction to handling the state as the complexity increases is in the video from Google about Using Jetpack Compose's automatic state observation.
Related
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.
I am still new to compose and I am curious how people treat this kind of thing.
Let's imagine that we have a screen that has two variants, one variant with some views, another variant with other views. That variant should be dictated by a persisted flag, which I have stored using DataStore (the new SharedPrefs). The only issues is that unlike SharedPrefs, DataStore is asynchronous and is made to work with coroutines. So here's what happens, the screen gets rendered in default state (variant A) for just a split second, atfer about 100-200ms the viewModel successfully reads the value from DataStore on a coroutine and posts it on a mutableStateOf(), which as a result triggers recomposition with the variant B of the screen that is saved in the prefs. This transition is visible and the entire behavior looks glitchy. How do you fix this? I don't want the screen to compose the default state before the viewModel has time to read the stored value, I want the screen to await those 100-200ms without doing anything and composing the views only after the reading from DataStore.
The code looks like:
#Composable
fun MyScreen(){
val viewModel = hiltViewModel<ScreenViewModel>()
val state = viewModel.uiState
if(state == MyScreenState.A){
[...] // some view here
} else {
[...] // other view here
}
}
#HiltViewModel
class ScreenViewModel #Inject constructor(
private val dataStoreService: DataStoreService
) : BaseViewModel() {
var uiState by mutableStateOf(MyScreenState.A)
init {
viewModelScope.launch {
dataStoreService.flag().collect { flag ->
uiState = if(flag) MyScreenState.A else MyScreenState.B
}
}
}
}
For simplicity, MyScreenState is just a simple enum in this case. One of the things I thought about is defaulting the uiState to null instead of variant A and in my screen check if the state is null and if so returning a Unit (basically rendering nothing). If the state is not null, render the screen accordingly. But the truth is that I don't feel like making that uiState nullable, I avoid working with nulls at all cost because they make the code just a little less readable and needs extra handling. What's your solution on this? Thanks.
Instead of creating nullable state create another state that represents nothing being happening. I generally use Idle state to set as initial or a UI state when nothing should happen. I also use this approach for fire once events after event is invoked and processed.
var uiState by mutableStateOf(MyScreenState.Idle)
it will be a loading or a blank screen depending on your UI
I am trying first handle the response from API by using observe. Later after observing the handled variable I want to save it to database.
The variable tokenFromApi is updated inside tokenResponseFromApi's observer. Is it possible to observe tokenFromApi outside the observer of tokenResponseFromApi? When debugged, the code did not enter inside tokenFromApi observer when the app started.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
var tokenResponseFromApi: LiveData<String>? = MutableLiveData<String>()
var tokenFromApi: LiveData<TokenEntity>? = MutableLiveData<TokenEntity>()
tokenResponseFromApi?.observe(viewLifecycleOwner, Observer {
tokenResponseFromApi ->
if (tokenResponseFromApi != null) {
tokenFromApi = viewModel.convertTokenResponseToEntity(tokenResponseFromApi, dh.asDate)
}
})
tokenFromApi?.observe(viewLifecycleOwner, Observer {
tokenFromApi ->
if (tokenFromApi != null) {
viewModel.saveTokenToDB(repo, tokenFromApi)
}
})
}
Your problem is that you're registering the observer on tokenFromApi during setup, and when you get your API response, you're replacing tokenFromApi without registering an observer on it. So if it ever emits a value, you'll never know about it. The only observer you have registered is the one on the discarded tokenFromApi which is never used by anything
Honestly your setup here isn't how you're supposed to use LiveData. Instead of creating a whole new tokenFromApi for each response, you'd just have a single LiveData that things can observe. When there's a new value (like an API token) you set that on the LiveData, and all the observers see it and react to it. Once that's wired up, it's done and it all works.
The way you're doing it right now, you have a data source that needs to be taken apart, replaced with a new one, and then everything reconnected to it - every time there's a new piece of data, if you see what I mean.
Ideally the Fragment is the UI, so it reacts to events (by observing a data source like a LiveData and pushes UI events to the view model (someone clicked this thing, etc). That API fetching and DB storing really belongs in the VM - and you're already half doing that with those functions in the VM you're calling here, right? The LiveDatas belong in the VM because they're a source of data about what's going on inside the VM, and the rest of the app - they expose info the UI needs to react to. Having the LiveData instances in your fragment and trying to wire them up when something happens is part of your problem
Have a look at the App Architecture guide (that's the UI Layer page but it's worth being familiar with the rest), but this is a basic sketch of how I'd do it:
class SomeViewModel ViewModel() {
// private mutable version, public immutable version
private val _tokenFromApi = MutableLiveData<TokenEntity>()
val tokenFromApi: LiveData<TokenEntity> get() = _tokenFromApi
fun callApi() {
// Do your API call here
// Whatever callback/observer function you're using, do this
// with the result:
result?.let { reponse ->
convertTokenResponseToEntity(response, dh.asDate)
}?.let { token ->
saveTokenToDb(repo, token)
_tokenFromApi.setValue(token)
}
}
private fun convertTokenResponseToEntity(response: String, date: Date): TokenEntity? {
// whatever goes on in here
}
private fun saveTokenToDb(repo: Repository, token: TokenEntity) {
// whatever goes on in here too
}
}
so it's basically all contained within the VM - the UI stuff like fragments doesn't need to know anything about API calls, whether something is being stored, how it's being stored. The VM can update one of its exposed LiveData objects when it needs to emit some new data, update some state, or whatever - stuff that's interesting to things outside the VM, not its internal workings. The Fragment just observes whichever one it's interested in, and updates the UI as required.
(I know the callback situation might be more complex than that, like saving to the DB might involve a Flow or something. But the idea is the same - in its callback/result function, push a value to your LiveData as appropriate so observers can receive it. And there's nothing wrong with using LiveData or Flow objects inside the VM, and wiring those up so a new TokenEntity gets pushed to an observer that calls saveTokenToDb, if that kind of pipeline setup makes sense! But keep that stuff private if the outside world doesn't need to know about those intermediate steps
As stated in the Jetpack compose documentation, a stable type must comply with the following contract.
The result of equals for two instances will forever be the same for
the same two instances.
If a public property of the type changes, Composition will be notified.
All public property types are also
stable.
The first and third contracts are straightforward. How can I comply with the second point?
For example, I have a stable type called User
data class User(val username: String, var email: String)
and I change the email during the flow of the app, how can I notify the Composition?
According to Jetpack Compose Stability Explained:
It’s important to note that the parameters don’t have to be immutable, they can be mutable as long as the Compose runtime is notified of all changes. For most types this would be an impractical contract to uphold however Compose provides mutable classes that do uphold this contract for you e.g MutableState, SnapshotStateMap/List/etc. As a result, using these types for your mutable properties will allow your class to uphold the contract of #Stable.
The example from the same post suggests ensuring any var in your class is actually a MutableState:
#Stable
class MyStateHolder {
var isLoading by mutableStateOf(false)
}
In your example, it could be something like this:
#Stable
data class User(val username: String) {
var email by mutableStateOf("")
}
Note that at this point a data class may not be desirable, as some of the functionality it generates (copy, equals) wouldn't work with the now-mutable email field.
Compose has a special State type that allows you to notify when a recomposition is needed. This is the most common way to initiate a recomposition.
The data class is a good tool that will give you copy for all fields out of the box, but you should avoid using var and use only val for your fields to avoid mistakes.
So, a basic example of updating a field would look like this:
var state by remember { mutableStateOf(User(username = "", email = "")) }
TextField(
value = state.username,
onValueChange = {
state = state.copy(username = it)
}
)
You can use Flow/LiveData in the same way, since they can be collected in State with collectAsState/observeAsState.
More info can be found in Compose documentation, including this youtube video which explains the basic principles.
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)