How to handle error states with LiveData? - android

The new LiveData can be used as a replacement for RxJava's observables in some scenarios. However, unlike Observable, LiveData has no callback for errors.
My question is: How should I handle errors in LiveData, e.g. when it's backed by some network resource that can fail to be retrieved due to an IOException?

In one of Google's sample apps for Android Architecture Components they wrap the LiveData emitted object in a class that can contain a status, data, and message for the emitted object.
https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.kt
With this approach you can use the status to determine if there was an error.

You can extend from MutableLiveData and create a holder Model to wrap your data.
This is your Wrapper Model
public class StateData<T> {
#NonNull
private DataStatus status;
#Nullable
private T data;
#Nullable
private Throwable error;
public StateData() {
this.status = DataStatus.CREATED;
this.data = null;
this.error = null;
}
public StateData<T> loading() {
this.status = DataStatus.LOADING;
this.data = null;
this.error = null;
return this;
}
public StateData<T> success(#NonNull T data) {
this.status = DataStatus.SUCCESS;
this.data = data;
this.error = null;
return this;
}
public StateData<T> error(#NonNull Throwable error) {
this.status = DataStatus.ERROR;
this.data = null;
this.error = error;
return this;
}
public StateData<T> complete() {
this.status = DataStatus.COMPLETE;
return this;
}
#NonNull
public DataStatus getStatus() {
return status;
}
#Nullable
public T getData() {
return data;
}
#Nullable
public Throwable getError() {
return error;
}
public enum DataStatus {
CREATED,
SUCCESS,
ERROR,
LOADING,
COMPLETE
}
}
This is your extended LiveData Object
public class StateLiveData<T> extends MutableLiveData<StateData<T>> {
/**
* Use this to put the Data on a LOADING Status
*/
public void postLoading() {
postValue(new StateData<T>().loading());
}
/**
* Use this to put the Data on a ERROR DataStatus
* #param throwable the error to be handled
*/
public void postError(Throwable throwable) {
postValue(new StateData<T>().error(throwable));
}
/**
* Use this to put the Data on a SUCCESS DataStatus
* #param data
*/
public void postSuccess(T data) {
postValue(new StateData<T>().success(data));
}
/**
* Use this to put the Data on a COMPLETE DataStatus
*/
public void postComplete() {
postValue(new StateData<T>().complete());
}
}
And this is how you use it
StateLiveData<List<Book>> bookListLiveData;
bookListLiveData.postLoading();
bookListLiveData.postSuccess(books);
bookListLiveData.postError(e);
And how it can be observed:
private void observeBooks() {
viewModel.getBookList().observe(this, this::handleBooks);
}
private void handleBooks(#NonNull StateData<List<Book>> books) {
switch (books.getStatus()) {
case SUCCESS:
List<Book> bookList = books.getData();
//TODO: Do something with your book data
break;
case ERROR:
Throwable e = books.getError();
//TODO: Do something with your error
break;
case LOADING:
//TODO: Do Loading stuff
break;
case COMPLETE:
//TODO: Do complete stuff if necessary
break;
}
}

