I have an activity on which I make a network request everytime the user input changes.
The api definition is as follows:
interface Api {
#GET("/accounts/check")
fun checkUsername(#Query("username") username: String): Observable<UsernameResponse>
}
Then the services that manages it all:
class ApiService {
var api: Api
init {
api = retrofit.create(Api::class.java)
}
companion object {
val baseUrl: String = "https://someapihost"
var rxAdapter: RxJava2CallAdapterFactory = RxJava2CallAdapterFactory.create()
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(rxAdapter)
.build()
}
fun checkUsername(username: String): Observable<UsernameResponse> {
return api.checkUsername(username)
}
}
Then inside my activity, whenever the EditText content changes, I make this call:
private fun checkUsername(username: String) {
cancelSubscription()
checkUsernameDisposable = ApiService()
.checkUsername(username)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
updateUi(it)
}
}
So this is creating a new disposable every time the input changes. This is obviously incorrect. What I want to do is to update the existing subscription with the results of the new network call.
First of all you're thinking right, creating an Observable for each change event is far from efficient.
There are 2 approaches to this:
One
You can use RxBinding to get a text change Observable, now you can flatMap the text changes to your apiService call, down to one disposable.
disposable = RxTextView.textChanges(editText)
.switchMap { ApiService().checkUsername(it) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { updateUi(it) }
Two
You can use a Subject to use as a channel for the changes of the EditText like this:
val editTextChangesSubject: PublishSubject<String> = PublishSubject.create()
// when the editText changes call
editTextChangesSubject.onNext(newText)
disposable = editTextChangesSubject
.switchMap { ApiService().checkUsername(it) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { updateUi(it) }
Now that's also down to one disposable!
Note: People sometimes tend to use the Subject technique if they're using a specific architecture pattern that separates the View logic from the middle man logic, if you're not bound by that, RxBinding is the way to go.
Also if worth mentioning, the two approaches will give you powers that didn't exist when subscribing for each text change event, like using flow control operators like debounce or onBackpressureLatest.
Edit:
Used switchMap instead of flatMap, see the difference in Here
Related
I'm creating offline first app as my side project using rxKotlin, MVVM + Clean Architecture and yesterday I decided to get ride off boilerplate subscribeOn and observeOn by using transformers. I quickly realized that apply function of transformers are ignored.
Here is code of my base completable use case (interactor):
abstract class CompletableUseCase(private val transformer: CompletableTransformer) {
abstract fun createCompletable(data: Map<String, Any>? = null) : Completable
fun completable(data: Map<String, Any>? = null) : Completable {
return createCompletable(data).compose(transformer)
}
}
And here is implementation of specific interactor:
class SaveRouteInteractor(
transformer: CompletableTransformer,
private val routeRepository: RouteRepository
) : CompletableUseCase(transformer) {
companion object {
private const val PARAM_ROUTE = "param_route"
}
fun saveRoute(route: Route) : Completable {
val data = HashMap<String, Route>()
data[PARAM_ROUTE] = route
return completable(data)
}
override fun createCompletable(data: Map<String, Any>?): Completable {
val routeEntity = data?.get(PARAM_ROUTE)
routeEntity?.let {
return routeRepository.saveRoute(routeEntity as Route)
} ?: return Completable.error(IllegalArgumentException("Argument #route must be provided."))
}
}
My custom transformer that is passed to the constructor of SaveRouteInteractor:
class IOCompletableTransformer(private val mainThreadScheduler: Scheduler) : CompletableTransformer {
override fun apply(upstream: Completable): CompletableSource {
return upstream.subscribeOn(Schedulers.io()).observeOn(mainThreadScheduler)
}
}
And implementation of RouteRepository method:
override fun saveRoute(route: Route): Completable {
return localRouteSource.saveRoute(route)
.flatMap { localID ->
route.routeId = localID
remoteRouteSource.saveRoute(route)
}
.flatMapCompletable { localRouteSource.updateRouteID(route.routeId, it) }
}
I'm using Room as my local source so after calling save interactor in my ViewModel I'm getting IlligalStateException telling me that I'm not allowed to access database on the main thread.
Maybe I'm missing something but it seems that transform function is ignored. I debugged this method and it is applying subscribeOn and observeOn to the upstream.
Thanks for help in advance,
Pace!
It's hard to tell you where the issue is because the code is partial.
For example here:
return localRouteSource.saveRoute(route)
.flatMap { localID ->
route.routeId = localID
remoteRouteSource.saveRoute(route)
}
.flatMapCompletable { localRouteSource.updateRouteID(route.routeId, it) }
I suppose the localRouteSource.saveRoute() is using the interactor you show us but it is not clear how remoteRouteSource.saveRoute() or localRouteSource.updateRouteID() are implemented.
they also need to be subscribed on the IO thread.
As a rule of thumb you should switch thread when you KNOW that you need it.
In other words, you should use subscribeOn() in places where you know you are doing IO as close as possible to the actual job. ObserveOn instead is to be used when you know you need to obtain those results in the UI thread and that you might get in some other thread.
in your example there's absolutely no need to keep using observeOn(MAIN_THREAD), the only time you do need it (I suppose) is when you want to show the result.
A couple of other things:
This code
override fun createCompletable(data: Map<String, Any>?): Completable {
val routeEntity = data?.get(PARAM_ROUTE)
routeEntity?.let {
return routeRepository.saveRoute(routeEntity as Route)
} ?: return Completable.error(IllegalArgumentException("Argument #route must be provided."))
}
it is evaluated at the time when the method is called rather then when the completable is subscribed.
In other words it break the Rx contract and compute data?.get(PARAM_ROUTE) when you call the method. If it is immutable there's no much difference, but if it can change value during execution it should be wrapped in a Completable.defer { }
Finally, here
.flatMap { localID ->
route.routeId = localID
remoteRouteSource.saveRoute(route)
}
you are modyfing something outside the chain (route.routeId = localID), this is called a side effect.
be careful with those kind of stuff, Rx is build in a way that is safer to be used with immutable objects.
I personally wouldn't mind too much as long as you understand what's going on and when it could create issues.
I'm using mvvm and android architecture component , i'm new in this architecture .
in my application , I get some data from web service and show them in recycleView , it works fine .
then I've a button for adding new data , when the user input the data , it goes into web service , then I have to get the data and update my adapter again.
this is my code in activity:
private fun getUserCats() {
vm.getCats().observe(this, Observer {
if(it!=null) {
rc_cats.visibility= View.VISIBLE
pb.visibility=View.GONE
catAdapter.reloadData(it)
}
})
}
this is view model :
class CategoryViewModel(private val model:CategoryModel): ViewModel() {
private lateinit var catsLiveData:MutableLiveData<MutableList<Cat>>
fun getCats():MutableLiveData<MutableList<Cat>>{
if(!::catsLiveData.isInitialized){
catsLiveData=model.getCats()
}
return catsLiveData;
}
fun addCat(catName:String){
model.addCat(catName)
}
}
and this is my model class:
class CategoryModel(
private val netManager: NetManager,
private val sharedPrefManager: SharedPrefManager) {
private lateinit var categoryDao: CategoryDao
private lateinit var dbConnection: DbConnection
private lateinit var lastUpdate: LastUpdate
fun getCats(): MutableLiveData<MutableList<Cat>> {
dbConnection = DbConnection.getInstance(MyApp.INSTANCE)!!
categoryDao = dbConnection.CategoryDao()
lastUpdate = LastUpdate(MyApp.INSTANCE)
if (netManager.isConnected!!) {
return getCatsOnline();
} else {
return getCatsOffline();
}
}
fun addCat(catName: String) {
val Category = ApiConnection.client.create(Category::class.java)
Category.newCategory(catName, sharedPrefManager.getUid())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ success ->
getCatsOnline()
}, { error ->
Log.v("this", "ErrorNewCat " + error.localizedMessage)
}
)
}
private fun getCatsOnline(): MutableLiveData<MutableList<Cat>> {
Log.v("this", "online ");
var list: MutableLiveData<MutableList<Cat>> = MutableLiveData()
list = getCatsOffline()
val getCats = ApiConnection.client.create(Category::class.java)
getCats.getCats(sharedPrefManager.getUid(), lastUpdate.getLastCatDate())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ success ->
list += success.cats
lastUpdate.setLastCatDate()
Observable.just(DbConnection)
.subscribeOn(Schedulers.io())
.subscribe({ db ->
categoryDao.insert(success.cats)
})
}, { error ->
Log.v("this", "ErrorGetCats " + error.localizedMessage);
}
)
return list;
}
I call getCat from activity and it goes into model and send it to my web service , after it was successful I call getCatsOnline method to get the data again from webservice .
as I debugged , it gets the data but it doesn't notify my activity , I mean the observer is not triggered in my activity .
how can I fix this ? what is wrong with my code?
You have made several different mistakes of varying importance in LiveData and RxJava usage, as well as MVVM design itself.
LiveData and RxJava
Note that LiveData and RxJava are streams. They are not one time use, so you need to observe the same LiveData object, and more importantly that same LiveData object needs to get updated.
If you look at getCatsOnline() method, every time the method gets called it's creating a whole new LiveData instance. That instance is different from the previous LiveData object, so whatever that is listening to the previous LiveData object won't get notified to the new change.
And few additional tips:
In getCatsOnline() you are subscribing to an Observable inside of another subscriber. That is common mistake from beginners who treat RxJava as a call back. It is not a call back, and you need to chain these calls.
Do not subscribe in Model layer, because it breaks the stream and you cannot tell when to unsubscribe.
It does not make sense to ever use AndroidSchedulers.mainThread(). There is no need to switch to main thread in Model layer especially since LiveData observers only run on main thread.
Do not expose MutableLiveData to other layer. Just return as LiveData.
One last thing I want to point out is that you are using RxJava and LiveData together. Since you are new to both, I recommend you to stick with just one of them. If you must need to use both, use LiveDataReactiveStreams to bridge these two correctly.
Design
How to fix all this? I am guessing that what you are trying to do is to:
(1) view needs category -> (2) get categories from the server -> (3) create/update an observable list object with the new cats, and independently keep the result in DB -> (4) list instance should notify activity automatically.
It is difficult to pull this off correctly because you have this list instance that you have to manually create and update. You also need to worry about where and how long to keep this list instance.
A better design would be:
(1) view needs category -> (2) get a LiveData from DB and observe -> (3) get new categories from the server and update DB with the server response -> (4) view is notified automatically because it's been observing DB!
This is much easier to implement because it has this one way dependency: View -> DB -> Server
Example CategoryModel:
class CategoryModel(
private val netManager: NetManager,
private val sharedPrefManager: SharedPrefManager) {
private val categoryDao: CategoryDao
private val dbConnection: DbConnection
private var lastUpdate: LastUpdate // Maybe store this value in more persistent place..
fun getInstance(netManager: NetManager, sharedPrefManager: SharedPrefManager) {
// ... singleton
}
fun getCats(): Observable<List<Cat>> {
return getCatsOffline();
}
// Notice this method returns just Completable. Any new data should be observed through `getCats()` method.
fun refreshCats(): Completable {
val getCats = ApiConnection.client.create(Category::class.java)
// getCats method may return a Single
return getCats.getCats(sharedPrefManager.getUid(), lastUpdate.getLastCatDate())
.flatMap { success -> categoryDao.insert(success.cats) } // insert to db
.doOnSuccess { lastUpdate.setLastCatDate() }
.ignoreElement()
.subscribeOn(Schedulers.io())
}
fun addCat(catName: String): Completable {
val Category = ApiConnection.client.create(Category::class.java)
// newCategory may return a Single
return Category.newCategory(catName, sharedPrefManager.getUid())
.ignoreElement()
.andThen(refreshCats())
.subscribeOn(Schedulers.io())
)
}
}
I recommend you to read through Guide to App Architecture and one of these livedata-mvvm example app from Google.
Retrofit first request with Single blocks UI thread. Below is relevant code, and more text:
RetrofitProvider
object RetrofitProvider {
private val TAG: String = RetrofitProvider::class.java.simpleName
val retrofit: Retrofit by lazy {
val httpClient = OkHttpClient.Builder()
.addInterceptor {
val request = it.request()
if (BuildConfig.DEBUG) {
Log.d(TAG, "${request.method()}: ${request.url()}")
}
it.proceed(request)
}
.build()
Retrofit.Builder()
.client(httpClient)
.baseUrl("http://192.168.0.10:3000")
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.addConverterFactory(JacksonConverterFactory.create(jacksonObjectMapper()))
.build()
}
}
ProductApi
interface ProductApi {
#GET("/products")
fun getProducts(): Single<List<Product>>
}
MainViewModel
fun fetchProducts() {
productData.value = Resource.Loading()
productApi.getProducts() // <- This call is a problem (even when I comment out all code below)
.subscribeOn(Schedulers.io())
.subscribe(
{
productData.postValue(Resource.Success(it))
},
{
productData.postValue(Resource.Fail(it.message))
})
.addTo(disposableContainer)
}
MainFragment
...
button.setOnClickListener {
Toast.makeText(requireContext(), "click", Toast.LENGTH_SHORT).show()
mainViewModel.fetchProducts()
}
...
App flow is simple, clicking a button on MainFragment calls MainViewModel's fetchProducts() which uses retrofit to fetch some stuff.
productApi.getProducts() happens on UI thread and blocks it significantly(~half a second), even Toast is delayed, even though it should be shown immediately on button click, before getProducts() call.
productApi.getProducts() by itself, without subscribe doesn't send network request (I checked on server side), it just prepares Single.
Important note, delay DOES NOT happen on subsequent clicks to button. Just the first time, I guess creating Single<> is expensive operation.
So my question is, why is UI thread blocked on first request, and how do I fix it the way it isn't ugly/hacking.
Also Observable acts the same, but Completable works much faster, but I need the data, so can't use Completable.
I think your problem lies with the lazy initialisation of your Retrofit object.
It will be deferred to the last possible moment, so I guess the first time you click on the button, you create the expensive retrofit button (this is done on the main thread).
My suggestion is to remove the lazy initialisation and try running the app once again.
Returning Completable also blocks the UI thread but for less time than returning Single or Observable so it seems like it doesn't have any impact but it does.
Invoking the API call on a background thread will not block your UI as the converter creation will not happen on the UI thread.
Something like this does the trick.
Completable.complete()
.observeOn(Schedulers.io())
.subscribe {
productApi.getProducts()
.subscribe(
{
productData.postValue(Resource.Success(it))
},
{
productData.postValue(Resource.Fail(it.message))
}
)
.addTo(disposableContainer)
}
.addTo(disposableContainer)
Another thing you can do instead of using the converter is to make a wrapper class around the Retrofit API which will call it in a fitting observable on a background thread.
fun getProducts() = Single.create<List<Product>> { emitter ->
try {
val response = productApi.getProducts().execute()
if (!response.isSuccessful) {
throw HttpException(response)
}
emitter.onSuccess(response.body()!!)
} catch (e: Exception) {
emitter.onError(e)
}
}.observeOn(Schedulers.io())
When you invoke a RxJava action, for example, a retrofit request you can to tell it where to perform the action and where to get the result the default location is where you subscribe to it
in order to change it you need to add two lines
observeOn(Where you will receive the result)
subscribeOn(Where the action will be executed)
In your case, it should be something like this
productApi.getProducts() // <- This call is a problem (even when I comment out all code below)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io()) //or .subscribeOn(Schedulers.newThread())
.subscribe({Success},{Failure})
I have made a library that has a lot of utilities/extensions for Android development in kotlin.
One of the packages is there to make it simple to avoid this issue.
All you need to do is type:
yourObservable //or any other reactive type
.runSafeOnMain() //it will perform you action in another thread and it will return the result in main
.subscribe({}, {])
I am trying to consume an API from thesportsdb to display lastmatch from specific league. in my recyclerview I want to show the team badge for every teams but when I request the lastmatch API it didn't include the team badge, only the id for each team and if I want to show the badge it require me to request the team profile which includes the url for the team badge.
Since I am new to rxJava so I am still familiarize myself with it. some posts suggest using flatmap but it kind a difficult for beginner like me to implement it.
this is the retrofitService:
interface FootballRest {
#GET("eventspastleague.php")
fun getLastmatch(#Query("id") id:String) : Flowable<FootballMatch>
#GET("lookupteam.php")
fun getTeam(#Query("id") id:String) : Flowable<Teams>
}
I used repository pattern
class MatchRepositoryImpl(private val footballRest: FootballRest) : MatchRepository {
override fun getFootballMatch(id: String): Flowable<FootballMatch> = footballRest.getLastmatch(id)
override fun getTeams(id: String): Flowable<Teams> =
footballRest.getTeam(id)
}
and this is the presenter who make the call and send the data to the view:
class MainPresenter(val mView : MainContract.View, val matchRepositoryImpl: MatchRepositoryImpl) : MainContract.Presenter{
val compositeDisposable = CompositeDisposable()
val requestMatch = matchRepositoryImpl.getFootballMatch("4328")
val requestTeam = matchRepositoryImpl.getTeams()
override fun getFootballMatchData() {
compositeDisposable.add(matchRepositoryImpl.getFootballMatch("4328")
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe{
mView.displayFootballMatch(it.events)
})
}
so far I only show the last match result, but I want also to show the badge team on the list.
You could use a map operator combined with lastElement().blockingGet() for the second Observable for this and then return a Pair of results. A simple example could be as follows:
#Test
public fun test1() {
Observable.just(1)
.map {
// here 'it' variable is calculated already so it can be passed to the second observable
val result = Observable.just(2).lastElement().blockingGet()
Pair<Int, Int>(it, result)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { t -> System.out.println("First : " + t?.first + ", second : " + t?.second) }
Thread.sleep(1000)
}
Output
1 2
If your second Observable depends on the result of the first one then just use the it variable inside the map operator and pass it to whatever place it's needed. So, if using the previous example your code could be converted to this:
override fun getFootballMatchData() {
compositeDisposable.add(matchRepositoryImpl.getFootballMatch("4328").toObservable(
.map {
// here 'it' variable is calculated already so it can be passed to the second observable
val next = matchRepositoryImpl.getTeams(it).toObservable().lastElement().blockingGet()
Pair<Int, Int>(it, next)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe{ t ->
mView.displayFootballMatch(t.first)
mView.displayBadgeTeam(t.second)
})
}
Rather than use a blockingGet operator its probably easier for you to use a flatmap and return all of this data as a single stream.
You could achieve this by combining the flatmap and zip operator. This would look something like the following, where MatchData holds both the FootballMatch data along with the homeTeam and awayTeam data.
data class MatchData(val footballMatch: FootballMatch, val homeTeam: Teams, val awayTeam: Teams)
Your flatmap operation would then need to invoke the getTeams method for both home and away team which can then be combined with the footballMatch data through the zip oprator.
override fun getFootballMatchData() {
compositeDisposable.add(matchRepositoryImpl.getFootballMatch("4328")
.subscribeOn(Schedulers.io())
.flatMap { footballMatch ->
Flowable.zip(
matchRepositoryImpl.getTeams(footballMatch.idHomeTeam),
matchRepositoryImpl.getTeams(footballMatch.idAwayTeam),
BiFunction { homeTeam: Teams, awayTeam: Teams ->
MatchData(
footballMatch = footballMatch,
homeTeam = homeTeam,
awayTeam = awayTeam)
}
)
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
mView.displayFootballMatch(it)
})
}
I'm using RxKotlin to build out my latest Android app, and I've come up against a familiar issue: how to handle network errors in an Rx-like way.
I have a stream set up for search terms against a TextView like this:
searchBar
.queryTextObservable()
.debounce(500, TimeUnit.MILLISECONDS)
.map { it.trim() }
.filter { it.isNotBlank() }
.observeOn(Schedulers.io())
This is a useful way to listen against changes to text input, so I then extend the code to feed the prepared text into a network request (using the Retrofit library with the RxJava extension) to search against:
searchBar
.queryTextObservable()
.debounce(500, TimeUnit.MILLISECONDS)
.map { it.trim() }
.filter { it.isNotBlank() }
.observeOn(Schedulers.io())
.switchMap { search(it) }
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(...)
The problem happens when there's a network error - my entire subscription is cancelled. It seems like I have some options to manage the failure, but none of them seem very clean:
Have an inner observable after text input is complete that makes the network request
Use onErrorResumeNext and pass a sentinel value
This is obviously not exhaustive, but what is the appropriate pattern(s) to gracefully handle network errors while preserving the stream (and hence the usefulness) of user input from the search bar?
Using operators such as onErrorReturn is a pretty standard approach if you look at reactive patterns which provide a unidirectional data flow such as MVI.
Following such patterns you typically map the state of your network call into an object which represents the state of the call.
A simple example without any MVx patterns would look something like below, where the observable from your RXBinding invokes a call to the API, but instead of just returning the data from the API it returns a state object which can then be rendered on screen.
private val disposables = CompositeDisposable()
override fun onStart() {
super.onStart()
disposables.add(
RxView.clicks(load_data_button)
.flatMap { requestData() }
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::renderRequestState)
)
}
override fun onStop() {
disposables.clear()
super.onStop()
}
private fun requestData(): Observable<RequestState> {
return myApi.requestData()
.toObservable()
.subscribeOn(Schedulers.io())
.map<RequestState>(RequestState::Success)
.onErrorReturn(RequestState::Error)
.startWith(RequestState.InFlight)
}
private fun renderRequestState(requestState: RequestState) {
when (requestState) {
RequestState.InFlight -> showProgress()
is RequestState.Success -> showResult(requestState.result)
is RequestState.Error -> showError(requestState.error)
}
}
sealed class RequestState {
object InFlight : RequestState()
data class Success(val result: MyData) : RequestState()
data class Error(val error: Throwable) : RequestState()
}
Hannes Dorfmann wrote a great set of articles on the MVI pattern which utilises this approach.
http://hannesdorfmann.com/android/model-view-intent
http://hannesdorfmann.com/android/mosby3-mvi-1