📜 ⬆️ ⬇️

Android Cuvettes, Part 3: SDK and RxJava (Final)

Android SDK and "surprise" - almost twins. You may know by heart the development.android.com , but at the same time continue to tear your hair when you try to do something more abruptly than the form-button-progress bar.
This is the final, third, part of a series of articles on Android's Ditches. In fact, of course there should have been about two dozen of them, but I am too modest. At this time, I finally finish the trouble in the SDK, which I happened to encounter, as well as touch on the now popular technology ReactiveX .
In general, the Android SDK, RxJava, Cuvettes - let's go!
image

Previous parts:


1. Activity.onOptionsItemSelected () is not called when actionLayout is set


Situation

Once I did a test task. It was boring, monotonous and ... old. Very old. PSD from the last century. Well, not the essence. Having finished all the main points, I began to read all the indents (agas, in handles, in a ruler, in the old fashioned way). Things went well until I discovered a nasty menu mismatch in the app and in the PSD . The icon was the same, but the padding is not the same. As an adventurer, I didn’t reduce the icon, but decided to use the MenuItem actionLayout property. Quickly adding a new layout with the parameters I needed and rechecking the indents of the icons on the emulator, I sent the solution and went into the sunset.

Situation

What was my surprise when the answer came (literally): "Editing does not work." By the way, I tested the application in the same way and this way and should not have missed something. The panic intensified and the laconic form of the answer from which it was not clear what exactly did not work ...
... fortunately, it did not take long to search. As it became clear from the title, onOptionsItemSelected () is simply ignored when setting a custom layout .
Why?
image

Since then, I clearly realized that with Android, jokes are bad and even changes in design can lead to changes in the behavior of the application. Well, as always, the solution :
workaround
@Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.main_menu, menu); final Menu m = menu; final MenuItem item = menu.findItem(R.id.your_menu_item); item.getActionView().setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { onOptionsItemSelected(item); } }); return true; } 


')

2. MVC / MVP / MVVM and other beautiful words vs. screen mover


Situation

Perhaps each of us at least once heard of MVC and its relatives. On the MVVM android is not yet built (lying, you can, but so far Beta ), but MVC and MVP are being used extensively. But how? Any android developer is aware that when you rotate the screen, the Activity and Fragment are completely destroyed (and with them a handful of nerves to boot). So how do you apply, for example, MVP and at the same time be able to rotate the screen without harming the presenter ?

Decision

And there are already 3 main solutions:
  1. “Use Fragment.setRetainInstance () everywhere and you will be happy,” or something newbies usually say. Unfortunately, this decision, though it saves at first, but destroys all plans, if necessary, to add a Presenter to the Activity . And it happens. Most often with the introduction of DualPane .
    What is DualPane?
    image

    And also setRetainInstance () has a bug that does not alleviate its benefit. But more on that later.
  2. Libraries, frameworks, etc., etc. Fortunately, there are quite a lot of them: Moxy (the “must read” article on a similar topic) , Mosby , Mortar , etc. Some of them at the same time will keep you nerves when trying to restore the so-called View State .
  3. Well, the approach is "crazy hands" - we create a Singleton , we give it the GetUniqueId () method (let it return AtomicInteger values ​​with increment on call). We create a Presenter and save the previously received ID in the Bundle of the Activity / Fragment ', and the Presenter is stored inside Singleton with access by ID . Is done. Now your Presenter does not depend on the lifecycle (otherwise, it’s in Singleton ) . Don't forget to just delete Presenters in onDestroy () !


3. TextView with a picture


And as usual, one is not a ditch, but advice.
What will you do if you need to do something like this?
Icon labeled
image

If your answer is “PF! What problems? TextView and ImageView in LinearLayout or RelativeLayout ”- then this advice is for you. Oddly enough, TextView has a TextView.drawable {ANY_SIDE} property along with TextView.drawablePadding ! They do exactly what they are supposed to do and no nested layouts to you.
What different TextView.drawable {ANY_SIDE} looks like
image