Wrap the Data that you return from LiveData with some sort of error Messaging
public class DataWrapper<T>T{
private T data;
private ErrorObject error; //or A message String, Or whatever
}
//Now in your LifecycleRegistryOwner Class
LiveData<DataWrapper<SomeObjectClass>> result = modelView.getResult();
result.observe(this, newData ->{
if(newData.error != null){ //Can also have a Status Enum
//Handle Error
}
else{
//Handle data
}
});
Just Catch an Exception instead or throwing it. use the error Object to pass this Data to the UI.
MutableLiveData<DataWrapper<SomObject>> liveData = new...;
//On Exception catching:
liveData.set(new DataWrapper(null, new ErrorObject(e));

Another approach is to use MediatorLiveData that will take sources of LiveData of different type. This will give you separation of each event:
For example:
open class BaseViewModel : ViewModel() {
private val errorLiveData: MutableLiveData<Throwable> = MutableLiveData()
private val loadingStateLiveData: MutableLiveData<Int> = MutableLiveData()
lateinit var errorObserver: Observer<Throwable>
lateinit var loadingObserver: Observer<Int>
fun <T> fromPublisher(publisher: Publisher<T>): MediatorLiveData<T> {
val mainLiveData = MediatorLiveData<T>()
mainLiveData.addSource(errorLiveData, errorObserver)
mainLiveData.addSource(loadingStateLiveData, loadingObserver)
publisher.subscribe(object : Subscriber<T> {
override fun onSubscribe(s: Subscription) {
s.request(java.lang.Long.MAX_VALUE)
loadingStateLiveData.postValue(LoadingState.LOADING)
}
override fun onNext(t: T) {
mainLiveData.postValue(t)
}
override fun onError(t: Throwable) {
errorLiveData.postValue(t)
}
override fun onComplete() {
loadingStateLiveData.postValue(LoadingState.NOT_LOADING)
}
})
return mainLiveData
}
}
In this example loading and error LiveData will start being observed once the MediatorLiveData will have active observers.

In my app, I had to translate RxJava Observables into LiveData. While doing that, I of course had to maintain the error state. Here's how I did it (Kotlin)
class LiveDataResult<T>(val data: T?, val error: Throwable?)
class LiveObservableData<T>(private val observable: Observable<T>) : LiveData<LiveDataResult<T>>() {
private var disposable = CompositeDisposable()
override fun onActive() {
super.onActive()
disposable.add(observable.subscribe({
postValue(LiveDataResult(it, null))
}, {
postValue(LiveDataResult(null, it))
}))
}
override fun onInactive() {
super.onInactive()
disposable.clear()
}
}

Just some implementation of the method from Chris Cook's answer:
At first, we need the object that will contain response data and exceptions:
/**
* A generic class that holds a value with its loading status.
*
* #see Sample apps for Android Architecture Components
*/
data class Resource<out T>(val status: Status, val data: T?, val exception: Throwable?) {
enum class Status {
LOADING,
SUCCESS,
ERROR,
}
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(exception: Throwable): Resource<T> {
return Resource(Status.ERROR, null, exception)
}
fun <T> loading(): Resource<T> {
return Resource(Status.LOADING, null, null)
}
}
}
And then my own invention - AsyncExecutor.
This small class do 3 important things:
Return standard convenient LiveData object.
Call provided callback asynchronously.
Takes the result of the callback or catch any exception and put it to the LiveData.
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
class AsyncExecutor {
companion object {
fun <T> run(callback: () -> T): LiveData<Resource<T>> {
val resourceData: MutableLiveData<Resource<T>> = MutableLiveData()
Thread(Runnable {
try {
resourceData.postValue(Resource.loading())
val callResult: T = callback()
resourceData.postValue(Resource.success(callResult))
} catch (e: Throwable) {
resourceData.postValue(Resource.error(e))
}
}).start()
return resourceData
}
}
}
Then you can create a LiveData in your ViewModel, contains the result of your callback or exception:
class GalleryViewModel : ViewModel() {
val myData: LiveData<Resource<MyData>>
init {
myData = AsyncExecutor.run {
// here you can do your synchronous operation and just throw any exceptions
return MyData()
}
}
}
And then you can get your data and any exceptions in the UI:
class GalleryFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
galleryViewModel = ViewModelProviders.of(this).get(GalleryViewModel::class.java)
// ...
// Subscribe to the data:
galleryViewModel.myData.observe(viewLifecycleOwner, Observer {
when {
it.status === Resource.Status.LOADING -> {
println("Data is loading...")
}
it.status === Resource.Status.ERROR -> {
it.exception!!.printStackTrace()
}
it.status === Resource.Status.SUCCESS -> {
println("Data has been received: " + it.data!!.someField)
}
}
})
return root
}
}

I have built a movie search app here in which I have used to different LiveData objects, one for the successful response from the network and one for the unsuccessful:
private val resultListObservable = MutableLiveData<List<String>>()
private val resultListErrorObservable = MutableLiveData<HttpException>()
fun findAddress(address: String) {
mainModel.fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<MainModel.ResultEntity>?>() {
override fun onSuccess(t: List<MainModel.ResultEntity>) {
entityList = t
resultListObservable.postValue(fetchItemTextFrom(t))
}
override fun onError(e: Throwable) {
resultListErrorObservable.postValue(e as HttpException)
}
})
}

Related

ClassCastException in NetworkBoundResource<ResultType, RequestType>

