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)
}
}
}
Related
I have a common situation of getting data. I use the Kotlin Coroutines.
1 variant:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
lateinit var data: List<String>
init {
viewModelScope.launch {
data = gettingData.get()
}
}
}
2 variant:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
val data = MutableStateFlow<List<String>?>(null)
init {
viewModelScope.launch {
data.emit(gettingData.get())
}
}
}
How can I initialize a data field not delayed, but immediately, with the viewModelScope but without a lateinit or nullble field? And without LiveData, my progect uses Coroutine Flow
I can't return a result of viewModelScope job in .run{} or by lazy {}.
I cant return a result drom fun:
val data: List<String> = getData()
fun getData(): List<String> {
viewModelScope.launch {
data = gettingData.get()
}
return ???
}
Also I can't make suspend fun getData() because I can't create coroutineScope in initialisation'
You're describing an impossibility. Presumably, gettingData.get() is defined as a suspend function, meaning the result literally cannot be retrieved immediately. Since it takes a while to retrieve, you cannot have an immediate value.
This is why apps and websites have loading indicators in their UI.
If you're using Flows, you can use a Flow with a nullable type (like in your option 2 above), and in your Activity/Fragment, in the collector, you show either a loading indicator or your data depending on whether it is null.
Your code 2 can be simplified using the flow builder and stateIn with a null default value:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
val data = flow<List<String>?> { emit(gettingData.get()) }
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
}
In your Activity or Fragment:
viewLifecycleOwner.lifecycleScope.launch {
viewModel.data
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect { list ->
if(list == null) {
// Show loading indicator in UI
} else {
// Show the data
}
}
}
If your data loads pretty quickly, instead of making the type nullable, you can just make the default value emptyList(). Then your collector can just not do anything when the list is empty. This works if the data loads quickly enough that the user isn't going to wonder if something is wrong because the screen is blank for so long.
You have to use SharedFlow with replay 1 (to store last value and replay it for a new subscriber) to implement it.
My sample:
interface DataSource {
suspend fun getData(): Int
}
class DataViewModel(dataSource: DataSource): ViewModel() {
val dataField =
flow<Int> {
emit(dataSource.getData())
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(1000), 1)
}
Still new to compose + kotlin, so I'm having some trouble getting stateflow working. There might be some fundamentals that I don't understand or missing some function.
Problem:
I have two stateflows in my view model and both would trigger a recomposition of the other one.
ViewModel:
private val _networkUiState = MutableStateFlow<NetworkUIState>(NetworkUIState.Empty)
val networkUIState: StateFlow<NetworkUIState> = _networkUiState.asStateFlow()
private val _uiState = MutableStateFlow<UIState>(UIState.Empty)
val uiState: StateFlow<UIState> = _uiState.asStateFlow()
fun someAPICall(
) {
_networkUiState.value = NetworkUIState.Loading
networkJob = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
try {
val response = repo.apiCall()
withContext(Dispatchers.Main) {
_networkUiState.value = NetworkUIState.Loaded
_uiState.value = UIState.DoSomething(response)
}
} catch (error: NetworkException) {
_networkUiState.value = NetworkUIState.NetworkError(error)
}
}
}
//exceptionHandler calls _networkUIState.value = NetworkUIState.NetworkError(some error) as well
Compose:
val networkUIState = viewModel.networkUIState.collectAsState().value
val uiState = viewModel.uiState.collectAsState().value
Column() {
//...
//UI code
when (uiState) {
is UIState.DoSomething -> {
//read uiState.response and do something
}
is UIState.DoAnotherThing -> {
//read response and do something else
}
}
when (networkUIState) {
is NetworkUIState.Error -> {
//display network error
}
is NetworkUIState.Loading -> {
//display loading dialog
}
else -> {}
}
}
What happens:
1st time calling api:
NetworkUIState triggers loading (display loading)
NetworkUIState triggers loaded (hides loading)
uiState triggers DoSomething with response data
2nd time calling api:
NetworkUIState triggers loading (display loading)
uiState triggers DoSomething with response data (from last call)
NetworkUIState triggers loaded (hides loading)
uiState triggers DoSomething with response data (new data)
I understand this is because of the recomposition of NetworkUiState before UIState but UIState still has the old value.
My question is how can I avoid this when I absolutely need to separate these 2 states at least in the view model?
Simplest thing you can do is providing an Idle or DoNothing state for your UiState which and set to this state when you are done doing event or when you start a new api call. This is a pattern i use a lot, i also use this if there shouldn't be a Loading in ui initially. uiState starts with Idle, when user clicks to fetch from api i set to Loading and then Error or Success based on data fetch result.
I also use this pattern with gestures to not leave state in Up state when user lifts fingers to not start drawing from Up state as in this answer
I have a counter logic using Flow in ViewModel, and auto increment.
class MainViewModel(
private val savedStateHandle: SavedStateHandle
): ViewModel() {
val counterFlow = flow {
while (true) {
val value = savedStateHandle.get<Int>("SomeKey") ?: 0
emit(value)
savedStateHandle["SomeKey"] = value + 1
delay(1000)
}
}
}
In the Activity
val counterFlowStateVariable = viewModel.externalDataWithLifecycle.collectAsStateWithLifecycle(0)
This counter will only increment and count during the App is active
It stops increment when onBackground, and continues when onForeground. It doesn't get reset. This is made possible by using collectAsStateWithLifecycle.
It stops increment when the Activity is killed by the system and restores the state when the Activity is back. The counter value is not reset. This is made possible by using savedStateHandle
I'm thinking if I can use a stateFlow instead of flow?
I would say, you should. flow is cold, meaning it has no state, so previous values aren't stored. Because your source of emitted values is external (savedStateHandle) and you mix emitting and saving that value within the flow builder, you introduce a synchronization problem, if more than one collector is active.
Perform small test:
// this value reflects "saveStateHandle"
var index = 0
val myFlow = flow {
while(true) {
emit(index)
index++
delay(300)
}
}
Now collect it three times:
launch {
myFlow.collect {
println("First: $it")
}
}
delay(299)
launch {
myFlow.collect {
println("second: $it")
}
}
delay(599)
launch {
myFlow.collect {
println("third: $it")
}
}
You'll start noticing that some collectors are reading previous values (newer values already read by other collectors), meaning their save operation will use that previous value, instead up to date one.
Using stateFlow you "centralize" the state read/update calls, making it independent of a number of active collectors.
var index = 0
val myFlow = MutableStateFlow(index)
launch {
while (true) {
index++
mySharedFlow.value = index
delay(300)
}
}
I'm trying to show a user information in DetailActivity. So, I request a data and get a data for the user from server. but in this case, the return type is Flow<User>. Let me show you the following code.
ServiceApi.kt
#GET("endpoint")
suspend fun getUser(#Query("id") id: Int): Response<User>
Repository.kt
fun getUser(id: Int): Flow<User> = flow<User> {
val userResponse = api.getUser(id = id)
if (userResponse.isSuccessful) {
val user = userResponse.body()
emit(user)
}
}
.flowOn(Dispatchers.IO)
.catch { // send error }
DetailViewModel.kt
class DetailViewModel(
private val repository : Repository
) {
val uiState: StateFlow<User> = repository.getUser(id = 369).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = User() // empty user
)
}
DetailActivity.kt
class DetailActivity: AppCompatActivity() {
....
initObersevers() {
lifecycleScope.launch {
// i used the `flowWithLifecycle` because the data is just a single object.
viewModel.uiState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { state ->
// show data
}
}
}
...
}
But, all of sudden, I just realized that this process is just an one-shot operation and thought i can use suspend function and return User in Repository.kt.
So, i changed the Repository.kt.
Repository.kt(changed)
suspend fun getUser(id: Int): User {
val userResponse = api.getUser(id = id)
return if(userResponse.isSuccessful) {
response.body()
} else {
User() // empty user
}
}
And in DetailViewModel, i want to convert the User into StateFlow<User> because of observing from DetailActivity and I'm going to use it the same way as before by using flowWithLifecycle.
the concept is... i thought it's just one single data and i dind't need to use Flow in Repository. because it's not several items like List.
is this way correct or not??
Yeap, this one-time flow doesn't make any sense - it emits only once and that's it.
You've got two different ways. First - is to create a state flow in your repo and emit there any values each time you're doing your GET request. This flow will be exposed to the use case and VM levels. I would say that it leads to more difficult error handling (I'm not fond of this way, but these things are always arguable, haha), but it also has some pros like caching, you can always show/get the previous results.
Second way is to leave your request as a simple suspend function which sends a request, returns the result of it back to your VM (skipping error handling here to be simple):
val userFlow: Flow<User>
get() = _userFlow
private val _userFlow = MutableStateFlow(User())
fun getUser() = launch(viewModelScope) {
_userFlow.value = repository.getUser()
}
This kind of implementation doesn't provide any cache out of scope of this VM's lifecycle, but it's easy to test and use.
So it's not like there is only one "the-coolest-way-to-do-it", it's rather a question what suits you more for your needs.
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()