How to handle errors with liveData - android

In my app, I have this flow:
ClickListender in my fragment:
search_button.setOnClickListener {
if(search_input.text.isNullOrEmpty())
Toast.makeText(activity, "Input Error", Toast.LENGTH_LONG).show()
else
viewModel.onSearchButtonClicked(search_input.text.toString())
}
onSearchButtonClicked inside viewModel:
fun onSearchButtonClicked(input: String) {
coroutineScope.launch {
repo.insertToDatabase(input)
}
}
insertToDatabase inside Repository:
suspend fun insertToDatabase(string: String) {
withContext(Dispatchers.IO) {
val dataList =
ExternalApi.retrofitCall.getData(string).await()
if (dataList.intialDataResult < 1) {
//show error
} else {
//all good
database.myDataBase.insertAll(dataList)
}
}
}
I need to show error message if intialDataResult is less then one.
I thought about create MutableLiveData inside my repository with initial value of false and listen from the fragment through the viewModel, but it's not good approach because I have no way to set the LiveData to "false" again after I show error message.
I also tried to return bool from the insertToDatabase function and decide if to show error or not, with no success.
Any ideas how can I solve this?

Why not create a LiveData to manage your work's result state?
Create a class to store result of work why sealed class?
sealed class ResultState{
object Success: ResultState() // this is object because I added no params
data class Failure(val message: String): ResultState()
}
Create a LiveData to report this result
val stateLiveData = MutableLiveData<ResultState>()
Make insertToDatabase() return a result
suspend fun insertToDatabase(input: String): ResultState {
return withContext<ResultState>(Dispatchers.IO) {
val dataList =
ExternalApi.retrofitCall.getData(string).await()
if (dataList.intialDataResult < 1) {
return#withContext ResultState.Failure("Reason of error...")
} else {
database.myDataBase.insertAll(dataList)
return#withContext ResultState.Success
}
}
}
Now, report result to UI
fun onSearchButtonClicked(input: String) {
coroutineScope.launch {
val resultState = repo.insertToDatabase(input)
stateLiveData.value = resultState
}
}
In UI,
viewModel.stateLiveData.observe(viewLifeCycleOwner, Observer { state ->
when (state) {
is ResultState.Success -> { /* show success in UI */ }
is ResultState.Failure -> { /* show error in UI with state.message variable */ }
}
})
Similarly, you can add a ResultState.PROGRESS to show that a task is running in the UI.
If you have any queries, please add a comment.

Related

Proper way of handle sealed class property in kotlin

Hey I am working in Android Kotlin. I am learning this LatestNewsUiState sealed class example from Android doc. I made my own sealed class example. But I am confused little bit, how can I achieved this. Is I am doing right for my scenario or not?
DataState.kt
sealed class DataState {
data class DataFetch(val data: List<Xyz>?) : DataState()
object EmptyOnFetch : DataState()
object ErrorOnFetch : DataState()
}
viewmodel.kt
var dataMutableStateFlow = MutableStateFlow<DataState>(DataState.EmptyOnFetch)
fun fetchData() {
viewModelScope.launch {
val result = repository.getData()
result.handleResult(
onSuccess = { response ->
if (response?.items.isNullOrEmpty()) {
dataMutableStateFlow.value = DataState.EmptyOnFetch
} else {
dataMutableStateFlow.value = DataState.DataFetch(response?.items)
}
},
onError = {
dataMutableStateFlow.value = DataState.ErrorOnFetch
}
)
}
}
fun fetchMoreData() {
viewModelScope.launch {
val result = repository.getData()
result.handleResult(
onSuccess = { response ->
if (response?.items.isNullOrEmpty()) {
dataMutableStateFlow.value = DataState.EmptyOnFetch
} else {
dataMutableStateFlow.value = DataState.DataFetch(response?.items)
}
},
onError = {
dataMutableStateFlow.value = DataState.ErrorOnFetch
}
)
}
}
Activity.kt
lifecycleScope.launchWhenStarted {
viewModel.dataMutableStateFlow.collectLatest { state ->
when (state) {
is DataState.DataFetch -> {
binding.group.visibility = View.VISIBLE
}
DataState.EmptyOnFetch,
DataState.ErrorOnFetch -> {
binding.group.visibility = View.GONE
}
}
}
}
}
I have some points which I want to achieve in the standard ways.
1. When your first initial api call fetchData() if data is not null or empty then we need to show view. If data is empty or null then we need to hide the view. But if api fail then we need to show an error message.
2. When view is visible and view is showing some data. Then we call another api fetchMoreData() and data is empty or null then I don't want to hide view as per code is written above. And If api fails then we show error message.
Thanks in advance