I am following the "best practices"(?) from the GitHub sample: https://github.com/android/architecture-components-samples/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/NetworkBoundResource.kt
I am getting an error
java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.xxx.yyy.data.model.TestUser
I've tried using Gson to convert to JSON and back to the Generic RequestType, but that doesn't work either. The data is coming back just fine. If I cast the response in the NetworkBoundResource, then it works - but that kind of defeats the purpose of generics. I am also using the aws-android-sdk to invoke lambda, which makes things a HUGE pain, but that's AWS for ya
TestUser
public class TestUser {
private String username;
public TestUser() {
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
NetworkBoundResource
abstract class NetworkBoundResource<ResultType, RequestType>
#MainThread constructor(private val appExecutors: AppExecutors) {
private val result = MediatorLiveData<Resource<ResultType>>()
init {
result.value = Resource.loading(null)
val dbSource = loadFromDb()
result.addSource(dbSource) { data ->
result.removeSource(dbSource)
if (shouldFetch(data)) {
Log.e("NetworkBoundResource", "fetching from network")
appExecutors.networkIO().execute {
fetchFromNetwork(dbSource)
}
} else {
Log.e("NetworkBoundResource", "not fetching from network")
result.addSource(dbSource) { newData ->
setValue(Resource.success(newData))
}
}
}
}
#MainThread
private fun setValue(newValue: Resource<ResultType>) {
if (result.value != newValue) {
result.value = newValue
}
}
private fun fetchFromNetwork(dbSource: LiveData<ResultType>) {
val apiResponse = MutableLiveData<ApiResponse<RequestType>>()
// This is super dumb, but can't createCall() on the main thread 🤦
var lambdaResponse: LambdaResponse<RequestType>? = null
var exception: Exception? = null
try {
lambdaResponse = createCall()
} catch (e: Exception) {
exception = e
}
appExecutors.mainThread().execute {
// Can't setValue on a background thread 🤦
if (exception != null) {
apiResponse.value = ApiResponse.create(exception)
} else if (lambdaResponse != null) {
apiResponse.value = ApiResponse.create(lambdaResponse)
}
// we re-attach dbSource as a new source, it will dispatch its latest value quickly
result.addSource(dbSource) { newData ->
setValue(Resource.loading(newData))
}
result.addSource(apiResponse) { response ->
result.removeSource(apiResponse)
result.removeSource(dbSource)
when (response) {
is ApiSuccessResponse -> {
appExecutors.diskIO().execute {
val x = processResponse(response)
// FAILING HERE
saveCallResult(x)
appExecutors.mainThread().execute {
// we specially request a new live data,
// otherwise we will get immediately last cached value,
// which may not be updated with latest results received from network.
result.addSource(loadFromDb()) { newData ->
setValue(Resource.success(newData))
}
}
}
}
is ApiEmptyResponse -> {
appExecutors.mainThread().execute {
// reload from disk whatever we had
result.addSource(loadFromDb()) { newData ->
setValue(Resource.success(newData))
}
}
}
is ApiErrorResponse -> {
onFetchFailed()
result.addSource(dbSource) { newData ->
setValue(Resource.error(response.errorMessage, newData))
}
}
}
}
}
}
protected open fun onFetchFailed() {
Log.e("NetworkBoundResource", "onFetchFailed")
}
fun asLiveData() = result as LiveData<Resource<ResultType>>
#WorkerThread
protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body
#WorkerThread
protected abstract fun saveCallResult(item: RequestType)
#MainThread
protected abstract fun shouldFetch(data: ResultType?): Boolean
#MainThread
protected abstract fun loadFromDb(): LiveData<ResultType>
#MainThread
protected abstract fun createCall(): LambdaResponse<RequestType>
}
LoginDataSource
fun auth(authRequest: AuthRequest): LiveData<Resource<User>> {
return object : NetworkBoundResource<User, TestUser>(appExecutors) {
override fun saveCallResult(item: TestUser) {
Log.e("username", item.username)
// Log.e("saveCallResult", "saving user to db: ${item.federatedIdentity}")
// userDao.insertAll(item)
}
override fun shouldFetch(data: User?): Boolean {
val fetch = data == null
Log.e("shouldFetch", "$fetch")
return true
}
override fun loadFromDb(): LiveData<User> {
Log.e("loadFromDb", "findById: ${authRequest.federatedIdentity}")
return userDao.findById(authRequest.federatedIdentity)
}
override fun createCall(): LambdaResponse<TestUser> {
Log.e("createCall", "authenticating user: ${authRequest.federatedIdentity}")
return authApiService.auth(authRequest)
}
}.asLiveData()
}

Making a generic network adapter using livedata, retrofit, mvvm and repository pattern

I am new to android architecture components and I am trying to use LiveData and ViewModels with mvvm, repository pattern and retrofit. Referred to GitHubSample google gave in its architecture guide but want to simplify it little bit for my needs. Below is the code which I had so far but having below problems in completing it.
onActive() method in LiveDataCallAdapter is not invoking at all
Not able to figure out how I can get the response as a LiveData(I get this as null always) in SettingsData class? Ideally here I just want to have success and failure listener and I should get the data inside these blocks. All the generic network errors should already be handled before coming to this class. I am not able to figure out how to do this.
3.I do not want to call.enqueue in this SettingsData class which many examples shows
Any help is greatly appreciated. Thanks in advance
//Activity
private fun loadApplicationSettings() {
val settingsViewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java)
settingsViewModel.userApplicationSettings.observe(this, Observer<UserApplicationSettings> { userApplicationSettingsResult ->
Log.d("UserApplicationSettings", userApplicationSettingsResult.toString())
userSettingsTextView.text = userApplicationSettingsResult.isPushNotificationEnabled
})
}
//ViewModel
class SettingsViewModel : ViewModel() {
private var settingsRepository: SettingsRepository
lateinit var userApplicationSettings: LiveData<UserApplicationSettings>
init {
settingsRepository = SettingsRepository()
loadUserApplicationSettings()
}
private fun loadUserApplicationSettings() {
userApplicationSettings = settingsRepository.loadUserApplicationSettings()
}
}
//Repository
class SettingsRepository {
val settingsService = SettingsData()
fun loadUserApplicationSettings(): LiveData<UserApplicationSettings> {
return settingsService.getUserApplicationSettings()
}
}
//I do not want to do the network calls in repository, so created a seperate class gets the data from network call
class SettingsData {
val apiBaseProvider = ApiBaseProvider()
fun getUserApplicationSettings(): MutableLiveData<UserApplicationSettings> {
val userApplicationSettingsNetworkCall = apiBaseProvider.create().getApplicationSettings()
//Not sure how to get the data from userApplicationSettingsNetworkCall and convert it to livedata to give to repository
// deally here I just want to have success and failure listener and I should get the data inside these blocks. All the generic network errors should already be handled before coming to this class. I am not able to figure out how to do this.
val userApplicationSettingsData: LiveData<ApiResponse<UserApplicationSettings>> = userApplicationSettingsNetworkCall
//Thinking of having a success and fail block here and create a LiveData object to give to repository. Not sure how to do this
return userApplicationSettingsData
}
}
//Settings Service for retrofit
interface SettingsService {
#GET("url")
fun getApplicationSettings(): LiveData<ApiResponse<UserApplicationSettings>>
}
//Base provider of retrofit
class ApiBaseProvider {
fun create(): SettingsService {
val gson = GsonBuilder().setLenient().create()
val okHttpClient = createOkHttpClient()
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(LiveDataCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create(gson))
.baseUrl("url")
.build()
return retrofit.create(SettingsService::class.java)
}
}
//
class LiveDataCallAdapterFactory : Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (getRawType(returnType) != LiveData::class.java) {
return null
}
val observableType = getParameterUpperBound(0, returnType as ParameterizedType)
val rawObservableType = getRawType(observableType)
if (rawObservableType != ApiResponse::class.java) {
throw IllegalArgumentException("type must be a resource")
}
if (observableType !is ParameterizedType) {
throw IllegalArgumentException("resource must be parameterized")
}
val bodyType = getParameterUpperBound(0, observableType)
return LiveDataCallAdapter<Any>(bodyType)
}
}
//Custom adapter that does the network call
class LiveDataCallAdapter<T>(private val responseType: Type) : CallAdapter<T, LiveData<ApiResponse<T>>> {
override fun responseType(): Type {
return responseType
}
override fun adapt(call: Call<T>): LiveData<ApiResponse<T>> {
return object : LiveData<ApiResponse<T>>() {
override fun onActive() {
super.onActive()
call.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
println("testing response: " + response.body())
postValue(ApiResponse.create(response))
}
override fun onFailure(call: Call<T>, throwable: Throwable) {
postValue(ApiResponse.create(throwable))
}
})
}
}
}
}
//I want to make this class as a generic class to do all the network success and error handling and then pass the final response back
/**
* Common class used by API responses.
* #param <T> the type of the response object
</T> */
sealed class ApiResponse<T> {
companion object {
fun <T> create(error: Throwable): ApiErrorResponse<T> {
return ApiErrorResponse(error.message ?: "unknown error")
}
fun <T> create(response: Response<T>): ApiResponse<T> {
println("testing api response in create")
return if (response.isSuccessful) {
val body = response.body()
if (body == null || response.code() == 204) {
ApiEmptyResponse()
} else {
ApiSuccessResponse(
body = body
)
}
} else {
val msg = response.errorBody()?.string()
val errorMsg = if (msg.isNullOrEmpty()) {
response.message()
} else {
msg
}
ApiErrorResponse(errorMsg ?: "unknown error")
}
}
}
}
/**
* separate class for HTTP 204 responses so that we can make ApiSuccessResponse's body non-null.
*/
class ApiEmptyResponse<T> : ApiResponse<T>()
data class ApiErrorResponse<T>(val errorMessage: String) : ApiResponse<T>()
data class ApiSuccessResponse<T>(
val body: T
) : ApiResponse<T>() {
}
We can connect Activity/Fragment and ViewModel as below:
Firstly, we have to create our ApiResource which will handle the retrofit response.
public class ApiResource<T> {
#NonNull
private final Status status;
#Nullable
private final T data;
#Nullable
private final ErrorResponse errorResponse;
#Nullable
private final String errorMessage;
private ApiResource(Status status, #Nullable T data, #Nullable ErrorResponse errorResponse, #Nullable String errorMessage) {
this.status = status;
this.data = data;
this.errorResponse = errorResponse;
this.errorMessage = errorMessage;
}
public static <T> ApiResource<T> create(Response<T> response) {
if (!response.isSuccessful()) {
try {
JSONObject jsonObject = new JSONObject(response.errorBody().string());
ErrorResponse errorResponse = new Gson()
.fromJson(jsonObject.toString(), ErrorResponse.class);
return new ApiResource<>(Status.ERROR, null, errorResponse, "Something went wrong.");
} catch (IOException | JSONException e) {
return new ApiResource<>(Status.ERROR, null, null, "Response Unreachable");
}
}
return new ApiResource<>(Status.SUCCESS, response.body(), null, null);
}
public static <T> ApiResource<T> failure(String error) {
return new ApiResource<>(Status.ERROR, null, null, error);
}
public static <T> ApiResource<T> loading() {
return new ApiResource<>(Status.LOADING, null, null, null);
}
#NonNull
public Status getStatus() {
return status;
}
#Nullable
public T getData() {
return data;
}
#Nullable
public ErrorResponse getErrorResponse() {
return errorResponse;
}
#Nullable
public String getErrorMessage() {
return errorMessage;
}
}
The Status is just an Enum class as below:
public enum Status {
SUCCESS, ERROR, LOADING
}
The ErrorResponse class must be created in such a way that the getter and setter can handle the error.
RetrofitLiveData class
public class RetrofitLiveData<T> extends LiveData<ApiResource<T>> {
private Call<T> call;
public RetrofitLiveData(Call<T> call) {
this.call = call;
setValue(ApiResource.loading());
}
Callback<T> callback = new Callback<T>() {
#Override
public void onResponse(Call<T> call, Response<T> response) {
setValue(ApiResource.create(response));
}
#Override
public void onFailure(Call<T> call, Throwable t) {
setValue(ApiResource.failure(t.getMessage()));
}
};
#Override
protected void onActive() {
super.onActive();
call.enqueue(callback);
}
#Override
protected void onInactive() {
super.onInactive();
if (!hasActiveObservers()) {
if (!call.isCanceled()) {
call.cancel();
}
}
}
}
Repository class
public class Repository {
public LiveData<ApiResource<JunoBalanceResponse>> getJunoBalanceResponse(Map<String, String> headers) {
return new RetrofitLiveData<>(ApiClient.getJunoApi(ApiClient.BASE_URL.BASE).getJunoBalance(headers));
}
}
JunoBalanceResponse contains the objects and its getters and setters that I am waiting as a response of my retrofit request.
below is an example for the api interface.
public interface JunoApi {
#Headers({"X-API-Version: 2"})
#GET("balance")
Call<JunoBalanceResponse> getJunoBalance(#HeaderMap Map<String, String> headers);
}
ApiClient class
public class ApiClient {
public enum BASE_URL {
AUTH, BASE
}
private static Retrofit retrofit;
private static final String JUNO_SANDBOX_AUTH_URL = "https://sandbox.boletobancario.com/authorization-server/";
private static final String JUNO_SANDBOX_BASE_URL = "https://sandbox.boletobancario.com/api-integration/";
private static Retrofit getRetrofit(String baseUrl) {
OkHttpClient okHttpClient = new OkHttpClient().newBuilder()
.connectTimeout(90, TimeUnit.SECONDS)
.readTimeout(90, TimeUnit.SECONDS)
.writeTimeout(90, TimeUnit.SECONDS)
.build();
retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build();
return retrofit;
}
public static JunoApi getJunoApi(BASE_URL targetPath) {
switch (targetPath) {
case AUTH: return getRetrofit(JUNO_SANDBOX_AUTH_URL).create(JunoApi.class);
case BASE: return getRetrofit(JUNO_SANDBOX_BASE_URL).create(JunoApi.class);
default: return getRetrofit(JUNO_SANDBOX_BASE_URL).create(JunoApi.class);
}
}
}
Now we can connect our Repository and ApiViewModel.
public class ApiViewModel extends ViewModel {
private Repository repository = new Repository();
public LiveData<ApiResource<JunoBalanceResponse>> getJunoBalanceResponse(Map<String, String> headers) {
return repository.getJunoBalanceResponse(headers);
}
}
And finally, we can observe the retrofit response in our Activity/Fragment
apiViewModel = ViewModelProviders.of(requireActivity()).get(ApiViewModel.class);
apiViewModel.getJunoBalanceResponse(headers).observe(getViewLifecycleOwner(), new Observer<ApiResource<JunoBalanceResponse>>() {
#Override
public void onChanged(ApiResource<JunoBalanceResponse> response) {
switch (response.getStatus()) {
case LOADING:
Log.i(TAG, "onChanged: BALANCE LOADING");
break;
case SUCCESS:
Log.i(TAG, "onChanged: BALANCE SUCCESS");
break;
case ERROR:
Log.i(TAG, "onChanged: BALANCE ERROR");
break;
}
}
});

How to show ProgressDialog when fetching data from ViewModel

I want to show ProgressDialog while fetching data from ViewModel and it works fine when I fetch data for the first time, but when I want to refresh the data from API the ProgressDialog starts and does not stops
I create MutableLiveData<Boolean>() and try to manage the visibility but it's not working
This is how i refresh my data from my Activity
private fun loadNorthTram() {
val model =
ViewModelProviders.of(this#MainActivity).get(MyViewModelNorth::class.java)
model.isNorthUpdating.observe(
this#MainActivity,
Observer { b ->
if (b!!)
AppUtil.showProgressSpinner(this#MainActivity)
else
AppUtil.dismissProgressDialog()
})
model.getNorthTrams().observe(this#MainActivity, Observer
{
if (it != null) {
setData(it)
}
})
}
Below is my ViewModel class
class MyViewModelNorth : ViewModel() {
private lateinit var mtoken: String
private val apiService: ApiInterface = ApiClient.client.create(ApiInterface::class.java)
private lateinit var trams: MutableLiveData<TramModel>
val isNorthUpdating = MutableLiveData<Boolean>().default(false)
fun getNorthTrams(): MutableLiveData<TramModel> {
isNorthUpdating.value = true
if (!::trams.isInitialized) {
trams = MutableLiveData()
callTokenAPI()
}
return trams
}
private fun callTokenAPI() {
val tokenObservable: Observable<TokenModel> = apiService.fetchToken()
tokenObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { self ->
mtoken = self.responseObject[0].DeviceToken
callTramAPI()
}
.subscribe(getTokenObserver())
}
private fun callTramAPI() {
val apiService: ApiInterface = ApiClient.client.create(ApiInterface::class.java)
val observable: Observable<TramModel> = apiService.fetchTrams(AppUtil.NORTH_TRAMS, mtoken)
observable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getTramObserver())
}
private fun getTokenObserver(): Observer<TokenModel> {
return object : Observer<TokenModel> {
override fun onComplete() {}
override fun onSubscribe(d: Disposable) {}
override fun onNext(tokenModel: TokenModel) {}
override fun onError(e: Throwable) {
if (e is HttpException) {
val errorBody = e.response().errorBody()
HttpErrorUtil(e.code()).handelError()
}
}
}
}
private fun getTramObserver(): Observer<TramModel> {
return object : Observer<TramModel> {
override fun onComplete() {
isNorthUpdating.value = false
}
override fun onSubscribe(d: Disposable) {}
override fun onNext(t: TramModel) {
if (!t.hasError && t.hasResponse)
trams.value = t
else if (t.errorMessage.isBlank())
applicationContext().showToast(t.errorMessage)
else
applicationContext().showToast(applicationContext().getString(R.string.something_wrong))
}
override fun onError(e: Throwable) {
if (e is HttpException) {
val errorBody = e.response().errorBody()
HttpErrorUtil(e.code()).handelError()
}
}
}
}
public fun getIsNothUpdating(): LiveData<Boolean> {
return isNorthUpdating
}
fun <T : Any?> MutableLiveData<T>.default(initialValue: T) = apply { setValue(initialValue) }
}
I have not tested your code but I think your problem is in function getNorthTrams() in viewmodel.
First time when you fetch data, trams is not initialized, your api call is happening and at only onCompleted, you are setting isNorthUpdating.value = false. This code works.
But when you refresh data, trams is already initialized. So, there is no case for isNorthUpdating.value = false, which is causing progress dialog to not dismiss.
So I think you should handle else case in your viewmodel.
fun getNorthTrams(): MutableLiveData<TramModel> {
isNorthUpdating.value = true
if (!::trams.isInitialized) {
trams = MutableLiveData()
callTokenAPI()
}else{
//do your thing for refresh
isNorthUpdating.value = false
}
return trams
}
Also, in api call, if error occur, you should set isNorthUpdating to false and show some error message. Otherwise, progress dialog will always be showing even if some error occur in api call.

NetworkBoundResource helper class without Room

When I tried to implements the NetworkBoundResource and Resource helper class for the Room Db and Retrofit, it works perfect. However, I need to implement the Search Result from RESTful using Retrofit only without Room. The Resources class is good and I dont need to change it. What I want to do is try to remove db source inside this class.
public abstract class NetworkBoundResource<ResultType, RequestType> {
private final AppExecutors appExecutors;
private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();
#MainThread
public NetworkBoundResource(AppExecutors appExecutors) {
this.appExecutors = appExecutors;
result.setValue(Resource.loading(null));
LiveData<ResultType> dbSource = loadFromDb();
result.addSource(dbSource, data -> {
result.removeSource(dbSource);
if (shouldFetch(data)) {
fetchFromNetwork(dbSource);
} else {
result.addSource(dbSource, newData -> setValue(Resource.success(newData)));
}
});
}
#MainThread
private void setValue(Resource<ResultType> newValue) {
if (!Objects.equals(result.getValue(), newValue)) {
result.setValue(newValue);
}
}
private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
LiveData<ApiResponse<RequestType>> apiResponse = createCall();
// we re-attach dbSource as a new source, it will dispatch its latest value quickly
result.addSource(dbSource, newData -> setValue(Resource.loading(newData)));
result.addSource(apiResponse, response -> {
result.removeSource(apiResponse);
result.removeSource(dbSource);
//noinspection ConstantConditions
if (response.isSuccessful()) {
appExecutors.diskIO().execute(() -> {
saveCallResult(processResponse(response));
appExecutors.mainThread().execute(() ->
// we specially request a new live data,
// otherwise we will get immediately last cached value,
// which may not be updated with latest results received from network.
result.addSource(loadFromDb(),
newData -> setValue(Resource.success(newData)))
);
});
} else {
onFetchFailed();
result.addSource(dbSource,
newData -> setValue(Resource.error(response.errorMessage, newData)));
}
});
}
protected void onFetchFailed() {
}
public LiveData<Resource<ResultType>> asLiveData() {
return result;
}
#WorkerThread
protected RequestType processResponse(ApiResponse<RequestType> response) {
return response.body;
}
#WorkerThread
protected abstract void saveCallResult(#NonNull RequestType item);
#MainThread
protected abstract boolean shouldFetch(#Nullable ResultType data);
#NonNull
#MainThread
protected abstract LiveData<ResultType> loadFromDb();
#NonNull
#MainThread
protected abstract LiveData<ApiResponse<RequestType>> createCall();
}
The problem is that any loaded data have to go through the database first, then loading it from the database to the UI, as NetworkBoundResource does. Consequently, What I did is to decouple the persistent database and create a temporary field to load from.
For example if I wanted to edit the original search method, I would suggest:
public LiveData<Resource<List<Repo>>> search(String query) {
return new NetworkBoundResource<List<Repo>, RepoSearchResponse>(appExecutors) {
// Temp ResultType
private List<Repo> resultsDb;
#Override
protected void saveCallResult(#NonNull RepoSearchResponse item) {
// if you don't care about order
resultsDb = item.getItems();
}
#Override
protected boolean shouldFetch(#Nullable List<Repo> data) {
// always fetch.
return true;
}
#NonNull
#Override
protected LiveData<List<Repo>> loadFromDb() {
if (resultsDb == null) {
return AbsentLiveData.create();
}else {
return new LiveData<List<Repo>>() {
#Override
protected void onActive() {
super.onActive();
setValue(resultsDb);
}
};
}
}
#NonNull
#Override
protected LiveData<ApiResponse<RepoSearchResponse>> createCall() {
return githubService.searchRepos(query);
}
#Override
protected RepoSearchResponse processResponse(ApiResponse<RepoSearchResponse> response) {
RepoSearchResponse body = response.body;
if (body != null) {
body.setNextPage(response.getNextPage());
}
return body;
}
}.asLiveData();
}
I ran it and it works.
Edit:
I made another simpler class to handle that (There is another answer here by Daniel Wilson has more feature and is updated).
However, this class has no dependencies and is converted to the basics to make fetch response only:
abstract class NetworkBoundResource<RequestType> {
private val result = MediatorLiveData<Resource<RequestType>>()
init {
setValue(Resource.loading(null))
fetchFromNetwork()
}
#MainThread
private fun setValue(newValue: Resource<RequestType>) {
if (result.value != newValue) {
result.value = newValue
}
}
private fun fetchFromNetwork() {
val apiResponse = createCall()
result.addSource(apiResponse) { response ->
result.removeSource(apiResponse)
when (response) {
is ApiSuccessResponse -> {
setValue(Resource.success(processResponse(response)))
}
is ApiErrorResponse -> {
onFetchFailed()
setValue(Resource.error(response.errorMessage, null))
}
}
}
}
protected fun onFetchFailed() {
}
fun asLiveData() = result as LiveData<Resource<RequestType>>
#WorkerThread
protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body
#MainThread
protected abstract fun createCall(): LiveData<ApiResponse<RequestType>>
}
So when using it, only one method could be implemented createCall():
fun login(email: String, password: String) = object : NetworkBoundResource<Envelope<User>>() {
override fun createCall() = api.login(email, password)
}.asLiveData()
Here is my attempt after a long while!
abstract class NetworkOnlyResource<ResultType, RequestType>
#MainThread constructor(private val appExecutors: AppExecutors) {
private val result = MediatorLiveData<Resource<ResultType>>() //List<Repo>
private val request = MediatorLiveData<Resource<RequestType>>() //RepoSearchResponse
init {
result.value = Resource.loading(null)
fetchFromNetwork()
}
#MainThread
private fun setResultValue(newValue: Resource<ResultType>) {
if (result.value != newValue) {
result.value = newValue
}
}
private fun fetchFromNetwork() {
val apiResponse = createCall()
result.addSource(apiResponse) { response ->
result.removeSource(apiResponse)
response?.let {
if (response.isSuccessful) {
appExecutors.diskIO().execute({
val requestType = processResponse(response)
val resultType = processResult(requestType)
appExecutors.mainThread().execute({
setResultValue(Resource.success(resultType))
}
)
})
} else {
val errorMessage = when (response.errorThrowable) {
is HttpException -> "An error has occurred: ${response.errorThrowable.code()} Please try again."
is SocketTimeoutException -> "A timeout error has occurred, please check your internet connection and try again"
is IOException -> "An IO error has occurred, most likely a network issue. Please check your internet connection and try again"
is UnauthorizedCredentialsException -> "This user name or password is not recognized"
else -> {
response.errorMessage
}
}
Timber.e(errorMessage)
errorMessage?.let {
val requestType = processResponse(response)
val resultType = processResult(requestType)
setResultValue(Resource.error(errorMessage, resultType, response.errorThrowable))
}
onFetchFailed()
}
}
}
}
protected open fun onFetchFailed() {}
fun asLiveData() = result as LiveData<Resource<ResultType>>
#WorkerThread
protected open fun processResponse(response: ApiResponse<RequestType>) = response.body
#WorkerThread
protected abstract fun processResult(item: RequestType?): ResultType?
#MainThread
protected abstract fun createCall(): LiveData<ApiResponse<RequestType>>
}
The processResult() function allows you to transform a successful RequestType into a ResultType. It seems to work for me but would love any feedback from someone that knows what they are doing :)
Fyi Yigit has since updated the NetworkBoundResource with better error handling which should also work here in the not-successful 'else' statement.
Here's my version which I wrote sometime back:
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MediatorLiveData
import android.support.annotation.MainThread
/**
* A generic class to send loading event up-stream when fetching data
* only from network.
*
* #param <RequestType>
</RequestType></ResultType> */
abstract class NetworkResource<RequestType> #MainThread constructor() {
/**
* The final result LiveData
*/
private val result = MediatorLiveData<Resource<RequestType>>()
init {
// Send loading state to UI
result.value = Resource.loading()
fetchFromNetwork()
}
/**
* Fetch the data from network and then send it upstream to UI.
*/
private fun fetchFromNetwork() {
val apiResponse = createCall()
// Make the network call
result.addSource(apiResponse) { response ->
result.removeSource(apiResponse)
// Dispatch the result
response?.apply {
when {
status.isSuccessful() -> setValue(this)
else -> setValue(Resource.error(errorMessage))
}
}
}
}
#MainThread
private fun setValue(newValue: Resource<RequestType>) {
if (result.value != newValue) result.value = newValue
}
fun asLiveData(): LiveData<Resource<RequestType>> {
return result
}
#MainThread
protected abstract fun createCall(): LiveData<Resource<RequestType>>
}
This for database operation only in case you needed it (with kotlin coroutine
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
/**
* A generic class that can provide a resource backed by the sqlite database.
*
*
* #param <ResultType>
</ResultType> */
abstract class DatabaseResource<ResultType> {
private val result = MediatorLiveData<Resource<ResultType>>()
init {
result.value = Resource.loading(null)
GlobalScope.launch(Dispatchers.IO) {
val dbSource = performDbOperation()
GlobalScope.launch(Dispatchers.Main) {
result.addSource(dbSource) { data ->
result.removeSource(dbSource)
result.addSource(dbSource) { newData ->
setValue(Resource.success(newData))
}
}
}
}
}
private fun setValue(newValue: Resource<ResultType>) {
if (result.value != newValue) {
result.value = newValue
}
}
fun asLiveData() = result as LiveData<Resource<ResultType>>
protected abstract fun performDbOperation(): LiveData<ResultType>
}
For future Kotlin users, make it simple as:
1. Resource class:
sealed class Resource<T>(
val data: T? = null,
val error: Throwable? = null
) {
class Success<T>(data: T) : Resource<T>(data)
class Loading<T>(data: T? = null) : Resource<T>(data)
class Error<T>(throwable: Throwable, data: T? = null) : Resource<T>(data, throwable)
}
2. NetworkBoundResource:
inline fun <T> networkBoundResource(
crossinline fetch : suspend () -> Response<T>
) = flow {
emit(Resource.Loading(null))
try {
emit(Resource.Success(fetch().body()))
}catch(throwable : Throwable){
emit(Resource.Error(throwable, null))
}
}

Get item by id in Room

I'm using Room + LiveData in my Android project. Following to Google Blueprints, I've implemented data layer of my application.
This is how my Dao looks like:
#Query("SELECT * FROM events WHERE id=:arg0")
fun loadSingle(id: String): LiveData<Event>
I'm calling it from my EventRepository:
fun loadSingle(eventId: String): LiveData<RequestReader<Event>> {
return object: NetworkManager<Event, Event>(appExecutors!!) {
override fun loadLocal(): LiveData<Event> {
val item = eventLocal!!.loadSingle("Title 1")
Crashlytics.log(Log.VERBOSE, TAG, "loadFromServer::loadLocal=$item")
return item
}
override fun isUpdateForced(data: Event?): Boolean {
Crashlytics.log(Log.VERBOSE, TAG, "loadFromServer::isUpdateForced")
return data == null || requestTimeout.isAllowed(UNDEFINED_KEY.toString())
}
override fun makeRequest(): LiveData<ApiResponse<Event>> {
Crashlytics.log(Log.VERBOSE, TAG, "loadFromServer::makeRequest")
return Database.createService(EventRemote::class.java).load(eventId)
}
override fun onSuccess(item: Event) {
eventLocal?.save(item)
}
override fun onFail() {
Crashlytics.log(Log.VERBOSE, TAG, "loadFromServer::onFail")
requestTimeout.reset(UNDEFINED_KEY.toString())
}
}.liveData
}
Where NetworkManager class is (has been "taken" from here):
abstract class NetworkManager<ResultType, RequestType> #MainThread constructor(val appExecutors: AppExecutors) {
companion object {
private val TAG = "TAG_NETWORK_MANAGER"
}
val liveData: MediatorLiveData<RequestReader<ResultType>> = MediatorLiveData()
init {
liveData.value = RequestReader.loading(null)
val localSource: LiveData<ResultType> = loadLocal()
Log.d(TAG, "before add::localSource=${localSource.value}")
liveData.addSource(localSource, { data ->
Log.d(TAG, "data=$data")
liveData.removeSource(localSource)
if (isUpdateForced(data)) {
loadRemote(localSource)
} else {
liveData.addSource(localSource, { reusedData -> liveData.value = RequestReader.success(reusedData)})
}
})
}
private fun loadRemote(localSource: LiveData<ResultType>) {
val remoteSource = makeRequest()
liveData.addSource(localSource, {
liveData.value = RequestReader.success(it)
})
liveData.addSource(remoteSource) { response ->
liveData.removeSource(localSource)
liveData.removeSource(remoteSource)
if (response!!.isSuccessful) {
appExecutors.diskIO.execute {
onSuccess(processResponse(response))
appExecutors.mainThread.execute {
liveData.addSource(localSource, {
liveData.value = RequestReader.success(it)
})
}
}
} else {
onFail()
liveData.addSource(localSource, {
liveData.value = RequestReader.error("Error: ${response.errorMessage}", it)
})
}
}
}
#MainThread
protected abstract fun loadLocal(): LiveData<ResultType>
#MainThread
protected abstract fun isUpdateForced(data: ResultType?): Boolean
#MainThread
protected abstract fun makeRequest(): LiveData<ApiResponse<RequestType>>
#WorkerThread
protected abstract fun onSuccess(item: RequestType)
#MainThread
protected abstract fun onFail()
#WorkerThread
protected fun processResponse(response: ApiResponse<RequestType>): RequestType {
return response.body!!
}
}
And after i expect to get my LiveData in ViewModel:
open class EventSingleViewModel: ViewModel(), RepositoryComponent.Injectable {
companion object {
private val TAG = "TAG_EVENT_SINGLE_VIEW_MODEL"
}
#Inject lateinit var eventRepository: EventRepository
var eventSingle: LiveData<RequestReader<Event>>? = null
override fun inject(repositoryComponent: RepositoryComponent) {
repositoryComponent.inject(this)
eventSingle = MutableLiveData<RequestReader<Event>>()
}
fun load(eventId: String) {
Crashlytics.log(Log.VERBOSE, TAG, "starts to loadList::eventId=$eventId")
eventSingle = eventRepository.loadSingle(eventId)
}
}
The problem.
I'm getting a list of events the same way (it works!) I've described above, but with a single event (this event is already in database) it doesn't work. I've found out that localSource.value is null (in NetworkManager). Maybe my query is bad or.. something else.
Check again your DAO implementation, the argument must be the same in both, the function parameter and the annotation arg.
Change this:
#Query("SELECT * FROM events WHERE id=:arg0")
fun loadSingle(id: String): LiveData<Event>
To:
#Query("SELECT * FROM events WHERE id=:id ")
fun loadSingle(id: String): LiveData<Event>

Categories

Resources