Here i got a sample of code in presenter. How do i make write a test for onSuccess and onFailure in retrofit call
public void getNotifications(final List<HashMap<String,Object>> notifications){
if (!"".equalsIgnoreCase(userDB.getValueFromSqlite("email",1))) {
UserNotifications userNotifications =
new UserNotifications(userDB.getValueFromSqlite("email",1),Integer.parseInt(userDB.getValueFromSqlite("userId",1).trim()));
Call call = apiInterface.getNotifications(userNotifications);
call.enqueue(new Callback() {
#Override
public void onResponse(Call call, Response response) {
UserNotifications userNotifications1 = (UserNotifications) response.body();
if(userNotifications1.getNotifications().isEmpty()){
view.setListToAdapter(notifications);
onFailure(call,new Throwable());
}
else {
for (UserNotifications.Datum datum:userNotifications1.getNotifications()) {
HashMap<String,Object> singleNotification= new HashMap<>();
singleNotification.put("notification",datum.getNotification());
singleNotification.put("date",datum.getDate());
notifications.add(singleNotification);
}
view.setListToAdapter(notifications);
}
}
#Override
public void onFailure(Call call, Throwable t) {
call.cancel();
}
});
}
}
}
How do i write unittesting to cover all cases for this piece of code.
Thanks
When you want to test different responses from service (API) it's probably best to mock it and return what you need.
#Test
public void testApiResponse() {
ApiInterface mockedApiInterface = Mockito.mock(ApiInterface.class);
Call<UserNotifications> mockedCall = Mockito.mock(Call.class);
Mockito.when(mockedApiInterface.getNotifications()).thenReturn(mockedCall);
Mockito.doAnswer(new Answer() {
#Override
public Void answer(InvocationOnMock invocation) throws Throwable {
Callback<UserNotifications> callback = invocation.getArgumentAt(0, Callback.class);
callback.onResponse(mockedCall, Response.success(new UserNotifications()));
// or callback.onResponse(mockedCall, Response.error(404. ...);
// or callback.onFailure(mockedCall, new IOException());
return null;
}
}).when(mockedCall).enqueue(any(Callback.class));
// inject mocked ApiInterface to your presenter
// and then mock view and verify calls (and eventually use ArgumentCaptor to access call parameters)
}
For those looking for an answer using Kotlin and MockK:
Assume you have something like this:
class ApiService(private val client: OkHttpClient) {
fun makeApiCall() {
val url = "https://someendpoint.com.br/"
val request = Request.Builder().url(url).build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, exception: IOException) {
//Logic to handle Failure
}
override fun onResponse(call: Call, response: Response) {
//Logic to handle Success
}
})
}
}
You can test this using Junit 5 and mockK
class ApiServiceTest {
private lateinit var client: okhttp3.OkHttpClient
private lateinit var apiService: ApiService
#BeforeEach
fun setup() {
// Setup a new mock for each test case
client = mockk(relaxed = true)
apiService = ApiService(client)
}
#Test
fun `test with mockedCallback`() {
val mockedCall = mockk<Call>(relaxed = true)
every { mockedCall.enqueue(any()) } answers {
//Get the callback from the arguments
val callback = args[0] as Callback
// Create a fakeRequest
val fakeRequest = okhttp3.Request.Builder().url("https://someendpoint.com.br/").build()
// Create the response you need to test, it can be failure or success
val errorResponse = Response.Builder()
.request(fakeRequest)
.protocol(Protocol.HTTP_1_1)
.code(400)
.message("Error")
.build()
//Call the callback with the mocked response you created.
callback.onResponse(mockedCall, errorResponse)
}
// Setup the client mock to return the mocked call you created
every {
client.newCall(any())
} returns mockedCall
apiService.makeApiCall()
// Verify whatever you need to test your logic to handle each case.
}
}
Related
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;
}
}
});
I am integrating a Login System for that I want the Response.Listener and Response.ErrorListener function to return a Boolean.
I searched on the google and found that it can be done using a callback interface ( This Question ). But I don't know how to do that in Kotlin
[ALSO UPDATED] This is my Kotlin file containing my function which are used by many activities:
package com.pratham.example
// My required imports
import ...
fun function1(){
...
}
fun function2(){
...
}
// This Auth function will return True if login success
fun auth(activity:Activity):Boolean{
val queue = Volley.newRequestQueue(activity)
val req = JsonObjectRequest(Request.Method.POST, url, jsonObj,
Response.Listener { response ->
val JSONObj = response.getString("Status")
if(JSONObj=="200"){
// return true
}
else{
// return false
}
}, Response.ErrorListener {
// return false
}
)
queue.add(req)
}
fun function4(){
...
}
This function will return true or false according to the Volley Response.
How can i return value from a Response.Listener and Response.ErrorListener in Kotlin
Edit:: I tried to use Interface like this
package com.pratham.sitapuriya
import ...
fun function1(){
...
}
fun function2(){
...
}
interface MyInterface {
fun onCallback(response: Boolean,context:Context)
}
class Login : MyInterface {
private val myInterface = this
override fun onCallback(response: Boolean,context:Context) {
Toast.makeText(context,"Working",Toast.LENGTH_SHORT).show()
}
fun auth(activity: Activity): Boolean {
val sp1 = activity.getSharedPreferences("Login", AppCompatActivity.MODE_PRIVATE)
if (sp1.contains("token") && sp1.contains("deviceId")) {
val token = sp1.getString("token", null)
val deviceId = sp1.getString("deviceId", null)
if (!token.isNullOrEmpty() && !deviceId.isNullOrEmpty()) {
// val url = "http://10.0.2.2:80/Projects/Sitapuriya/login.php"
val url = activity.getString(R.string.server) + "/tokenAuth.php"
val params = HashMap<String, String>()
params["token"] = token
params["deviceId"] = deviceId
val jsonObj = JSONObject(params)
val queue = Volley.newRequestQueue(activity)
val req = JsonObjectRequest(Request.Method.POST, url, jsonObj,
Response.Listener { response ->
val JSONObj = response.getString("Status")
if (JSONObj == "200") {
// return true
myInterface.onCallback(true,activity)
} else {
// return false
myInterface.onCallback(false,activity)
}
}, Response.ErrorListener {
// return false
myInterface.onCallback(false,activity)
}
)
queue.add(req)
} else {
return false
}
} else {
return false
}
return false
}
}
fun function4(){
...
}
But now i have no idea that how the callback will return value in auth() function and also how I'll be calling this auth() fucntion in my other activities .
Please help I have never used a callback before..
You can't, the code you have executes asynchronously so you can't have the return statement there.
But it doesn't mean you can not get the value. The easiest option is create a LiveData<Boolean> and observe it in your activity. When the value changes you will get notified.
The next thing you can do is you can create an interface.
interface MyCallback{
fun onValueChanged()
}
Then implement this interface in your activity and override the method. Then you can use it to get a callback when your asynchronous call finishes. See the below code.
interface MyInterface{
fun onCallback(response:Boolean)
}
class LoginActivity : AppCompatActivity(), AuthListener, MyInterface {
val myInterface = this
override fun onCallback(response: Boolean) {
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val queue = Volley.newRequestQueue(activity)
val req = JsonObjectRequest(Request.Method.POST, url, jsonObj,
Response.Listener { response ->
val JSONObj = response.getString("Status")
if(JSONObj=="200"){
//return true
myInterface.onCallback(true)
}
else{
myInterface.onCallback(false)
}
}, Response.ErrorListener {
// return false
}
)
queue.add(req)
}
}
Hope this helps. Thank You :)
How to get a LiveData list using Retrofit GET to populate a recyclerView. My code is not showing the data on screen. Is onChange() called if I'm not changing an existing list, but starting with new data first time?
MainActivity where I call the ViewModel for check data and load when set into the adapter:
var mContactViewModel = ViewModelProviders.of(this).get(ContactViewModel::class.java)
mContactViewModel.getContacts(this)?.observe(this, object : Observer<List<Contact>> {
override fun onChanged(contactsList: List<Contact>?) {
loading_contacts_list.visibility = View.GONE
if(contactsList!!.isNotEmpty()){
rv_users.adapter = ContactsAdapter(contactsList, onContactClick, context)
} else {
//TODO throw exception
println("list of contacts returned empty")
}
}
})
ContactViewModel class where I wrap the getContact function from ContactRepository:
class ContactViewModel(application: Application) : AndroidViewModel(application) {
private val mRepository: ContactRepository
init {
mRepository = ContactRepository()
}
fun getContacts(context: Context) : LiveData<List<Contact>>? {
return mRepository.getContacts(context)
}
}
ContactRepository class where Retrofit makes the GET request, put a LiveData> in a list inside this class and return it:
class ContactRepository {
var allContacts: LiveData<List<Contact>>? = null
var call: Call<LiveData<List<Contact>>>
init {
var requestService: RequestService = RetrofitClient().getClient(MY_URL)!!.create(RequestService::class.java)
call = requestService.getContacts()
}
fun getContacts(context: Context) : LiveData<List<Contact>>? {
call.enqueue(object : Callback<LiveData<List<Contact>>> {
override fun onFailure(call: Call<LiveData<List<Contact>>>, t: Throwable) {
Log.e("Tag onfailure", "retrofit on failure", t)
}
override fun onResponse(call: Call<LiveData<List<Contact>>>, response: Response<LiveData<List<Contact>>>) {
if(response.isSuccessful)
allContacts = response.body()
}
})
return allContacts
}
}
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))
}
}
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)
}
})
}