Fragment popbackstack trigger lifecyclescope collect

Situation
I submit data setTripDeliver, the collect works fine (trigger LOADING and then SUCCESS). I pressed a button go to next fragment B (using replace). After that, I press back button (using popbackstack). the collect SUCCESS triggered.
Codes Related
These codes at the FragmentA.kt inside onViewCreated.
private fun startLifeCycle() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
collectTripDeliver()
}
launch {
collectTripReattempt()
}
}
}
}
These codes when to submit data at a button setOnClickListener.
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.setTripDeliver(
verificationCode,
remark
)
}
Method to collect flow collectTripReattempt()
private suspend fun collectTripReattempt() {
viewModel.tripReattempt.collect {
when (it) {
is Resource.Initialize -> {
}
is Resource.Loading -> {
Log.i("???","collectTripReattempt loading")
handleSaveEarly()
}
is Resource.Success -> {
val error = it.data?.error
if (error == null) {
Tools.showToast(requireContext(), "Success Reattempt")
Log.i("???","collectTripReattempt Success")
} else {
Tools.showToast(requireContext(), "$error")
}
handleSaveEnding()
}
is Resource.Error -> {
handleSaveEnding()
}
}
}
}
Below codes are from ViewModel.
private val _tripDeliver =
MutableStateFlow<Resource<TripDeliverResponse>>(Resource.Initialize())
val tripDeliver: StateFlow<Resource<TripDeliverResponse>> = _tripDeliver
This method to call repository.
suspend fun setTripDeliver(
verificationCode: String?,
remark: String?
) {
_tripDeliver.value = Resource.Loading()
try {
val result = withContext(ioDispatcher) {
val tripDeliverParameter = DeliverParameter(
verificationCode,
remark
)
val response = appRepository.setTripDeliver(tripDeliverParameter)
Resource.getResponse { response }
}
_tripDeliver.value = result
} catch (e: Exception) {
when (e) {
is IOException -> _tripDeliver.value =
Resource.Error(messageInt = R.string.no_internet_connection)
else -> _tripDeliver.value =
Resource.Error("Trip Deliver Error: " + e.message)
}
}
}
Logcat
2021-07-09 19:56:10.946 7446-7446/com.package.app I/???: collectTripReattempt loading
2021-07-09 19:56:11.172 7446-7446/com.package.app I/???: collectTripReattempt Success
2021-07-09 19:56:17.703 7446-7446/com.package.app I/???: collectTripReattempt Success
As you can see, the last Success is called again AFTER I pressed back button (popbackstack)
Question
How to make it trigger once only? Is it the way I implement it is wrong? Thank you in advance.
This is not problem of your implementation this is happening because of stateIn() which use used in your viewModel to convert regular flow into stateFlow
If according to your code snippet the success is triggered once again, then why not loading has triggered?
as per article, it is showing the latest cached value when you left the screen and came back you got the latest cached value on view.
Resource:
https://medium.com/androiddevelopers/migrating-from-livedata-to-kotlins-flow-379292f419fb
The latest value will still be cached so that when the user comes back to it, the view will have some data immediately.
I have found the solution, thanks to #Nurseyit Tursunkulov for giving me a clue. I have to use SharedFlow.
At the ViewModel, I replace the initialize with these:
private val _tripDeliver = MutableSharedFlow<Resource<TripDeliverResponse>>(replay = 0)
val tripDeliver: SharedFlow<Resource<TripDeliverResponse>> = _tripDeliver
At the replay I have to use 0, so this SharedFlow will trigger once. Next, change _tripDeliver.value to _tripDeliver.emit() like the codes below:
fun setTripDeliver(
verificationCode: String?,
remark: String?
) = viewModelScope.launch {
_tripDeliver.emit(Resource.Loading())
if (verificationCode == null && remark == null) {
_tripDeliver.emit(Resource.Error("Remark cannot be empty if verification is empty"))
return#launch
}
try {
val result = withContext(ioDispatcher) {
val tripDeliverParameter = DeliverParameter(
verificationCode,
remark,
)
val response = appRepository.setTripDeliver(tripDeliverParameter)
Resource.getResponse { response }
}
_tripDeliver.emit(result)
} catch (e: Exception) {
when (e) {
is IOException -> _tripDeliver.emit(Resource.Error(messageInt = R.string.no_internet_connection))
else -> _tripDeliver.emit(Resource.Error("Trip Deliver Error: " + e.message))
}
}
}
I hope this answer will help the others also.
I think this is because of coldFlow, you need a HotFlow. Another option is to try to hide and show fragment, instead of replacing. And yet another solution is to keep this code in viewModel.
In my opinion, I think your way of using coroutines in lifeScope is incorrect. After the lifeScope status of FragmentA is at Started again, the coroutine will be restarted:
launch {
collectTripDeliver()
}
launch {
collectTripReattempt()
}
So I think: You need to modify this way:
private fun startLifeCycle() {
viewLifecycleOwner.lifecycleScope.launch {
launch {
collectTripDeliver()
}
launch {
collectTripReattempt()
}
}
}

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
}

