whenever I try to update a mutable state flow (uiState), the code in the init of the viewModel executes again (and hence the uiState resets).
This only happens if I use any info related to the previous state of the uiState (at least that's why I think given the following code).
The Code:
init {
Log.d("Error log", "init again")
}
private val _uiState: MutableStateFlow<CreateGameState> =
MutableStateFlow(
CreateGameState.InputDataState(
selectedHeroes = listOf(),
selectedVillain = null,
selectedEncounters = listOf(),
)
)
val uiState: StateFlow<CreateGameState> = _uiState
fun onAction(action: CreateGameActions) {
when (action) {
is CreateGameActions.SelectHero -> {
when (_uiState.value) {
is CreateGameState.InputDataState -> {
_uiState.update {
/* this line causes the error */(it as CreateGameState.InputDataState).copy(selectedHeroes = it.selectedHeroes.plusElement(action.hero))
/* this line doesn't cause error */(it as CreateGameState.InputDataState).copy(selectedHeroes = listOf(Hero.SPIDERMAN))
}
}
}
}
}
}
}
So when only executing the line that causes the error, the Log of the init function is shown in Logcat again.
With this, I loose the uiState and my app gets stuck on the initial state always.
Thanks a lot for your help in advance!
#Tenfour04 was right.
I turns out I was injecting the viewModel in a composable with the constructor as a default parameter. Recomposition was causing the creation of a new ViewModel.
Simply by injecting the same instance of the viewModel solved it.
Related
I have following code for my main screen:
#Composable
fun MainScreen() {
val viewModel = getViewModel<MainViewModel>()
val tasks: List<Task> by viewModel.taskList.observeAsState(listOf())
LazyColumn(contentPadding = PaddingValues(bottom = 96.dp)) {
items(tasks) { task ->
if (task == tasks[0]) {
ListItemActive(task = task)
} else {
ListItemInactive(task = task)
}
}
}
val isInProgress: Boolean by viewModel.isInProgress.observeAsState(false)
if (isInProgress) {
Preloader()
}
}
Navigation is managed in main activity like this:
NavHost(navController = navController, startDestination = Screen.Main.route) {
composable(Screen.Login.route) { LoginScreen() }
composable(Screen.Main.route) { MainScreen() }
}
The problem is that whenever I adjust paddings or other sizes in android studio my Main screen composition is reevaluated. Which is what one would expect, but instead of taking existing view model, new one is created. And onCleared is not called for the old one. Is this an expected behavior, or am I missing something?
EDIT:
I'm also encountering a crash when any changes are made in code and live editor tries to update them on emulator.
Exception: reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
It happens in code below, when accessing SharedPreferences. This code is called from init block in view model which is initialised a second time during padding change in code.
httpClient.addInterceptor { chain ->
val request: Request = chain
.request()
.newBuilder()
.addHeader("Authorization", "Bearer ${get<PreferenceRepository>().accessToken}")
.build()
chain.proceed(request)
}
Though it does seem to be linked to viewModelScope I'm using. Replacing it with GlobalScope stops app from crashing.
Koin 3.1.2 doesn't work with Navigation Compose. The bug is tracked on GitHub: https://github.com/InsertKoinIO/koin/issues/1079.
As an alternative you can use cokoin library:
#Composable
fun App() {
val navController = rememberNavController()
KoinNavHost(navController, startDestination = "1") {
composable("1") {
val navViewModel = getNavViewModel<NavViewModel>()
//...
}
}
}
Okay so I've been using StateFlow with Room database for a while. Now I have one common case. At the start of my app I have a logic that if ROOM database is empty I should show an EmptyContent(), otherwise I will show the ListContent() from ROOM database.
Now every time I launch the app, I'm always getting that EmptyContent() shown for a HALF a second maybe, and then the ListContent() is displayed. After that when I'm using the app everything works normal. But at that app launch time, while ROOM database is working I guess, that EmptyContent() is shown for just a small amount of period (Because my StateFlow default value is an empty list), and after that the actual LIST from Database is displayed.
Now I have one solution for that, to just use delay() function inside a Coroutine, to wait for example 200MS and then trigger the function for reading the DATABASE, because those 200MS are enough for ROOM database to actually get the value and update my STATE FLOW variable with the actual data instead of using that StateFlow default value for a half second at the beginning.
Is that a good solution, I must ask? Because I'm using coroutine, the thread is not blocked, and I'm just waiting until ROOM database updates my STATE FLOW variable the second time.
#Composable
fun displayContent(
tasks: List<ToDoTask>,
ListContent: #Composable () -> Unit
) {
val scope = rememberCoroutineScope()
var counter by remember { mutableStateOf(0)}
LaunchedEffect(Unit){
scope.launch {
delay(200)
counter = 1
}
}
if(counter == 1){
if (tasks.isNotEmpty()) {
ListContent()
} else {
EmptyContent()
}
}
}
My suggestion would be map your expected states.
For instance:
sealed class RequestState<out T> {
object Idle : RequestState<Nothing>()
object Loading : RequestState<Nothing>()
data class Success<T>(val data: T) : RequestState<T>()
data class Error(
val t: Throwable,
var consumed: Boolean = false
) : RequestState<Nothing>()
}
And your function would be something like:
#Composable
fun YourScreen() {
val requestState = viewModel.screenData.collectAsState()
when (requestState) {
is Idle ->
// This is the default state, do nothing
is Loading ->
// Display some progress indicator
is Success ->
YourListScreen(requestState.data) // Show the list
is Error ->
// Display an error.
}
LaunchedEffect(Unit) {
viewModel.loadData()
}
}
Of course, in your view model you must emit these values properly...
class YourView: ViewModel() {
private val _screenData =
MutableStateFlow<RequestState<List<ToDoTask>>>(RequestState.Idle)
val screenDatae: StateFlow<RequestState<List<ToDoTask>>> = _screenData
fun loadData() {
_screenData.value = Loading
try {
// load the data from database
_screenData.value = Success(yourLoadedData)
} catch (e: Exception) {
_screenData.value = Error(e)
}
}
}
I have a usecase:
Open app + disable network -> display error
Exit app, then enable network, then open app again
Expected: app load data
Actual: app display error that meaning state error cached, liveData is not emit
Repository class
class CategoryRepository(
private val api: ApiService,
private val dao: CategoryDao
) {
val categories: LiveData<Resource<List<Category>>> = liveData {
emit(Resource.loading(null))
try {
val data = api.getCategories().result
dao.insert(data)
emit(Resource.success(data))
} catch (e: Exception) {
val data = dao.getCategories().value
if (!data.isNullOrEmpty()) {
emit(Resource.success(data))
} else {
val ex = handleException(e)
emit(Resource.error(ex, null))
}
}
}
}
ViewModel class
class CategoryListViewModel(
private val repository: CategoryRepository
): ViewModel() {
val categories = repository.categories
}
Fragment class where LiveDate obsever
viewModel.apply {
categories.observe(viewLifecycleOwner, Observer {
// live data only trigger first time, when exit app then open again, live data not trigger
})
}
can you help me explain why live data not trigger in this usecase and how to fix? Thankyou so much
Update
I have resolved the above problem by replace val categories by func categories() at repository class. However, I don't understand and can't explain why it works properly with func but not val.
Why does this happen? This happens because your ViewModel has not been killed yet. The ViewModel on cleared() is called when the Fragment is destroyed. In your case your app is not killed and LiveData would just emit the latest event already set. I don't think this is a case to use liveData builder. Just execute the method in the ViewModel when your Fragment gets in onResume():
override fun onResume(){
viewModel.checkData()
super.onResume()
}
// in the viewmodel
fun checkData(){
_yourMutableLiveData.value = Resource.loading(null)
try {
val data = repository.getCategories()
repository.insert(data)
_yourMutableLiveData.value = Resource.success(data)
} catch (e: Exception) {
val data = repository.getCategories()
if (!data.isNullOrEmpty()) {
_yourMutableLiveData.value = Resource.success(data)
} else {
val ex = handleException(e)
_yourMutableLiveData.value = Resource.error(ex,null)
}
}
}
Not sure if that would work, but you can try to add the listener directly in onResume() but careful with the instantiation of the ViewModel.
Small advice, if you don't need a value like in Resource.loading(null) just use a sealed class with object
UPDATE
Regarding your question that you ask why it works with a function and not with a variable, if you call that method in onResume it will get executed again. That's the difference. Check the Fragment or Activity lifecycle before jumping to the ViewModel stuff.
I am using MVVM, LiveData and trying and implement Repository pattern.
But, calling a method in my repository class - RegisterRepo which returns LiveData is not working. I have no idea why. Any suggestions would be greatly appreciated.
Boilerplate code is removed for breivity.
Activity' s onCreateMethod
mViewModel.status.observe(this, Observer {
when (it) {
true -> {
Log.d("----------", " true ") //These message is never being printed.
}
false -> {
Log.d("----------", "false ") //These message is never being printed.
}
}
})
button.setOnClickListener {
mViewModel.a()
}
ViewModel
class AuthViewModel (val repo: RegisterRepo): ParentViewModel() {
//...
var status = MutableLiveData<Boolean>()
fun a() {
status = repo.a()
}
}
RegisterRepo
class RegisterRepo () {
fun a(): MutableLiveData<Boolean> {
var result = MutableLiveData<Boolean>()
result.value = true
return result
}
}
However, if I change my code in ViewModel to this, everything is working fine.
ViewModel
class AuthViewModel (val repo: RegisterRepo): ParentViewModel() {
//...
var status = MutableLiveData<Boolean>()
fun a() {
status.value = true //Change here causing everything work as expected.
}
}
In the first ViewModel code, when method a is called, you assign another LiveData to status variable, this live data is different from the one observed by the Activity, so that the value won't be notify to your Activity
the 2nd way is correct to use and it will work fine the 1st is not working because you are creating new MutableLive data in your RegisterRepo, so basically at the time your create an observable to "status" is deferent where you assign a value into it is different. so the second one is the only way to do this
In my current Android project I have a dialog that retrieves a list of objects from a webservice and show these in a list. It has a problem though. The webservice (outside my control) is not the fastest, so the process takes a while and often the user changes the orientation of the device while this process is ongoing. For now the orientation change results in the original webservice call to be cancelled and a new one is created, but this is not how it should be done I know. I would like the dialog to able to continue the loading from the webservice when an orientation change happens, but I just can't get my head around how to do this. How is it possible to hook into an ongoing call and display this state in the view (the dialog)? Any recommendations are appreciated. I've played around with Android Architecture Components and kotlins sealed classes saving the viewstate in a livedata object that the view observes, but have not found a solution that I like.
I believe a lot of developers use RxJava for this kind of issue. Is this my only option?
Btw. at the moment I use MVP architecture in my project.
Edit
This is where I cancel the job - if the listener is null its cancelled (I'm using kotlin coroutines):
override fun getWorklist() {
job = onWorkerThread {
val result = repository.getResult().awaitResult()
onMainThread {
when (result) {
is Result.Ok -> listener?.onResult(result.getOrDefault(emptyList())) :? job.cancel()
// Any HTTP error
is Result.Error -> listener?.onHttpError(result.exception) :? job.cancel()
// Exception while request invocation
is Result.Exception -> listener?.onException(result.exception) :? job.cancel()
}
}
}
}
Edit 2
I've tried controlling the viewstate using this Android Arch ViewModel, but the MediatorLiveData object isn't triggered on changes of the viewstate object. And the view stays in Loading state:
class MainModel : ViewModel() {
private var job: Job? = null
val viewState = MutableLiveData<MainViewState>().apply { value = Loading("Warsaw") }
val dataGetter = MediatorLiveData<MainViewState>().apply {
addSource(viewState, {
Log.d("MainModel", "Viewstate is: " + viewState.value.toString() + ")...")
when(it) {
is Loading -> {
Log.d("MainModel", "Launching coroutine...")
job = launch(UI) {
Log.d("MainModel", "Loading...")
val items = Repository.getWorklist()
Log.d("MainModel", "Charts retrieved...")
this#MainModel.viewState.postValue(Success(it.items))
Log.d("MainModel", "Posted update to viewstate...")
}
}
}
})
}
override fun onCleared() {
Log.d("MainModel", "Clearing ViewModel")
job?.cancel()
}
}
interface ViewState
sealed class MainViewState : ViewState
class Loading() : MainViewState()
class Error(val error: String) : MainViewState()
class Success(val items: List<WorklistItem>) : MainViewState()