Honestly, I myself found out about this property relatively recently and accidentally, because it didn’t even occur to me to search TextView properties related to pictures.

4. Fragment.setRetainInstance () allows you to save only direct descendants of Activity ( AppCompat )


Situation

If your father is John Taytor , and your mother is Sarah Connor , and you came from far 2013, then you still have a fresh feeling of hatred for the Fragments that you put in . Indeed, at that time it was quite difficult to cope with their “disobedience” ( tyts , tyts ) and the “code with embedded Fragments ” quickly turned into a “code with crutches”.
At that time I was just beginning to program and, after reading such horrors, I decided not to take the nested Fragments in my hands.
As time went on, I did not use Fragment's nesting , but for some reason all the news of this plan passed me ... And then, suddenly, I came across the news (sorry, I sowed a link) that Fragment'y now all Nested and life in general == fairy tale. And what to say - I believed! Created a project, dashed an example where Fragment's hash Presenters were converted to color (this would immediately determine if the retain worked), launched, turned the screen and ...

AND..?

And I spent the whole weekend searching for the reason why only the first level Fragments (those stored in the Activity itself) are saved. Naturally, the first thing I began to sin on was myself. I rummaged through all the code, starting from the painting code, ending with the eradication of MVP , studied the SDK sources, dug tons of posts on Nested Fragments (and there is such a cloud that even developers feel sorry for), reinstalled the emulator (!) And only by the end of the last weekend found THIS !
For those who are too lazy to read: Fragment.setRetainInstance () keeps Fragment from being destroyed with the help of the FragmentManager - that's all ok. However, for some reason, one of the developers took, and added the line mFragmentManager = null; , and only for Fragment's implementation - so the Activity was fine!
Why, why and how it happened - interesting questions that remain unanswered. This single-line bug already stretches 2.5 versions. In the above link ( for the lazy, it is the same ) describes the workaround on reflection . Unfortunately, for now this is the only way to solve the problem (well, except for the complete copying of the source code to my project, of course). The problem itself is described in more detail on the bug tracker .