Coroutines with sealed class

My project has a lot of operations that must be performed one after another. I was using listeners, but I found this tutorial Kotlin coroutines on Android and I wanted to change my sever call with better readable code. But I think I am missing something. The below code always return an error from getTime1() function:
suspend fun getTimeFromServer1() :ResultServer<Long> {
val userId = SharedPrefsHelper.getClientId()
return withContext(Dispatchers.IO) {
val call: Call<ResponseFromServer>? = userId?.let { apiInterface.getTime(it) }
(call?.execute()?.body())?.run {
val time:Long? = this.data?.time
time?.let {
Timber.tag("xxx").e("time received it ${it}")// I am getting the right result here
ResultServer.Success(it)
}
Timber.tag("xxx").e("time received ${time}")
}
ResultServer.Error(Exception("Cannot get time"))
}
}
fun getTime1() {
GlobalScope.launch {
when (val expr: ResultServer<Long> = NetworkLayer.getTimeFromServer1()) {
is ResultServer.Success<Long> -> Timber.tag("xxx").e("time is ${expr.data}")
is ResultServer.Error -> Timber.tag("xxx").e("time Error") //I am always get here
}}
}
}
But if I am using listeners (getTime()) everything works perfectly:
suspend fun getTimeFromServer(savingFinishedListener: SavingFinishedListener<Long>) {
val userId = SharedPrefsHelper.getClientId()
withContext(Dispatchers.IO) {
val call: Call<ResponseFromServer>? = userId?.let { apiInterface.getTime(it) }
(call?.execute()?.body())?.run {
val time:Long? = this.data?.time
time?.let {
Timber.tag("xxx").e("time received it ${it}")
savingFinishedListener.onSuccess(it)
}
}
savingFinishedListener.onSuccess(null)
}
}
fun getTime() {
GlobalScope.launch {
NetworkLayer.getTimeFromServer(object:SavingFinishedListener<Long>{
override fun onSuccess(t: Long?) {
t?.let {
Timber.tag("xxx").e("time here $it") //I am getting the right result
}
}
})
}
}
Thanks in advance for any help.
The last line of a lambda is implicitly the return value of that lambda. Since you don't have any explicit return statements in your withContext lambda, its last line:
ResultServer.Error(Exception("Cannot get time"))
means that it always returns this Error. You can put return#withContext right before your ResultServer.Success(it) to make that line of code also return from the lambda.
Side note: don't use GlobalScope.

