i'am trying to build an app with jetpack compose but when it's come to api call with view model i get an infinite loop. the app keep calling the api and i don't get why. here is my viewmodel :
class LibraryViewModel() : ViewModel() {
var library: ArrayList<PKIssue> = arrayListOf()
var loadLibrary by mutableStateOf(false)
init {
getLibrary()
}
fun getLibrary(){
viewModelScope.launch {
Press.issues(
result = object : result<ArrayList<Issue>, Error> {
override fun succeed(result: ArrayList<Issue>?) {
loadLibrary = true
if (result != null) {
library = result
}
}
override fun failed(error: Error?) {
loadLibrary = false
}
})
}
}
But as soon as i init my viewModel i get infinite call to my api, here is how i try to declare it :
#SuppressLint("StateFlowValueCalledInComposition")
#Destination
#Composable
fun HomeScreen(
navigator: DestinationsNavigator,
libraryViewModel: LibraryViewModel = LibraryViewModel()
) {
or inside the composable : val libraryViewModel = LibraryViewModel() but i get the same problem, i am i missing something ? it seem that it wait the end of the api call to put loadLibrary at true but in the mean time it keep call getLibrary() in loop. Thanks for helping
When you use
libraryViewModel: LibraryViewModel = LibraryViewModel()
You are directly constructing a brand new instance of your LibraryViewModel every time that method recomposes. Since you probably read your loadLibrary value once getLibrary returns, that causes a recomposition of that method, hence the infinite loop (as the recomposition again causes another brand new instance to be created...which kicks off a load...which causes another recomposition).
Instead, you should be following the documentation on using ViewModels with Compose:
Add the androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1 dependency to your build.gradle file
Use the viewModel() method to instantiate your ViewModel.
fun HomeScreen(
navigator: DestinationsNavigator,
libraryViewModel: LibraryViewModel = viewModel()
) {
The viewModel() method is what actually causes your ViewModel to be cached and stored across recompositions, navigating to a different screen, and across configuration changes. Since by using that method you'll only have a single instance of that ViewModel, you won't run into the same infinite loop.
Related
I'm trying to get specific behavior with focus and so use something like this :
val (focusA, focusB) = remember { FocusRequester.createRefs() }
And since i didn't get the correct behavior, start to investigate and the destructuring pattern with remember is the problem.
If you try this (this is what is it done under the hood of FocusRequester.createRefs()):
` class MyClass
object MyClassFactory{
operator fun component1() = MyClass()
operator fun component2() = MyClass()
}
fun createRefs() = MyClassFactory
#Composable
private fun ContentBody() {
val (a, b) = remember {
createRefs()
}
Log.d(">>:a", "${a.hashCode()}")
Log.d(">>:b", "${b.hashCode()}")
}
`
You will realise that a and b are new instance each time there is a recomposition.
Does any one have some information about that? Why remember fail with destructuring pattern. We can see many time this pattern (i use it with constraint layout for example), and according to that, it is a complete failure because each time a new instance are created...
What I'm doing wrong? I solved all my problem by using a remember without destructuring.
Thank.
In a Jetpack Compose component I'm subscribing to Room LiveData object using observeAsState.
The initial composition goes fine, data is received from ViewModel/LiveData/Room.
val settings by viewModel.settings.observeAsState(initial = AppSettings()) // Works fine the first time
A second composition is initiated, where settings - A non nullable variable is set to null, and the app crashed with an NPE.
DAO:
#Query("select * from settings order by id desc limit 1")
fun getSettings(): LiveData<AppSettings>
Repository:
fun getSettings(): LiveData<AppSettings> {
return dao.getSettings()
}
ViewModel:
#HiltViewModel
class SomeViewModel #Inject constructor(
private val repository: AppRepository
) : ViewModel() {
val settings = repository.getSettings()
}
Compose:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun ItemsListScreen(viewModel: AppViewModel = hiltViewModel()) {
val settings by viewModel.settings.observeAsState(initial = AppSettings())
Edit:
Just to clearify, the DB data does not change. the first time settings is fetched within the composable, a valid instance is returned.
Then the component goes into recomposition, when ItemsListScreen is invoked for the second time, then settings is null (the variable in ItemsListScreen).
Once the LiveData<Appsettings> is subscribed to will have a default value of null. So you get the default value required by a State<T> object, when you call LiveData<T>::observeAsState, followed by the default LiveData<T> value, this being null
LiveData<T> is a Java class that allows nullable objects. If your room database doesn't have AppSettings it will set it a null object on the LiveData<AppSettings> instance. As Room is also a Java library and not aware of kotlin language semantics.
Simply put this is an interop issue.
You should use LiveData<AppSettings?> in kotlin code and handle null objects, or use some sort of MediatorLiveData<T> that can filter null values for example some extensions functions like :
#Composable
fun <T> LiveData<T?>.observeAsNonNullState(initial : T & Any, default : T & Any) : State<T> =
MediatorLiveData<T>().apply {
addSource(this) { t -> value = t ?: default }
}.observeAsState(initial = initial)
#Composable
fun <T> LiveData<T?>.observeAsNonNullState(initial : T & Any) : State<T> =
MediatorLiveData<T>().apply {
addSource(this) { t -> t?.run { value = this } }
}.observeAsState(initial = initial)
If you only need to fetch settings when viewModel is initialised, you can try putting it in an init block inside your ViewModel.
I'm trying to get the last id added from entity A to entity B to add to entity B by it , I fetched the id of the last element added to entity A like this :
in Dao :
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(addSpendEntity: AddSpendEntity) : Long
and in fun insert in repo i used mutableLiveData to save the last id inserted and get it to viewmodel then to observing it in fragment
in repo :
class AddSpendRepository(private var database: PersonalAccountingDateBase) {
private var id : Long = 0
private var mutableLiveData = MutableLiveData<Long>()
suspend fun insert(addSpendEntity: AddSpendEntity){
id = database.getAddSpendDao().insert(addSpendEntity)
Log.e("addspendrepository",id.toString())
mutableLiveData.postvalue(id)
Log.e("addspendrepositoryid",mutableLiveData.value.toString())
...
}}
fun getMutableLiveData() : MutableLiveData<Long> = mutableLiveData
and in VM :
fun insertSpend(addSpendEntity: AddSpendEntity) = viewModelScope.launch(Dispatchers.IO) {
addSpendRepository.insert(addSpendEntity)
}
fun getMutableLiveData() : MutableLiveData<Long> = addSpendRepository.getMutableLiveData()
the observer in fragment i try to add to entity B When mutableLiveData is change :
private fun insert()
{
val totalMoney = binding.edtAddSpendSpendMoney.text.toString().toInt()
val notice = binding.edtAddSpendNotice.text.toString()
val date = binding.txtAddSpendDateText.text.toString()
val addSpendEntity = AddSpendEntity(totalMoney,notice,date)
addSpendViewModel.insertSpend(addSpendEntity)
addSpendViewModel.getMutableLiveData().observe(viewLifecycleOwner,
Observer {
Log.e("addspendfragment",it.toString())
if(it.toInt() != 0)
{
val dailyMovementEntity = DailyMovementEntity("make",totalMoney,notice,5,it.toInt())
addSpendViewModel.insertDailyMovement(dailyMovementEntity)
}
})
so the problem i faced is when to insert in the first time the value of mutable get null and the observer does'nt notice any thing then in the second time the observer notice the previos state of id and this condition continues as long as the application is running , when i close the app and do the same in the same way : The same problem is repeated as shown
enter image description here
You didn't show it in your code, so I'm just guessing, but here's a possible cause of your issue.
I'm guessing your ViewModel's insertSpend function is doing something like this:
fun insertSpend(addSpendEntity: AddSpendEntity) {
viewModelScope.launch(Dispatchers.IO) {
repository.insert(addSpendEntity)
}
}
The problem is, if you call MutableLiveData.value on a thread other than the main thread, then the change is not viewable until another loop of the main thread has occurred. You're not supposed to call .value on any thread besides the main thread. Then you get the proper value in your observer because observers are called on the next loop of the main thread.
Also, a suspend function should never require being called from a specific dispatcher, so you should not need to specify Dispatchers.IO when you launch your coroutine. More properly, your repository function should look like this, so it is safe to call it from anywhere. Any time a suspend function calls a function that requires a specific dispatcher, it is best to specify that dispatcher internally (I think of this as an extension of the single responsibility principle--outside functions shouldn't have to know what state to specify when calling another function if it can be avoided).
I would define it like this:
suspend fun insert(addSpendEntity: AddSpendEntity) = withContext(Dispatchers.Main) {
id = database.getAddSpendDao().insert(addSpendEntity) // it's safe to call this on main because it's a suspend function which by convention must not block
Log.e("addspendrepository",id.toString())
mutableLiveData.value = id
Log.e("addspendrepositoryid",mutableLiveData.value.toString())
// ...
}
Just my opinion:
On the ViewModel side, in general, you should rarely ever be launching a coroutine on the ViewModel scope with a specific dispatcher. Android has a general convention of treating the main thread as the default, and it is full of functions that must be called on main for proper behavior. So it is clean to always leave that as your default and only use withContext(Dispatchers.IO) (or .Default) for the bits of your coroutine that need it. And you should never need those just to call suspend functions, because of the coroutine convention that suspend functions must never block. So you only need them when calling blocking code.
I have problem working with MutableStateFlow, I cannot understand how it is working or I am mistaken somewhere. For example purpose I created simpler classes to get the idea what I am doing.
First I have data class which holds the values and controller which update values in the data class
data class ExampleUiState(
val dataFlag: Boolean = false
)
class ExampleController {
private val _exampleUiState = MutableStateFlow(ExampleUiState())
val exampleUiState = _exampleUiState.asStateFlow()
fun onChangeFlag(flag: Boolean) {
_exampleUiState.update { it.copy(dataFlag = flag) }
}
}
I am using koin, and I created Example controller singleton.
Second I am injection it in my ViewModel where I have two functions there
class ExampleViewModel(
private val exampleController: ExampleController
) : ViewModel() {
val exampleUiState = exampleController.exampleUiState.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
ExampleUiState()
)
//called second
private fun useFlagInViewModelFun() {
//here the value is not updated
exampleUiState.value.dataFlag
}
//called first from UI
fun changeValueFromUi(flag: Boolean) {
//change it from default false to true
exampleController.onChangeFlag(flag)
useFlagInViewModelFun()
}
}
The idea is when I call changeValueFromUi from some compose function, I update the value with my controller function, and after it I call other function where I want to use already updated state of data class, but I don't get the correct value.
Where I am mistaken?
Is there any time needed for onChangeFlag() to react and update the value?
Am I mistaken the way that I am trying to get the value after exampleUiState.value.dataFlag ?
I'm trying to follow the official guidelines to migrate from LiveData to Flow/StateFlow with Compose, as per these articles:
A safer way to collect flows from Android UIs
Migrating from LiveData to Kotlin’s Flow
I am trying to follow what is recommended in the first article, in the Safe Flow collection in Jetpack Compose section near the end.
In Compose, side effects must be performed in a controlled
environment. For that, use LaunchedEffect to create a coroutine that
follows the composable’s lifecycle. In its block, you could call the
suspend Lifecycle.repeatOnLifecycle if you need it to re-launch a
block of code when the host lifecycle is in a certain State.
I have managed to use .flowWithLifecycle() in this way to make sure the flow is not emmiting when the app goes to the background:
#Composable
fun MyScreen() {
val lifecycleOwner = LocalLifecycleOwner.current
val someState = remember(viewModel.someFlow, lifecycleOwner) {
viewModel.someFlow
.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.stateIn(
scope = viewModel.viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null
)
}.collectAsState()
}
I find this very "boilerplatey" -there must be something better. I would like to have StateFlow in the ViewModel, instead of Flow that gets converted to StateFLow in the #Composable, and use .repeatOnLifeCycle(), so I can use multiple .collectAsState() with less boilerplate.
When I try to use .collectAsState() inside a coroutine (LaunchedEffect), I obviously get an error about .collectAsState() having to be called from the context of #Composable function.
How can I achieve similar functionality as with .collectAsState(), but inside .repeatOnLifecycle(). Do I have to use .collect() on the StateFlow and then wrap the value with State? Isn't there anything with less boilerplate than that?
From "androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha01" you can use the collectAsStateWithLifecycle() extension function to collect from flow/stateflow and represents its latest value as Compose State in a lifecycle-aware manner.
import androidx.lifecycle.compose.collectAsStateWithLifecycle
#Composable
fun MyScreen() {
val state by viewModel.state.collectAsStateWithLifecycle()
}
Source: Android Lifecycle release
After reading a few more articles, including
Things to know about Flow’s shareIn and stateIn operators
repeatOnLifecycle API design story
and eventually realising that I wanted to have the StateFlow in the ViewModel instead of within the composable, I came up with these two solutions:
1. What I ended up using, which is better for multiple StateFlows residing in the ViewModel that need to be collected in the background while there is a subscriber from the UI (in this case, plus 5000ms delay to deal with configuration changes, like screen rotation, where the UI is still interested in the data, so we don't want to restart the StateFlow collecting routine). In my case, the original Flow is coming from Room, and been made a StateFlow in the VM so other parts of the app can have access to the latest data.
class MyViewModel: ViewModel() {
//...
val someStateFlow = someFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = Result.Loading()
)
val anotherStateFlow = anotherFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = Result.Loading()
)
//...
}
Then collected in the UI:
#Composable
fun SomeScreen() {
var someUIState: Any? by remember { mutableStateOf(null)}
var anotherUIState: Any? by remember { mutableStateOf(null)}
LaunchedEffect(true) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.someStateFlow.collectLatest {
someUIState = it
}
}
launch {
viewModel.anotherStateFlow.collectLatest {
anotherUIState = it
}
}
}
}
}
2. An extension function to alleviate the boilerplate when collecting a single StateFlow as State within a #Composable. This is useful only when we have an individual HOT flow that won't be shared with other Screens/parts of the UI, but still needs the latest data at any given time (hot flows like this one created with the .stateIn operator will keep collecting in the background, with some differences in behaviour depending on the started parameter). If a cold flow is enough for our needs, we can drop the .stateIn operator together with the initial and scope parameters, but in that case there's not so much boilerplate and we probably don't need this extension function.
#Composable
fun <T> Flow<T>.flowWithLifecycleStateInAndCollectAsState(
scope: CoroutineScope,
initial: T? = null,
context: CoroutineContext = EmptyCoroutineContext,
): State<T?> {
val lifecycleOwner = LocalLifecycleOwner.current
return remember(this, lifecycleOwner) {
this
.flowWithLifecycle(
lifecycleOwner.lifecycle,
Lifecycle.State.STARTED
).stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = initial
)
}.collectAsState(context)
}
This would then be used like this in a #Composable:
#Composable
fun SomeScreen() {
//...
val someState = viewModel.someFlow
.flowWithLifecycleStateInAndCollectAsState(
scope = viewModel.viewModelScope //or the composable's scope
)
//...
}
Building upon OP's answer, it can be a bit more light-weight by not going through StateFlow internally, if you don't care about the WhileSubscribed(5000) behavior.
#Composable
fun <T> Flow<T>.toStateWhenStarted(initialValue: T): State<T> {
val lifecycleOwner = LocalLifecycleOwner.current
return produceState(initialValue = initialValue, this, lifecycleOwner) {
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
collect { value = it }
}
}
}