Using livedata coroutine doesn't gets executed - android

I am using the liveData coroutine as follows. My function takes 3 params - accessing database, make a API call and return the API result
fun <T, A> performGetOperation(
databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> Resource<A>,
saveCallResult: suspend (A) -> Unit
): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
emit(Resource.loading())
val source = databaseQuery.invoke().map { Resource.success(it) }
emitSource(source)
val responseStatus = networkCall.invoke()
if (responseStatus.status == SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == ERROR) {
emit(Resource.error(responseStatus.message!!))
emitSource(source)
}
}
I am calling the function as
fun getImages(term: String) = performGetOperation(
databaseQuery = {
localDataSource.getAllImages(term) },
networkCall = {
remoteDataSource.getImages(term) },
saveCallResult = {
val searchedImages = mutableListOf<Images>()
it.query.pages.values.filter {
it.thumbnail != null
}.map {
searchedImages.add(Images(it.pageid, it.thumbnail!!.source, term))
}
localDataSource.insertAll(searchedImages)
}
)
This is my viewmodel class
class ImagesViewModel #Inject constructor(
private val repository: WikiImageRepository
) : ViewModel() {
var images: LiveData<Resource<List<Images>>> = MutableLiveData()
fun fetchImages(search: String) {
images = repository.getImages(search)
}
}
From my fragment I am observing the variable
viewModel.images?.observe(viewLifecycleOwner, Observer {
when (it.status) {
Resource.Status.SUCCESS -> {
println(it)
}
Resource.Status.ERROR ->
Toast.makeText(requireContext(), it.message, Toast.LENGTH_SHORT).show()
Resource.Status.LOADING ->
println("loading")
}
})
I have to fetch new data on click of button viewModel.fetchImages(binding.searchEt.text.toString())
Function doesn't gets executed. Is there something I have missed out?

The liveData {} extension function returns an instance of MediatorLiveData
liveData { .. emit(T) } // is a MediatorLiveData which needs a observer to execute
Why is the MediatorLiveData addSource block not executed ?
We need to always observe a MediatorLiveData using a liveData observer else the source block is never executed
So to make the liveData block execute just observe the liveData,
performGetOperation(
databaseQuery = {
localDataSource.getAllImages(term) },
networkCall = {
remoteDataSource.getImages(term) },
saveCallResult = {
localDataSource.insertAll(it)
}
).observe(lifecyleOwner) { // observing the MediatorLiveData is necessary
}
In your case every time you call
images = repository.getImages(search)
a new instance of mediator liveData is created which does not have any observer. The old instance which is observed is ovewritten. You need to observe the new instance of getImages(...) again on button click.
images.observe(lifecycleOwner) { // on button click we observe again.
// your observer code goes here
}
See MediatorLiveData and this

Related

Stop collecting Flow in ViewModel when app in background

