I have a state class
object SomeState {
data class State(
val mainPhotos: List<S3Photo>? = emptyList(),
)
}
VM load data via init and updates state
class SomeViewModel() {
var viewState by mutableStateOf(SomeState.State())
private set
init {
val photos = someSource.load()
viewState = viewState.cope(mainPhotos = photos)
}
}
Composable takes data from state
#Composable
fun SomeViewFun(
state = SomeState.State
) {
HorizontalPager(
count = state .mainPhotos?.size ?: 0,
) {
//view items
}
}
The problem is that count in HorizontalPager always == 0, but in logcat and debugger i see that list.size() == 57
I have a lot of screen with arch like this and they works normaly. But on this screen view state doesn't updates and i can't understand why.
UPDATE
VM passes to Composable like this
#Composable
fun SomeDistanation() {
val viewModel: SomeViewModel = hiltViewModel()
SomeViewFun(
state = viewModel.state
)
}
Also Composable take Flow<ViewEffect> and etc, but in this question it doesn't matter, because there is no user input or side effects
UPDATE 2
The problem was in data source. All code in question work correctly. Problem closed.
object wrapping is completely redundant (no fields, no functions), you can remove it (also, change the name so it won't confuse with compose's State):
data class MyState(
val mainPhotos: List<S3Photo>? = emptyList(),
)
According to Android Developers, you need to create the state in the view model, and observe the state in the composable function - your code is a bit unclear for me so I'll just show you how I do it in my apps.
create the state in the view model:
class SomeViewModel() {
private val viewState = mutableStateOf(MyState())
// Expose as immutable so it won't be edited
fun getState(): State<MyState> = viewState
init {
val photos = someSource.load()
viewState.value = viewState.value.copy(mainPhotos = photos)
}
}
observe the state in the composable function:
#Composable
fun SomeDistanation() {
val viewModel: SomeViewModel = hiltViewModel()
val state: MyState by remember { viewModel.getState() }
SomeViewFun(state)
}
Now you'll get automatic recomposition in case the state changes.
Related
I have a CounterScreenUiState data class with a single property called counterVal (integer). If I am updating the value of my counter from viewModel which of the following is the correct approach?
Approach A:
data class CounterUiState(
val counterVal: Int = 0,
)
class CounterViewModel : ViewModel() {
var uiState by mutableStateOf(CounterUiState())
private set
fun inc() {
uiState = uiState.copy(counterVal = uiState.counterVal + 1)
}
fun dec() {
uiState = uiState.copy(counterVal = uiState.counterVal - 1)
}
}
or
Approach B:
data class CounterUiState(
var counterVal: MutableState<Int> = mutableStateOf(0)
)
class CounterViewModel : ViewModel() {
var uiState by mutableStateOf(CounterUiState())
private set
fun inc() {
uiState.counterVal.value = uiState.counterVal.value + 1
}
fun dec() {
uiState.counterVal.value = uiState.counterVal.value - 1
}
}
For the record, I tried both approach and both works well without unnecessary re-compositions.
Thanks in Advance!!!
So to summarize, "implementation" and "performance" wise, your'e only
choice is A.
This is not true. It's a common pattern that is used other Google's sample apps, JetSnack for instance, and default functions like rememberScrollable or Animatable are the ones that come to my mind. And in that article it's also shared as
#Stable
class MyStateHolder {
var isLoading by mutableStateOf(false)
}
or
#Stable
class ScrollState(initial: Int) : ScrollableState {
/**
* current scroll position value in pixels
*/
var value: Int by mutableStateOf(initial, structuralEqualityPolicy())
private set
// rest of the code
}
Animatable class
class Animatable<T, V : AnimationVector>(
initialValue: T,
val typeConverter: TwoWayConverter<T, V>,
private val visibilityThreshold: T? = null,
val label: String = "Animatable"
) {
internal val internalState = AnimationState(
typeConverter = typeConverter,
initialValue = initialValue
)
/**
* Current value of the animation.
*/
val value: T
get() = internalState.value
/**
* The target of the current animation. If the animation finishes un-interrupted, it will
* reach this target value.
*/
var targetValue: T by mutableStateOf(initialValue)
private set
}
Omitted some code from Animatable for simplicity but as can be seen it's a common pattern to use a class that hold one or multiple MutableStates. Even type AnimationState hold its own MutableState.
You can create state holder classes and since these are not e not variables but states without them changing you won't have recompositions unless these states change. The thing needs to be changed with option B is instead of using
data class CounterUiState(
var counterVal: MutableState<Int> = mutableStateOf(0)
)
You should change it to
class CounterUiState(
var counterVal by mutableStateOf(0)
)
since you don't need to set new instance of State itself but only the value.
And since you already wrap your states inside your uiState there is no need to use
var uiState by mutableStateOf(CounterUiState())
private set
you can have this inside your ViewModel as
val uiState = CounterUiState()
or inside your Composable after wrapping with remember
#Composable
fun rememberCounterUiState(): CounterUiState = remember {
CounterUiState()
}
With this pattern you can store States in one class and hold variables that should not trigger recomposition as part of internal calculations and it's up to developer expose these non-state variables based on the design.
https://github.com/android/compose-samples/blob/main/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt
#Stable
class SearchState(
query: TextFieldValue,
focused: Boolean,
searching: Boolean,
categories: List<SearchCategoryCollection>,
suggestions: List<SearchSuggestionGroup>,
filters: List<Filter>,
searchResults: List<Snack>
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
var categories by mutableStateOf(categories)
var suggestions by mutableStateOf(suggestions)
var filters by mutableStateOf(filters)
var searchResults by mutableStateOf(searchResults)
val searchDisplay: SearchDisplay
get() = when {
!focused && query.text.isEmpty() -> SearchDisplay.Categories
focused && query.text.isEmpty() -> SearchDisplay.Suggestions
searchResults.isEmpty() -> SearchDisplay.NoResults
else -> SearchDisplay.Results
}
}
Also for skippibility
Compose will treat your CounterUiState as unstable and down the road
it will definitely cause you headaches because what ever you do,
This is misleading. Most of the time optimizing for skippability is premature optimization as mentioned in that article and the one shared by originally Chris Banes.
Should every Composable be skippable? No.
Chasing complete skippability for every composable in your app is a
premature optimization. Being skippable actually adds a small overhead
of its own which may not be worth it, you can even annotate your
composable to be non-restartable in cases where you determine that
being restartable is more overhead than it’s worth. There are many
other situations where being skippable won’t have any real benefit and
will just lead to hard to maintain code. For example:
A composable that is not recomposed often, or at all.
When I change ViewModel variable, Composable Doesn't Update the View and I'm not sure what to do.
This is my MainActivity:
class MainActivity : ComponentActivity() {
companion object {
val TAG: String = MainActivity::class.java.simpleName
}
private val auth by lazy {
Firebase.auth
}
var isAuthorised: MutableState<Boolean> = mutableStateOf(FirebaseAuth.getInstance().currentUser != null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val user = FirebaseAuth.getInstance().currentUser
setContent {
HeroTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
if (user != null) {
Menu(user)
} else {
AuthTools(auth, isAuthorised)
}
}
}
}
}
}
I have a a View Model:
class ProfileViewModel: ViewModel() {
val firestore = FirebaseFirestore.getInstance()
var profile: Profile? = null
val user = Firebase.auth.currentUser
init {
fetchProfile()
}
fun fetchProfile() {
GlobalScope.async {
getProfile()
}
}
suspend fun getProfile() {
user?.let {
val docRef = firestore.collection("Profiles")
.document(user.uid)
return suspendCoroutine { continuation ->
docRef.get()
.addOnSuccessListener { document ->
if (document != null) {
this.profile = getProfileFromDoc(document)
}
}
.addOnFailureListener { exception ->
continuation.resumeWithException(exception)
}
}
}
}
}
And a Composable View upon user autentication:
#Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel = ProfileViewModel()
Column(
modifier = Modifier
.background(color = Color.White)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text("Signed in!");
ProfileVModel.profile?.let {
Text(it.username);
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
TextButton(onClick = {
FirebaseAuth.getInstance().signOut()
context.startActivity(Intent(context, MainActivity::class.java))
}) {
Text(
color = Color.Black,
text = "Sign out?",
modifier = Modifier.padding(all = 8.dp)
)
}
}
}
}
When my Firestore method returns, I update the profile var, and "expect" it to be updated in the composable, here:
ProfileVModel.profile?.let {
Text(it.username);
}
However, nothing is changing?
When I was adding firebase functions from inside composable, I could just do:
context.startActivity(Intent(context, MainActivity::class.java))
And it would update the view. However, I'm not quite sure how to do this from inside a ViewModel, since "context" is a Composable-specific feature?
I've tried to look up Live Data, but every tutorial is either too confusing or differs from my code. I'm coming from SwiftUI MVVM so when I update something in a ViewModel, any view that's using the value updates. It doesn't seem to be the case here, any help is appreciated.
Thank you.
Part 1: Obtaining a ViewModel correctly
On the marked line below you are setting your view model to a new ProfileViewModel instance on every recomposition of your Menu composable, which means your view model (and any state tracked by it) will reset on every recomposition. That prevents your view model to act as a view state holder.
#Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel = ProfileViewModel() // <-- view model resets on every recomposition
// ...
}
You can fix this by always obtaining your ViewModels from the ViewModelStore. In that way the ViewModel will have the correct owner (correct lifecycle owner) and thus the correct lifecycle.
Compose has a helper for obtaining ViewModels with the viewModel() call.
This is how you would use the call in your code
#Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel: ProfileViewModel = viewModel()
// or this way, if you prefer
// val ProfileVModel = viewModel<ProfileViewModel>()
// ...
}
See also ViewModels in Compose that outlines the fundamentals related to ViewModels in Compose.
Note: if you are using a DI (dependency injection) library (such as Hilt, Koin...) then you would use the helpers provided by the DI library to obtain ViewModels.
Part 2: Avoid GlobalScope (unless you know exactly why you need it) and watch out for exceptions
As described in Avoid Global Scope you should avoid using GlobalScope whenever possible. Android ViewModels come with their own coroutine scope accessible through viewModelScope. You should also watch out for exceptions.
Example for your code
class ProfileViewModel: ViewModel() {
// ...
fun fetchProfile() {
// Use .launch instead of .async if you are not using
// the returned Deferred result anyway
viewModelScope.launch {
// handle exceptions
try {
getProfile()
} catch (error: Throwable) {
// TODO: Log the failed attempt and/or notify the user
}
}
}
// make it private, in most cases you want to expose
// non-suspending functions from VMs that then call other
// suspend factions inside the viewModelScope like fetchProfile does
private suspend fun getProfile() {
// ...
}
// ...
}
More coroutine best practices are covered in Best practices for coroutines in Android.
Part 3: Managing state in Compose
Compose tracks state through State<T>. If you want to manage state you can create MutableState<T> instances with mutableStateOf<T>(value: T), where the value parameter is the value you want to initialize the state with.
You could keep the state in your view model like this
// This VM now depends on androidx.compose.runtime.*
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
class ProfileViewModel: ViewModel() {
var profile: Profile? by mutableStateOf(null)
private set
// ...
}
then every time you would change the profile variable, composables that use it in some way (i.e. read it) would recompose.
However, if you don't want your view model ProfileViewModel to depend on the Compose runtime then there are other options to track state changes while not depending on the 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.
This means that you could also use a MutableStateFlow<T> to track changes inside the ViewModel and expose it outside your view model as a StateFlow<T>.
// This VM does not depend on androidx.compose.runtime.* anymore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class ProfileViewModel : ViewModel() {
private val _profileFlow = MutableStateFlow<Profile?>(null)
val profileFlow = _profileFlow.asStateFlow()
private suspend fun getProfile() {
_profileFlow.value = getProfileFromDoc(document)
}
}
And then use StateFlow<T>.collectAsState() inside your composable to get the State<T> that is needed by Compose.
A general Flow<T> can also be collected as State<T> with Flow<T : R>.collectAsState(initial: R), where the initial value has to be provided.
#Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel: ProfileViewModel = viewModel()
val profile by ProfileVModel.profileFlow.collectAsState()
Column(
// ...
) {
// ...
profile?.let {
Text(it.username);
}
// ...
}
}
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.
An introduction to handling the state as the complexity increases is in the video from Google about Using Jetpack Compose's automatic state observation.
Profile in view model should be State<*>
private val _viewState: MutableState<Profile?> = mutableStateOf(null)
val viewState: State<Profile?> = _viewState
In composable
ProfileVModel.profile.value?.let {
Text(it.username);
}
I recommend using MutableStateFlow.
a simple sample is described in this Medium article :
https://farhan-tanvir.medium.com/stateflow-with-jetpack-compose-7d9c9711c286
I am quite new to Jetpack compose and have an issue that my list is not recomposing when a property of an object in the list changes. In my composable I get a list of available appointments from my view model and it is collected as a state.
// AppointmentsScreen.kt
#Composable
internal fun AppointmentScreen(
navController: NavHostController
) {
val appointmentsViewModel = hiltViewModel<AppointmentViewModel>()
val availableAppointments= appointmentsViewModel.appointmentList.collectAsState()
AppointmentContent(appointments = availableAppointments, navController = navController)
}
In my view model I get the data from a dummy repository which returns a flow.
// AppointmentViewModel.kt
private val _appointmentList = MutableStateFlow(emptyList<Appointment>())
val appointmentList : StateFlow<List<Appointment>> = _appointmentList.asStateFlow()
init {
getAppointmentsFromRepository()
}
// Get the data from the dummy repository
private fun getAppointmentsFromRepository() {
viewModelScope.launch(Dispatchers.IO) {
dummyRepository.getAllAppointments()
.distinctUntilChanged()
.collect { listOfAppointments ->
if (listOfAppointments.isNullOrEmpty()) {
Log.d(TAG, "Init: Empty Appointment List")
} else {
_appointmentList.value = listOfAppointments
}
}
}
}
// dummy function for demonstration, this is called from a UI button
fun setAllStatesToPaused() {
dummyRepository.setSatesInAllObjects(AppointmentState.Finished)
// Get the new data
getAppointmentsFromRepository()
}
Here is the data class for appointments
// Appointment data class
data class Appointment(
val uuid: String,
var state: AppointmentState = AppointmentState.NotStarted,
val title: String,
val timeStart: LocalTime,
val estimatedDuration: Duration? = null,
val timeEnd: LocalTime? = null
)
My question: If a property of one of the appointment objects (in the view models variable appointmentList) changes then there is no recomposition. I guess it is because the objects are still the same and only the properties have changed. What do I have to do that the if one of the properties changes also a recomposition of the screen is fired?
For example if you have realtime app that display stocks/shares with share prices then you will probably also have a list with stock objects and the share price updates every few seconds. The share price is a property of the stock object so this quite a similiar situation.
Using LiveData to store the value of the slider makes it lag and move jerky (I suppose because of postValue ())
#Composable
fun MyComposable(
viewModel: MyViewModel
) {
val someValue = viewModel.someValue.observeAsState()
Slider(someValue) {
viewModel.setValue(it)
}
}
class MyViewModel() : ViewModel() {
val someValue: LiveData<Float> = dataStore.someValue // MutableLiveData
fun setValue(value: Float) {
dataStore.setValue(value)
}
}
class MyDataStore() {
val someValue = MutableLiveData<Float>()
fun setValue(value: Float) {
// Some heavy logic
someValue.postValue(value)
}
}
As I understand it, postValue() takes a while, and because of this, the slider seems to be trying to resist changing the value.
In order to somehow get around this, I had to create additional State variables so that the slider would directly update its value
#Composable
fun MyComposable(
viewModel: MyViewModel
) {
val someValue = viewModel.someValue.observeAsState()
var someValue2 by remember { mutableStateOf(someValue) }
Slider(someValue2) {
someValue2 = it
viewModel.setValue(it) // I also had to remove postValue ()
}
}
As I understand it, if the data from the DataStore comes with a delay, then the value in someValue will not have time to initialize and it will be null by the time the view appears (this has not happened yet, but is it theoretically possible?), And thus the value of the slider will not be relevant. Are there any solutions to this problem?
I'm having an issue trying to display the data saved in my DataStore on startup in Jetpack Compose.
I have a data store set using protocol buffers to serialize the data. I create the datastore
val Context.networkSettingsDataStore: DataStore<NetworkSettings> by dataStore(
fileName = "network_settings.pb",
serializer = NetworkSettingsSerializer
)
and turn it into a livedata object in the view model
val networkSettingsLive = dataStore.data.catch { e ->
if (e is IOException) { // 2
emit(NetworkSettings.getDefaultInstance())
} else {
throw e
}
}.asLiveData()
Then in my #Composable I try observing this data asState
#Composable
fun mycomposable(viewModel: MyViewModel) {
val networkSettings by viewModel.networkSettingsLive.observeAsState(initial = NetworkSettings.getDefaultInstance())
val address by remember { mutableStateOf(networkSettings.address) }
Text(text = address)
}
I've confirmed that the data is in the datastore, and saving properly. I've put some print statements in the composible and the data from the datastore makes it, eventually, but never actually displays in my view. I want to say I'm not properly setting my data as Stateful the right way, but I think it could also be not reading from the data store the right way.
Is there a display the data from the datastore in the composable, while displaying the initial data on start up as well as live changes?
I've figured it out.
What I had to do is define the state variables in the composable, and later set them via a state controlled variable in the view model, then set that variable with what's in the dataStore sometime after initilization.
class MyActivity(): Activity {
private val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
MainScope.launch {
val networkSettings = viewModel.networkSettingsFlow.firstOrNull()
if (networkSettings != null) {
viewModel.mutableNetworkSettings.value = networkSettings
}
}
}
}
class MyViewModel(): ViewModel {
val networkSettingsFlow = dataStore.data
val mutableNetworkSettings = mutableStateOf(NetworkSettings.getInstance()
}
#Composable
fun NetworkSettings(viewModel: MyViewModel) {
val networkSettings by viewModel.mutableNetworkSettings
var address by remember { mutableStateOf(networkSettings.address) }
address = networkSettings.address
Text(text = address)
}