ps I do not sell the time machine ┬┴┬┴┤ (・ _├┬┴┬┴

5. RxJava : the difference between observeOn () and subscribeOn ()


Perhaps I'll start with the simplest and most important.
When I first took on the Rx , I was completely unclear about the difference between these methods. From a logic point of view, subscribeOn () changes the Scheduler on which subscribe () is called. But ... from the point of view of another logic, Subscriber inherits the Observer , and what does the Observer do? Observe probably. And this is where cognitive dissonance occurred. Understandings did not introduce either google , or stackoverflow , or even official marbles . But of course, such knowledge is extremely important and it came about after a week or two of errors with Schedulers .
I often hear this question from my acquaintances and sometimes I meet in various forums, so here’s an explanation for those who are still going to be “reactive” or use these operators simply intuitively, without worrying about the consequences:
Code
 Observable.just(null) .doOnNext(v0id -> Log.i("TAG", "0")) //  : computation .observeOn(Schedulers.newThread()) .doOnNext(v0id -> Log.i("TAG", "1")) //  : newThread .observeOn(Schedulers.io()) // io .doOnNext(v0id -> Log.i("TAG", "2"))  : io .subscribeOn(Schedulers.computation()) .subscribe(v0id -> Log.i("TAG", "3")); // -  : io 


I suppose (from my own experience) that the most incomprehensible thing is that everywhere ReactiveX is moving forward with the slogan “Everything is a stream” . As a result, the novice expects that each operator affects only the operators following it, but not the entire stream at all. However, it is not. For example, startWith () affects the beginning of the stream, and finallyDo - at its end.
And as for names, having rummaged in Rx source codes, you find out that the data is generated not by the Observable class (suddenly, yes?), But by the OnSubscribe class. I think it is from here that the confusing name of the operator subscribeOn () .
By the way, I strongly advise beginners, and even experienced connoisseurs, to familiarize themselves with the logos for Frodo logging . Save yourself a lot of time, because debugging the Rx code is another problem.

6. RxJava : Operators and Transformers


Situation

Often it happens that the Rx code grows and you want to somehow reduce it. The method of calling methods in the form of chains is good, yes, but here it has zero reuse - you have to call all the same methods every time doing small things, etc. etc.
Faced with such a need, beginners begin to think in terms of OOP and create, if absolutely everything is bad, static-methods and wrap the beginning of the call chain into it. If time does not end with this approach, it will degenerate into 3-4 wrappers into one Observable .
The real code in one of the real products
 RxUtils.HandleErrors( RxUtils.FireGlobalEvents( RxUtils.SaveToCaches( Observable.defer(() -> storeApi.getAll(filter)).subscribeOn(Schedulers.io()), caches) , new StoreDataLoadedEvent() ) ).subscribe(storeDataObserver); 


In the future, this will bring a lot of problems to those who just want to understand what the code is doing and to those who want to change something.

And now what?

Chain-methods are good because they are easy to read. I advise you as soon as possible to learn how to make your operators and transformers . It is easier than it seems. It is only important to understand that Operator works with a data unit (for example, one onNext () call at a time), and Transformer converts the Observable itself (here you can combine the usual map () / doOnNext (), etc. into one).

All finished with children's games. Let's go to the Tubes.

7. RxJava : Chaos in the implementation of Subscriptions


Situation

So, you are reactive! You tried, you liked it, you want more! You are already writing all the tests on Rx . You rewrite your home project on Rx . You teach your cat Rx . And now the time has come to create the Grail - to build the whole architecture on Rx . You are ready, you breathe often and languidly and ... start ... moooya preheelest

What is it for me?

Unfortunately, the above is just about me. I was so impressed with the power of Rx that I decided to completely revise all of my approaches to writing architecture. You could say I tried to reinvent MVVM through MVP + Rx .
However, I assume the biggest newbie mistake - I decided that I understood Rx .
To understand it well, it is absolutely not enough to write a couple of Rx applications . As soon as a task appears more difficult than to link click and download photos, videos and test data from three different sources, then sudden problems like backpressure will manifest themselves. And when you decide that you know backpressure , you realize that you know nothing about the Producer (which even has no normal documentation) ... Something I digress (and at the end of the article it will be clear why).
In general, the essence of the problem is again in logic, which runs counter to what is in reality.
How does listening usually happen?
 //... data.registerListener(listener); // data.mListener == listener //... data.unregisterListener(); // data.mListener == null 


That is, the data source stores a reference to the listener.
But what happens in Rx? (Carefully, pieces of code trash will go now)
observer.unsubscribe () after 500ms

Code
 Observable.interval(300, TimeUnit.MILLISECONDS).map(i -> "t1-" + i).subscribe(observer); l("interval-1"); Observable.interval(330, TimeUnit.MILLISECONDS).map(i -> "t2-" + i).subscribe(observer); l("interval-2"); Observable.timer(500, TimeUnit.MILLISECONDS).map(i -> "t3-" + i).subscribe(ignored -> observer.unsubscribe()); 


Result
 interval-1 interval-2 t1-0 t2-0 


I guess this is the most expected result. Yes, in our class Subscriber (also known as Observer ) stores references to data sources, and not vice versa, so everything calms down after the first unsubscribe (just to be on the safe side , I ’m reminding you that unsubscribed is one of the end states in Rx that you can't get out of how to recreate anything and everything).

subscription1.unsubscribe () after 500ms

And now let's try to unsubscribe from Subscription , not from Subscriber . From a logical point of view, the subscription should bind the Observer and Observable as 1: 1 and allow you to selectively unsubscribe from something, but ...
Code
 Subscription subscription1 = Observable.interval(300, TimeUnit.MILLISECONDS).map(i -> "t1-" + i).subscribe(observer); l("interval-1"); Observable.interval(330, TimeUnit.MILLISECONDS).map(i -> "t2-" + i).subscribe(observer); l("interval-2"); Observable.timer(500, TimeUnit.MILLISECONDS).map(i -> "t3-" + i).subscribe(ignored -> subscription1.unsubscribe()); 


Result
 interval-1 interval-2 t1-0 t2-0 


... suddenly the result is exactly the same. I learned this far from the very beginning of my acquaintance with Rx , although I used a similar approach for a long time thinking that it works. The fact is that Subscriber implements the Observer interface and ... Subscription . Those. that Subscription that we have is the same Observer ! Here is such a turn.

Observable.defer () and Observable.fromCallable ()

I think defer () is one of the most frequently used operators in Rx (somewhere on par with Observable.flatMap () ). Its task is to postpone the initialization of the Observable data until it is called subscribe () . Let's try:
Code
 Observable.defer(() -> Observable.just("s1")).subscribe(observer); l("just-1"); Observable.defer(() -> Observable.just("s2")).subscribe(observer); l("just-2"); observer.unsubscribe(); Observable.defer(() -> Observable.just("s3")).subscribe(observer); l("just-3"); 


Result
 s1 just-1 s2 just-2 s3 just-3 


"So what? Nothing unexpected, you say. "Probably" - I will answer.
But what if you are tired of writing Observable.just () ? In Rx, and there is an answer. A quick search in Google finds the Observable.fromCallable () method, which allows defer'it not Observable , but the usual lambda. We try:
Code
 Observable.fromCallable(() -> "z1").subscribe(observer); l("callable-1"); Observable.fromCallable(() -> "z2").subscribe(observer); l("callable-2"); observer.unsubscribe(); Observable.fromCallable(() -> "z3").subscribe(observer); l("callable-3"); 


Result (ATTENTION! Remove children and hamsters from the screen)
 z1 callable-1 callable-2 callable-3 


It would seem that a method that does the same thing only with other initial data, but such a difference. The most incomprehensible (if you think logically) as a result is that he is not z1-z2-callable ... (if you believe everything described up to this point), but z1-callable .... What's the matter?

The fact is that...

And now to the point. The fact is that many operators are written differently. Someone before the next onNext () checks Subscriber's subscription, someone checks it after emit, but until the end of onNext () , and someone before and after, etc. This brings some ... chaos to the expected result. But even this does not explain the behavior of Observable.fromCallable () .
Inside Rx there is a class SafeSubscriber . This is the class that is responsible for the main contract Rx (well, the one that says: “after onError (), there will be no more onNext () and there will be a reply, etc., etc.”). And whether it is necessary to use it ( SafeSubscriber ) in the operator or not is not stated anywhere. In general, Observable.fromCallable () calls the usual subscribe () , so SafeSubscriber is implicitly created and unsubscribe () occurs after the emith, but Observable.defer () calls unsafeSubscribe () , which does not cause unsubscribe () at the end. So actually (suddenly!) This is Observable.defer () bad, not Observable.fromCallable () .

8. RxJava : repeatWhen () instead of manual unsubscribe / subscribe


Situation

You need to update the data every X-seconds. Downloading new data, of course, cannot be done until old ones are loaded (this is possible due to lags, bugs and other mischief). What to do?
And in the answer everyone begins: Observable.interval () with Observable.throttle () or AtomicBoolean , and some even through manual unsubscribe () manage to make. In fact, everything is much simpler.

Decision

Sometimes it seems that Rx has operators for all occasions. So it is now. There is a repeatWhen () method that does everything for you - re-subscribes to the Observable at the specified interval:
Example use repeatWhen ()
 Log.i("MY_TAG", "Loading data"); Observable.defer(() -> api.loadData())) .doOnNext(data -> view.setDataWithNotify(data)) .repeatWhen(completed -> completed.delay(7_777, TimeUnit.MILLISECONDS)) .subscribe( data -> Log.i("MY_TAG", "Data loaded"), e -> {}, v0id -> Log.i("MY_TAG", "Loading data")); // "Loading data" -   ; "Data loaded" -    ~8 . 


The only negative is that at first it is not entirely clear how this method works at all. But as usual, here is a good article on repeatWhen () / retryWhen () .

retryWhen

By the way, besides repeatWhen () there is also retryWhen () , which does the same, but for onError () . But unlike repeatWhen () , situations where retryWhen () can be useful are quite specific. In the case described above, it might be possible to add it. But in general, it is better to use Rx Plugins / Hooks and hang the global handler for the error of interest. This will not only re-subscribe to any Observable in case of an error, but also notify the user about it (I use something similar for a SocketTimeoutException for example).

Extra. RxJava : 16


Finally, that’s why I started writing about the Cuvettes. The problem I devoted to 2 weeks of my life and still have no idea what the ... magic is going on there ... But let's order.

Situation

You need to make an authorization screen, checking for incorrectly filled fields and issuing a special warning for every 3rd error.
The task itself is not difficult, and that is why I chose it as a “test site” for Rx . Thought, I will solve, I will look, as Rx behaves in business, other than simple data jump from the server.
So, the code was something like:
Login error code
 PublishSubject<String> wrongPasswordSubject = PublishSubject.create(); /*...*/ wrongPasswordSubject .compose(IndexingTransformer.Create()) .map(indexed -> String.format(((indexed.index % 3 == 0) ? "GREAT ERROR" : "Simple error") + " #%d : %s", indexed.index, indexed.value)) .observeOn(AndroidSchedulers.mainThread()) .subscribe(message -> getView().setMessage(message)); 


Code processing button [Sign In]
 private void setSignInAction() { getView().getSignInButtonObservable() .observeOn(AndroidSchedulers.mainThread()) .doOnNext((v) -> getView().setSigningInState()) //    .observeOn(Schedulers.newThread()) .withLatestFrom(formDataSubject, (v, formData) -> formData) .map(formData -> auth(formData.login, formData.password)) // .   WrongLoginOrPassException .lift(new SuppressErrorOperator<>(throwable -> wrongPasswordSubject.onNext(throwable.getMessage()))) //      .compose(new UnObservableTransformer<>()) //       flatMap().      .observeOn(AndroidSchedulers.mainThread()) .subscribe(user -> getView().setSignedInState(user)); // happy end } 


Postpone claims to the Rx- style code - everything is bad, I know myself. The point is not that, and it was written a long time ago.
So, getView (). GetSignInButtonObservable () returns the Observable received from RxAndroid 'for clicking the [Sign In] button. It is hot-observable , i.e., it will never be able to be completed . Events start from it, pass through map () , in which authorization takes place and further along the chain. If an error occurs, the custom Operator will intercept the error and simply will not miss it further:
SuppressErrorOperator
 public final class SuppressErrorOperator<T> implements Observable.Operator<T, T> { final Action1<Throwable> errorHandler; public SuppressErrorOperator(Action1<Throwable> errorHandler) { this.errorHandler = errorHandler; } @Override public Subscriber<? super T> call(final Subscriber<? super T> subscriber) { return new Subscriber<T>(subscriber) { @Override public void onCompleted() { subscriber.onCompleted(); } @Override public void onError(Throwable e) { errorHandler.call(e); //  ,    } @Override public void onNext(T t) { subscriber.onNext(t); } }; } } 


So the question is. What's wrong with this code?
If they asked me about it, I would even now answer: “everything is OK”. Well, except that memory leaks, because nowhere is there a save Subscription . Yes, only onNext is overwritten in subscribe , but other methods will never be called. All right, working on.

Pain

Outset

And here the strangest begins. The code really works. However, I am a meticulous person and therefore I decided to press the authorization button ... many times. And, quite suddenly, I discovered that for some reason, after the 5th “GREAT ERROR”, the authorization progress bar (which was delivered via setSigningInState () ) did not appear (this function also turns off the [Sign In] button).
"Hmm" - I think. Rechecked the functions in Fragment 'e, responsible for the UI (all of a sudden there is something wrong inserted). I revised the auth () function, maybe I set the timeout for the tests. Not. Everything is good.
Then I decided that this is a race of threads . I started it again and checked it again ... Exactly 5 “GREAT ERROR” and again the stagnation of endless progress bar. And here I am tensed. Launched again, and then another and another. Exactly 5! Each time, exactly after the 5th “GREAT ERROR”, the button stops responding to pressing, the progress bar turns and silence.
“Okay” - I decided, “I will remove the setSigningInState () . You never know, Android loves to play with people. Suddenly, there was something in the SDK that broke and the whole thing was just that I couldn’t press the button again, and not that its handler didn’t work. ” Not. Did not help.
By this moment I was already very tense. In LogCat is empty, there were no errors, the application is running and has not hung. Just the handler is no longer processing.

Analysis

It turned out that the task itself had deceived me. I considered the number of "GREAT ERROR", but in fact it was necessary to count the number of button presses. Exactly 16. The number has changed, but the situation remains.
So, the code for the next attempt after getting rid of all unnecessary:
Code with logs in doOnNext ()
 private void setSignInAction() { getView().getSignInButtonObservable() .observeOn(AndroidSchedulers.mainThread()) .doOnNext((v) -> l("1")) .observeOn(Schedulers.newThread()) .doOnNext((v) -> l("2")) .map(v -> { throw new RuntimeException(); }) .lift(new SuppressErrorOperator<>(throwable -> wrongPasswordSubject.onNext(throwable.getMessage()))) .doOnNext((v) -> l("3")) .observeOn(AndroidSchedulers.mainThread()) .doOnNext((v) -> l("4")) .subscribe(user -> runOnView(view -> view.setTextString("ON NEXT"))); } 


And here the situation has become even stranger. From 1 to 15 clicks went right, the digits “1” and “2” were displayed, but for the 16th time the last line in the logs was ... “1”! It just did not reach the error generator!
"So maybe it's not at all in Exception ?!" - I thought. throw new RuntimeException() return null … , 4 (, 100 , … ).
2 3 , :



ReactiveX . RxJava , , wiki, , … « ».
, , : onBackpressureBuffer() . backpressure wiki RxJava' , , , .
, . backpressure , , . — zip() . 1 , — 1 , zip() . onBackpressureBuffer() — , , , zip() ( OutOfMemoryException , ).
, onBackpressureBuffer() ? , . [Sign In] only once a minute (well, you never know, what if I click The Flash and click too fast?). Of course it did not help.

The final

, , , observeOn() . « ?» — . " ¯\_(ツ)_/¯ " — .
, onBackpressureBuffer() Observable . OnSubscribe -, Producer … . , Rx , , , — , — .
stackoverflow , .
2 , onBackpressureBuffer() ( , , , ?).

, , observeOn() Subscriber - Subscriber Exception' , ( Exception , , 16). 17 , observeOn() isUnsubscribed() , .. true , . ( ).
16 — Backpressure Buffer Android' . Java 128 , , . , 16 - , 5 — . 16 , 2+2=17.
, , — SuppressErrorOperator . , MissingBackpressureException . - . , — SuppressErrorOperator , MissingBackpressureException . Since , ( 16 [Sign In] ).

Conclusion


. , Rx — Loader' . Netflix .
, Rx : . , , — . - . Rx — , . Rx- . (, Retrofit- ), Rx, , Subscription .. ( - View State Backpressure Producer. . ). , , .
, Rx, , : (Ctrl+F - Scan ), RxJava wiki github' ( - ) .
ps - , — , . , .

UPD : 16 stackoverflow akarnokd ( RxJava , artemgapchenko ). , observeOn() decople' , backpressure buffer . Since Exception request() , «» , observeOn() , — 16 . onBackpressureBuffer() , Long.MAX_VALUE . akarnokd' .

Source: https://habr.com/ru/post/281312/


All Articles