Need to collect flow in ViewModel and after some data modification, the UI is updated using _batteryProfileState.
Inside compose I'm collecting states like this
val batteryProfile by viewModel.batteryProfileState.collectAsStateWithLifecycle()
batteryProfile.voltage
In ViewModel:
private val _batteryProfileState = MutableStateFlow(BatteryProfileState())
val batteryProfileState = _batteryProfileState.asStateFlow()
private fun getBatteryProfileData() {
viewModelScope.launch {
// FIXME In viewModel we should not collect it like this
_batteryProfile(Unit).collect { result ->
_batteryProfileState.update { state ->
when(result) {
is Result.Success -> {
state.copy(
voltage = result.data.voltage?.toString()
?.plus(result.data.voltageUnit
)
}
is Result.Error -> {
state.copy(
errorMessage = _context.getString(R.string.something_went_wrong)
)
}
}
}
}
}
}
The problem is when I put my app in the background the _batteryProfile(Unit).collect does not stop collecting while in UI batteryProfile.voltage stop updating UI which is correct behavior as I have used collectAsStateWithLifecycle() for UI.
But I have no idea how to achieve the same behavior for ViewModel.
In ViewModel I have used stateIn operator and access data like below everything working fine now:
val batteryProfileState = _batteryProfile(Unit).map { result ->
when(result) {
is Result.Success -> {
BatteryProfileState(
voltage = result.data.voltage?.toString()
?.plus(result.data.voltageUnit.unit)
?: _context.getString(R.string.msg_unknown),
)
}
is Result.Error -> {
BatteryProfileState(
errorMessage = _context.getString(R.string.something_went_wrong)
)
}
}
}.stateIn(viewModelScope, WhileViewSubscribed, BatteryProfileState())
collecting data in composing will be the same
Explanation: WhileViewSubscribed Stops updating data while the app is in the background for more than 5 seconds.
val WhileViewSubscribed = SharingStarted.WhileSubscribed(5000)
You can try to define getBatteryProfileData() as suspend fun:
suspend fun getBatteryProfileData() {
// FIXME In viewModel we should not collect it like this
_batteryProfile(Unit).collect { result ->
_batteryProfileState.update { state ->
when(result) {
is Result.Success -> {
state.copy(
voltage = result.data.voltage?.toString()
?.plus(result.data.voltageUnit
)
}
is Result.Error -> {
state.copy(
errorMessage = _context.getString(R.string.something_went_wrong)
)
}
}
}
}
}
And than in your composable define scope:
scope = rememberCoroutineScope()
scope.launch {
yourviewmodel.getBatteryProfileData()
}
And I think you can move suspend fun getBatteryProfileData() out of ViewModel class...

How to omit the last value of a liveData when the fragment / activity is recreated?

I have a problem with liveData in a particular case. When the response from a http service is Code = 2, it means that the session token has expired. In that case I navigate to the LoginFragment to login the user again. If the user logs in, then I return to the fragment which was previously and when I start to observe the liveData in onViewCreated function, it gives me its last value which is: Code = 2, so the Application navigates back to the login, which is wrong.
I have a Sealed Class:
sealed class Resource<T>(
var data: T? = null,
val message: String? = null,
val Code: Int? = null
) {
class Success<T>(data: T?) : Resource<T>(data)
class Error<T>(message: String, code: Int? = null) : Resource<T>(message = message, Code = code)
class Loading<T> : Resource<T>()
}
This is the code on the ViewModel:
val mLiveData: MutableLiveData<Resource<Data>> = MutableLiveData()
fun getData() {
viewModelScope.launch {
mLiveData.postValue(Resource.Loading())
try {
if (app.hasInternetConnection()) {
// get Data From API
val response = repository.getData()
if(response.isSuccessful){
mLiveData.postValue(Resource.Success(parseSuccessResponse(response.body())))
} else {
mLiveData.postValue(Resource.Error(parseErrorResponse(response.body())))
}
} else {
mLiveData.postValue(Resource.Error("Connection Fail"))
}
} catch (t: Throwable) {
when (t) {
is IOException -> mLiveData.postValue(Resource.Error("Connection Fail"))
else -> mLiveData.postValue(Resource.Error("Convertion Fail"))
}
}
}
}
This is the code on the fragment, observeForData() is called in onViewCreated function:
private fun observeForData() {
mLiveData.getData.observe(viewLifecycleOwner, Observer { response ->
when (response) {
is Resource.Success -> {
isLoading = false
updateUI(response.data)
}
is Resource.Error -> {
isLoading = false
if (response.Code == 2) {
// Token Expired
navigateToLogin()
} else {
showErrorMessage(response.message)
}
}
is Resource.Loading -> {
isLoading = true
}
}
})
}
How can i solve this?
Is there a way to remove the last value or state from a liveData when the fragment is destroyed when navigating to the LoginFragment?
Thank you.
One often-suggested solution is SingleLiveEvent, which is a simple class you can copy-paste into your project.
For a framework solution, I suggest SharedFlow. Some Android developers recommend switching from LiveData to Flows anyway to better decouple data from views. If you give SharedFlow a replay value of 0, new Activities and Fragments that observe it will not get the previous value, only newly posted values.
Sample untested ViewModel code:
val dataFlow: Flow<Resource<Data>> = MutableSharedFlow(replay = 0)
init {
viewModelScope.launch {
// Same as your code, but replace mLiveData.postValue with dataFlow.emit
}
}
And in the Fragment:
private fun observeForData() {
isLoading = true
lifecycleScope.launch {
mLiveData.dataFlow
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.collect { onDataResourceUpdate(it) }
}
}
// (Broken out into function to reduce nesting)
private fun onDataResourceUpdate(resource: Resource): Unit = when(resource) {
is Resource.Success -> {
isLoading = false
updateUI(response.data)
}
is Resource.Error -> {
isLoading = false
if (response.Code == 2) {
// Token Expired
navigateToLogin()
} else {
showErrorMessage(response.message)
}
}
is Resource.Loading -> isLoading = true
}
To change last updated value for live data,You can set "Resource" class with default null values when onDestroy().
onDestroy(){
//in java ,it will be new Resource instance
Resource resourceWithNull=new Resource();
mLiveData.setValue(resourceWithNull);
}
when you relaunch the fragment live data will emit Resource with null value as response.
Then You can write your code with in observer
if(response.data!=null)
{
//your code
}

#Composable invocations can only happen from the context of a #Composable functionn

How can I call a composable function from context of corrutines?
I trying the following code but I getting the error.
#Composable
fun ShowItems(){
var ListArticle = ArrayList<Article>()
lifecycleScope.launchWhenStarted {
// Triggers the flow and starts listening for values
viewModel.uiState.collect { uiState ->
// New value received
when (uiState) {
is MainViewModel.LatestNewsUiState.Success -> {
//Log.e(TAG,"${uiState.news}")
if(uiState.news != null){
for(i in uiState.news){
ListArticle.add(i)
}
context.ItemNews(uiState.news.get(4))
Log.e(TAG,"${uiState.news}")
}
}
is MainViewModel.LatestNewsUiState.Error -> Log.e(TAG,"${uiState.exception}")
}
}
}
}
You should do something like this:
#Composable
fun ShowItems(){
val uiState = viewModel.uiState.collectAsState()
// Mount your UI in according to uiState object
when (uiState.value) {
is MainViewModel.LatestNewsUiState.Success -> { ... }
is MainViewModel.LatestNewsUiState.Error -> { ... }
}
// Launch a coroutine when the component is first launched
LaunchedEffect(viewModel) {
// this call should change uiState internally in your viewModel
viewModel.loadYourData()
}
}

Trying to expose SavedStateHandle.getLiveData() as MutableStateFlow, but the UI thread freezes

I am trying to use the following code:
suspend fun <T> SavedStateHandle.getStateFlow(
key: String,
initialValue: T? = get(key)
): MutableStateFlow<T?> = this.let { handle ->
withContext(Dispatchers.Main.immediate) {
val liveData = handle.getLiveData<T?>(key, initialValue).also { liveData ->
if (liveData.value === initialValue) {
liveData.value = initialValue
}
}
val mutableStateFlow = MutableStateFlow(liveData.value)
val observer: Observer<T?> = Observer { value ->
if (value != mutableStateFlow.value) {
mutableStateFlow.value = value
}
}
liveData.observeForever(observer)
mutableStateFlow.also { flow ->
flow.onCompletion {
withContext(Dispatchers.Main.immediate) {
liveData.removeObserver(observer)
}
}.onEach { value ->
withContext(Dispatchers.Main.immediate) {
if (liveData.value != value) {
liveData.value = value
}
}
}.collect()
}
}
}
I am trying to use it like so:
// in a Jetpack ViewModel
var currentUserId: MutableStateFlow<String?>
private set
init {
runBlocking(viewModelScope.coroutineContext) {
currentUserId = state.getStateFlow("currentUserId", sessionManager.chatUserFlow.value?.uid)
// <--- this line is never reached
}
}
UI thread freezes. I have a feeling it's because of collect() as I'm trying to create an internal subscription managed by the enclosing coroutine context, but I also need to get this StateFlow as a field. There's also the cross-writing of values (if either changes, update the other if it's a new value).
Overall, the issue seems to like on that collect() is suspending, as I never actually reach the line after getStateFlow().
Does anyone know a good way to create an "inner subscription" to a Flow, without ending up freezing the surrounding thread? The runBlocking { is needed so that I can synchronously assign the value to the field in the ViewModel constructor. (Is this even possible within the confines of 'structured concurrency'?)
EDIT:
// For more details, check: https://gist.github.com/marcellogalhardo/2a1ec56b7d00ba9af1ec9fd3583d53dc
fun <T> SavedStateHandle.getStateFlow(
scope: CoroutineScope,
key: String,
initialValue: T
): MutableStateFlow<T> {
val liveData = getLiveData(key, initialValue)
val stateFlow = MutableStateFlow(initialValue)
val observer = Observer<T> { value ->
if (value != stateFlow.value) {
stateFlow.value = value
}
}
liveData.observeForever(observer)
stateFlow.onCompletion {
withContext(Dispatchers.Main.immediate) {
liveData.removeObserver(observer)
}
}.onEach { value ->
withContext(Dispatchers.Main.immediate) {
if (liveData.value != value) {
liveData.value = value
}
}
}.launchIn(scope)
return stateFlow
}
ORIGINAL:
You can piggyback over the built-in notification system in SavedStateHandle, so that
val state = savedStateHandle.getLiveData<State>(Key).asFlow().shareIn(viewModelScope, SharingStarted.Lazily)
...
savedStateHandle.set(Key, "someState")
The mutator happens not through methods of MutableLiveData, but through the SavedStateHandle that will update the LiveData (and therefore the flow) externally.
I am in a similar position, but I do not want to modify the value through the LiveData (as in the accepted solution). I want to use only flow and leave LiveData as an implementation detail of the state handle.
I also did not want to have a var and initialize it in the init block. I changed your code to satisfy both of these constraints and it does not block the UI thread. This would be the syntax:
val currentUserId: MutableStateFlow<String?> = state.getStateFlow("currentUserId", viewModelScope, sessionManager.chatUserFlow.value?.uid)
I provide a scope and use it to launch a coroutine that handles flow's onCompletion and collection. Here is the full code:
fun <T> SavedStateHandle.getStateFlow(
key: String,
scope: CoroutineScope,
initialValue: T? = get(key)
): MutableStateFlow<T?> = this.let { handle ->
val liveData = handle.getLiveData<T?>(key, initialValue).also { liveData ->
if (liveData.value === initialValue) {
liveData.value = initialValue
}
}
val mutableStateFlow = MutableStateFlow(liveData.value)
val observer: Observer<T?> = Observer { value ->
if (value != mutableStateFlow.value) {
mutableStateFlow.value = value
}
}
liveData.observeForever(observer)
scope.launch {
mutableStateFlow.also { flow ->
flow.onCompletion {
withContext(Dispatchers.Main.immediate) {
liveData.removeObserver(observer)
}
}.collect { value ->
withContext(Dispatchers.Main.immediate) {
if (liveData.value != value) {
liveData.value = value
}
}
}
}
}
mutableStateFlow
}

How to make Coroutine sequential call

I have a requirement after I call the local database(room) and get the data then only I will fire the network call.
If local data is present then network call will not happen otherwise network call will happen and it will store the data.
I have tried the same with a strategy class but network call is happening before it checks the local database.
DataAccessStrategy.kt
fun <T, A> fetchCategory(databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> Resource<A>,
saveCallResult: suspend (A) -> Unit,
clearDB: suspend () -> Unit): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
emit(Resource.loading())
val categoryList = databaseQuery().map { it ->
Resource.success(it)
}
emitSource(categoryList)
if (categoryList.value?.data == null || Preferences(MainApplication.getContext())
.getBooleanPreference(MainApplication.getContext(), Preferences.APP_CATEGORY_UPDATE)) {
val networkCallStatus = networkCall()
if (networkCallStatus.status == SUCCESS) {
if (Preferences(MainApplication.getContext()).getBooleanPreference(MainApplication.getContext(),
Preferences.APP_CATEGORY_UPDATE)) {
clearDB()
}
saveCallResult(networkCallStatus.data!!)
Preferences(MainApplication.getContext()).storeBooleanPreference(MainApplication.getContext(),
Preferences.APP_CATEGORY_UPDATE, false)
} else if (networkCallStatus.status == ERROR) {
emit(Resource.error(networkCallStatus.message!!))
emitSource(categoryList)
}
}
}
ProjectRepository
class ProjectRepository constructor(
private val homePagePostDAO: HomePagePostDAO,
private val categoryDAO: CategoryDAO,
private val homeRemoteDataSource: HomeRemoteDataSource
) {
val TAG = "ProjectRepository"
val getCategoryFromDB = categoryDAO.getAllCategory()
fun getAllCategoryList() = fetchCategory(
databaseQuery = { categoryDAO.getAllCategory() },
networkCall = { homeRemoteDataSource.getCategory() },
saveCallResult = { categoryDAO.insertAllCategory(it.data) },
clearDB = { categoryDAO.deleteAllCategory() }
)
}
So the main requirement is until I get the data from the room db it
should not execute the next line.
I want to make a sequential call using a coroutine. That means I have
two calls one is db select query another one is a retrofit network
call. Retrofit network call will start only if db call is finish.

Categories

Resources