I am developing an application which doing network request using retrofit2 and rxjava2. I am doing it using MVVM approach which is in my RestInterface the result of the request returned in Flowable and in my repository I convert the Flowable into livedata so I can make the activity observe it in my viewmodel. But by doing this I got confuse on how to handle if there is no network where I ussually handle this in the rxJava side but since it's in the repository I can't do much thing about it.
Here is the code for the rest :
#GET(NEWS_ARTICLE)
fun getArticlesFromSources(#Query("domains") source: String,
#Query("apiKey") apiKey: String = BuildConfig.NEWS_API_KEY):
Flowable<NewsResponse>
The code for repository
fun getArticleFromSources(source: String) : LiveData<NewsResponse>{
return LiveDataReactiveStreams.fromPublisher(newsRest.getArticlesFromSources(source)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io()))
}
and in my viewmodel :
private var mTriggerFetchData = MutableLiveData<String>()
private val article: LiveData<NewsResponse> = Transformations.switchMap(mTriggerFetchData){
newsRepository.getArticleFromSources(it)
}
fun getArticle() = article
fun loadArticle(source: String?){
mTriggerFetchData.value = source
}
and I observe it on my Activity :
getViewModel().getArticle().observe(this, Observer {newsResponse ->
Log.v("test", newsResponse?.articles?.size.toString())
articleList?.clear()
newsResponse?.articles?.let { articleList?.addAll(it) }
articleAdapter.notifyDataSetChanged()
})
getViewModel().loadArticle(sourceUrl)
As you can see, I was thinking to handle it in the activity but I still got confused about it. any help would be much appreciated. thanks!
You can try add onErrorReturn() to the chain
fun getArticleFromSources(source: String) : LiveData<NewsResponse>{
return LiveDataReactiveStreams.fromPublisher(newsRest.getArticlesFromSources(source)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.onErrorReturn((e: Throwable) -> {
return e
})
}
Or, rather than exposing just the NewsResponse through your LiveData object you can wrap the object and error into a wrapper class that can hold the error.
You can do something like this:
LiveDataReactiveStreams.fromPublisher(newsRest.getArticlesFromSources(source)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io()
.map(Result::success)
.onErrorReturn(Result::error))
Where Result class that holds either the error or result is something like this
class Result<T>(val data: T?, val error: Throwable?) {
companion object {
fun <T> success(data: T): Result<T> {
return Result(data, null)
}
fun <T> error(error: Throwable): Result<T> {
return Result(null, error)
}
}
}
You can then check if there's an error from the network
Related
I have a favorite way of doing network request on Android (using Retrofit). It looks like this:
// NetworkApi.kt
interface NetworkApi {
#GET("users")
suspend fun getUsers(): List<User>
}
And in my ViewModel:
// MyViewModel.kt
class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
val usersLiveData = flow {
emit(networkApi.getUsers())
}.asLiveData()
}
Finally, in my Activity/Fragment:
//MyActivity.kt
class MyActivity: AppCompatActivity() {
private viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.usersLiveData.observe(this) {
// Update the UI here
}
}
}
The reason I like this way is because it natively works with Kotlin flow, which is very easy to use, and has a lot of useful operations (flatMap, etc).
However, I am not sure how to elegantly handle network errors using this method. One approach that I can think of is to use Response<T> as the return type of the network API, like this:
// NetworkApi.kt
interface NetworkApi {
#GET("users")
suspend fun getUsers(): Response<List<User>>
}
Then in my view model, I can have an if-else to check the isSuccessful of the response, and get the real result using the .body() API if it is successful. But it will be problematic when I do some transformation in my view model. E.g.
// MyViewModel.kt
class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
val usersLiveData = flow {
val response = networkApi.getUsers()
if (response.isSuccessful) {
emit(response.body()) // response.body() will be List<User>
} else {
// What should I do here?
}
}.map { // it: List<User>
// transform Users to some other class
it?.map { oneUser -> OtherClass(oneUser.userName) }
}.asLiveData()
Note the comment "What should I do here?". I don't know what to do in that case. I could wrap the responseBody (in this case, a list of Users) with some "status" (or simply just pass through the response itself). But that means that I pretty much have to use an if-else to check the status at every step through the flow transformation chain, all the way up to the UI. If the chain is really long (e.g. I have 10 map or flatMapConcat on the chain), it is really annoying to do it in every step.
What is the best way to handle network errors in this case, please?
You should have a sealed class to handle for different type of event. For example, Success, Error or Loading. Here is some of the example that fits your usecases.
enum class ApiStatus{
SUCCESS,
ERROR,
LOADING
} // for your case might be simplify to use only sealed class
sealed class ApiResult <out T> (val status: ApiStatus, val data: T?, val message:String?) {
data class Success<out R>(val _data: R?): ApiResult<R>(
status = ApiStatus.SUCCESS,
data = _data,
message = null
)
data class Error(val exception: String): ApiResult<Nothing>(
status = ApiStatus.ERROR,
data = null,
message = exception
)
data class Loading<out R>(val _data: R?, val isLoading: Boolean): ApiResult<R>(
status = ApiStatus.LOADING,
data = _data,
message = null
)
}
Then, in your ViewModel,
class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
// this should be returned as a function, not a variable
val usersLiveData = flow {
emit(ApiResult.Loading(true)) // 1. Loading State
val response = networkApi.getUsers()
if (response.isSuccessful) {
emit(ApiResult.Success(response.body())) // 2. Success State
} else {
val errorMsg = response.errorBody()?.string()
response.errorBody()?.close() // remember to close it after getting the stream of error body
emit(ApiResult.Error(errorMsg)) // 3. Error State
}
}.map { // it: List<User>
// transform Users to some other class
it?.map { oneUser -> OtherClass(oneUser.userName) }
}.asLiveData()
In your view (Activity/Fragment), observe these state.
viewModel.usersLiveData.observe(this) { result ->
// Update the UI here
when(result.status) {
ApiResult.Success -> {
val data = result.data <-- return List<User>
}
ApiResult.Error -> {
val errorMsg = result.message <-- return errorBody().string()
}
ApiResult.Loading -> {
// here will actually set the state as Loading
// you may put your loading indicator here.
}
}
}
//this class represent load statement management operation
/*
What is a sealed class
A sealed class is an abstract class with a restricted class hierarchy.
Classes that inherit from it have to be in the same file as the sealed class.
This provides more control over the inheritance. They are restricted but also allow freedom in state representation.
Sealed classes can nest data classes, classes, objects, and also other sealed classes.
The autocomplete feature shines when dealing with other sealed classes.
This is because the IDE can detect the branches within these classes.
*/
ٍٍٍٍٍ
sealed class APIResponse<out T>{
class Success<T>(response: Response<T>): APIResponse<T>() {
val data = response.body()
}
class Failure<T>(response: Response<T>): APIResponse<T>() {
val message:String = response.errorBody().toString()
}
class Exception<T>(throwable: Throwable): APIResponse<T>() {
val message:String? = throwable.localizedMessage
}
}
create extention file called APIResponsrEX.kt
and create extextion method
fun <T> APIResponse<T>.onSuccess(onResult :APIResponse.Success<T>.() -> Unit) : APIResponse<T>{
if (this is APIResponse.Success) onResult(this)
return this
}
fun <T> APIResponse<T>.onFailure(onResult: APIResponse.Failure<*>.() -> Unit) : APIResponse<T>{
if (this is APIResponse.Failure<*>)
onResult(this)
return this
}
fun <T> APIResponse<T>.onException(onResult: APIResponse.Exception<*>.() -> Unit) : APIResponse<T>{
if (this is APIResponse.Exception<*>) onResult(this)
return this
}
merge it with Retrofit
inline fun <T> Call<T>.request(crossinline onResult: (response: APIResponse<T>) -> Unit) {
enqueue(object : retrofit2.Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
// success
onResult(APIResponse.Success(response))
} else {
//failure
onResult(APIResponse.Failure(response))
}
}
override fun onFailure(call: Call<T>, throwable: Throwable) {
onResult(APIResponse.Exception(throwable))
}
})
}
The "proper" way to update views with Android seems to be LiveData. But I can't determine the "proper" way to connect that to a model. Most of the documentation I have seen shows connecting to Room which returns a LiveData object. But (assuming I am not using Room), returning a LiveData object (which is "lifecycle aware", so specific to the activity/view framework of Android) in my model seems to me to violate the separation of concerns?
Here is an example with Activity...
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_activity);
val viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
val nameText = findViewById<TextView>(R.id.nameTextBox)
viewModel.getName().observe(this, { name ->
nameText.value = name
})
}
}
And ViewModel...
class UserViewModel(): ViewModel() {
private val name: MutableLiveData<String> = MutableLiveData()
fun getName() : LiveData<String> {
return name
}
}
But how do I then connect that to my Model without putting a "lifecycle aware" object that is designed for a specific framework in my model (LiveData)...
class UserModel {
val uid
var name
fun queryUserInfo() {
/* API query here ... */
val request = JSONObjectRequest( ...
{ response ->
if( response.name != this.name ) {
this.name = response.name
/* Trigger LiveData update here somehow??? */
}
}
)
}
}
I am thinking I can maybe put an Observable object in my model and then use that to trigger the update of the LiveData in my ViewModel. But don't find any places where anyone else says that is the "right" way of doing it. Or, can I instantiate the LiveData object in the ViewModel from an Observable object in my model?
Or am I just thinking about this wrong or am I missing something?
This is from official documentation. Check comments in code...
UserModel should remain clean
class UserModel {
private val name: String,
private val lastName: String
}
Create repository to catch data from network
class UserRepository {
private val webservice: Webservice = TODO()
fun getUser(userId: String): LiveData<UserModel > {
val data = MutableLiveData<UserModel>() //Livedata that you observe
//you can get the data from api as you want, but it is important that you
//update the LiveDate that you will observe from the ViewModel
//and the same principle is in the relation ViewModel <=> Fragment
webservice.getUser(userId).enqueue(object : Callback<UserModel > {
override fun onResponse(call: Call<User>, response: Response<UserModel >) {
data.value = response.body()
}
// Error case is left out for brevity.
override fun onFailure(call: Call<UserModel >, t: Throwable) {
TODO()
}
})
return data //you will observe this from ViewModel
}
}
The following picture should explain to you what everything looks like
For more details check this:
https://developer.android.com/jetpack/guide
viewmodels-and-livedata-patterns-antipatterns
hello I'm trying to study dataBinding, mvvm, retrofit and rxjava
in viewModel I used this code
private var mainRepository: MainRepository = MainRepository(NetManager(getApplication()))
val isLoading = ObservableField(false)
var mainModel = MutableLiveData<ArrayList<MainModel>>()
private val compositeDisposable = CompositeDisposable()
fun loadRepositories(id: Int, mainContract: MainContract) {
isLoading.set(true)
compositeDisposable += mainRepository
.getData(id, mainContract)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableObserver<ArrayList<MainModel>>() {
override fun onError(e: Throwable) {
//if some error happens in our data layer our app will not crash, we will
// get error here
}
override fun onNext(data: ArrayList<MainModel>) {
mainModel.value= data
}
override fun onComplete() {
isLoading.set(false)
}
})
}
and in the MainRepository I used the retrofit with RxJava code
private val model = ArrayList<MainModel>()
fun getData(id: Int, mainContract: MainContract): Observable<ArrayList<MainModel>> {
Api.getData.getMainCategory(id)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe ({
model.clear()
model.addAll(it)
AppLogger.log("testingModel1", model.toString())
}, {
AppLogger.log("error", "Failed to load Category : $it")
mainContract.toast("Failed to load Category")
})
AppLogger.log("testingModel2", model.toString())
return Observable.just(model)
}
if you notified that I'm using log to see the output data
but what I see is that
AppLogger.log("testingModel2", model.toString())
and
return Observable.just(model)
are running before
Api.getData.getMainCategory(id)
so the output in Logcat testingModel2 first and it is empty then testingModel1 and it is have data
so the result data in
return Observable.just(model)
is nothing
I hope you understand ^_^
Thank you for help
do like:
fun getData(id: Int, mainContract: MainContract): Observable<ArrayList<MainModel>> {
return Api.getData.getMainCategory(id)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
}
but remember to subscribe to it later and add ErrorHandling
And about logs: the problem that actions in subscribe block runs only when Api.getData.getMainCategory(id) emit something, which could take a time.
I need to do custom error handling in my api and I wanted to use coroutines with the new version of Retrofit. Since we don't have to use Deferred any longer, our own Jake Wharton wrote this on reddit a month ago
https://github.com/square/retrofit/blob/master/samples/src/main/java/com/example/retrofit/RxJavaObserveOnMainThread.java
But I'm having problems creating the CallAdapterFactory properly.
To be specific, I don't understand: "Delegates to built-in factory and then wraps the value in sealed class"
Is there anyone already using this setup that can help?
Here's the current code
sealed class Results<out T: Any> {
class Success<out T: Any>(val response: T): Results<T>()
class Failure(val message: String, val serverError: ServerError?): Results<Nothing>()
object NetworkError: Results<Nothing>()
}
class ResultsCallAdapterFactory private constructor() : CallAdapter.Factory() {
companion object {
#JvmStatic
fun create() = ResultsCallAdapterFactory()
}
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
return try {
val enclosedType = returnType as ParameterizedType
val responseType = getParameterUpperBound(0, enclosedType)
val rawResultType = getRawType(responseType)
val delegate: CallAdapter<Any,Any> = retrofit.nextCallAdapter(this,returnType,annotations) as CallAdapter<Any,Any>
if(rawResultType != Results::class.java)
null
else {
object: CallAdapter<Any,Any>{
override fun adapt(call: Call<Any>): Any {
val response = delegate.adapt(call)
//What should happen here?
return response
}
override fun responseType(): Type {
return delegate.responseType()
}
}
}
} catch (e: ClassCastException) {
null
}
}
}
I've created an example of such a factory, you can find it here on GitHub. Also take a look at a similar question: How to create a call adapter for suspending functions in Retrofit?.
My MainRepository is what fetches data from the API and inserts into the database, then displaying on the UI.
override fun fetchAll() {
Observable.fromCallable { local.fetchPosts() }
.doOnNext {
remote.fetchPosts().concatMap { posts ->
local.insert(*posts.toTypedArray())
Observable.just(posts)
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ outcome.success(it) },
{ error: Throwable -> outcome.failed(error) }
).addTo(compositeDisposable)
}
The outcome variable is a PublishObject of type Response, that is Loading, Success, or Failure.
override val outcome = PublishSubject.create<Response<List<Post>>>()
[...]
sealed class Response<T> {
companion object {
fun <T> loading(loading: Boolean): Response<T> = Progress(loading)
fun <T> success(data: T): Response<T> = Success(data)
fun <T> failure(e: Throwable): Response<T> = Failure(e)
}
data class Progress<T>(var loading: Boolean) : Response<T>()
data class Success<T>(var data: T) : Response<T>()
data class Failure<T>(var e: Throwable) : Response<T>()
}
It executes a method local.fetchPosts(), which is a function responsible to access DAO functions.
fun fetchPosts() = database.postDao().fetchAll()
[...]
#Query("SELECT * FROM posts ORDER BY createdAt DESC")
fun fetchAll(): List<Post>
The addTo is an extension of Disposable:
fun Disposable.addTo(compositeDisposable: CompositeDisposable) {
compositeDisposable.add(this)
}
I've tried using concatMap right after the Observable.fromCallable, but it will display data from the API directly while doOnNext will show from the database but it will not update the list, removing what has been removed from the remote server.
First you have to understand that doOnNext is a Side Effect Operator, along with it's family, side effect operators only anticipate the emiisions for doing a minor action (like logging for example), they don't affect the stream in anyway.
So, with that you're doing the observable
remote.fetchPosts().concatMap { posts ->
local.insert(*posts.toTypedArray())
Observable.just(posts)
}
Never gets to work, because it's not being subscribed to.
Now you have to make a decision depending on the behavior you want, I'll assume that your use case is:
Try to get data from API.
if successful cache it, if not query DAO
Display it
then something like this will work:
remote.fetchPosts()
// cache the data from remote.
.doOnNext(posts -> local.insert(*posts.toTypedArray()))
// if an error happens, use the posts in the DAO.
.onErrorResumeNext { Observable.fromCallable { local.fetchPosts() } }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
[...]