How to perform call sequence to a REST API in Android App?

I'm having a hard time making a call to my api. I'm using Reactivex with kotlin and Flowables. My API returns a list of items if the date I passed by the "If-Modified_since" header is less than the last update.
If there is no update I get as an app return android app a 304 error.
I need to do the following procedure.
1-> I make a call to the api
2-> If the call is successful, save the list in Realm and return to the viewmodel
3-> If the error is 304, I perform a cache search (Realm) of the items
4-> If it is another error, I return the error normally for the ViewModel
Here is the code below, but I'm not sure if it's that way.
override fun getTickets(eventId: String): Flowable<List<Ticket>> {
return factory
.retrieveRemoteDataStore()
.getTickets(eventId)
.map {
saveTickets(it)
it
}.onErrorResumeNext { t: Throwable ->
if (t is HttpException && t.response().code() == 304) {
factory.retrieveCacheDataStore().getTickets(eventId)
} else
//Should return error
}
The question is, what is the best way to do this?
Thank you.
I'm going to assume, that you're using Retrofit. If that's the case, then you could wrap your getTickets call in Single<Response<SomeModel>>. This way, on first map you can check the errorcode, something among the lines of:
...getTickets(id)
.map{ response ->
when {
response.isSuccessful && response.body!=null -> {
saveTickets(it)
it
}
!response.isSuccessful && response.errorCode() == 304 -> {
factory.retrieveCacheDataStore().getTickets(eventId)
}
else -> throw IOException()
}
}
This could of course be made pretty using standard/extension functions but wanted to keep it simple for readability purposes.
Hope this helps!
Most of my comments are my explanations.
data class Ticket(val id:Int) {
companion object {
fun toListFrom(jsonObject: JSONObject): TICKETS {
/**do your parsing of data transformation here */
return emptyList()
}
}
}
typealias TICKETS = List<Ticket>
class ExampleViewModel(): ViewModel() {
private var error: BehaviorSubject<Throwable> = BehaviorSubject.create()
private var tickets: BehaviorSubject<TICKETS> = BehaviorSubject.create()
/**public interfaces that your activity or fragment talk to*/
fun error(): Observable<Throwable> = this.error
fun tickets(): Observable<TICKETS> = this.tickets
fun start() {
fetch("http://api.something.com/v1/tickets/")
.subscribeOn(Schedulers.io())
.onErrorResumeNext { t: Throwable ->
if (t.message == "304") {
get(3)
} else {
this.error.onNext(t)
/** this makes the chain completed gracefuly without executing flatMap or any other operations*/
Observable.empty()
}
}
.flatMap(this::insertToRealm)
.subscribe(this.tickets)
}
private fun insertToRealm(tickets: TICKETS) : Observable<TICKETS> {
/**any logic here is mainly to help you save into Realm**/
/** I think realm has the option to ignore items that are already in the db*/
return Observable.empty()
}
private fun get(id: Int): Observable<TICKETS> {
/**any logic here is mainly to help you fetch from your cache**/
return Observable.empty()
}
private fun fetch(apiRoute: String): Observable<TICKETS> {
/**
* boilerplate code
wether you're using Retrofit or Okhttp, that's the logic you
should try to have
* */
val status: Int = 0
val rawResponse = ""
val error: Throwable? = null
val jsonResponse = JSONObject(rawResponse)
return Observable.defer {
if (status == 200) {
Observable.just(Ticket.toListFrom(jsonResponse))
}
else if (status == 304) {
Observable.error<TICKETS>(Throwable("304"))
}
else {
Observable.error<TICKETS>(error)
}
}
}
override fun onCleared() {
super.onCleared()
this.error = BehaviorSubject.create()
this.tickets = BehaviorSubject.create()
}
}

Categories

Resources