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!

Previous parts:
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 .
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; }
')
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:
- “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 .
And also setRetainInstance () has a bug that does not alleviate its benefit. But more on that later. - 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 .
- 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?
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 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.
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 ┬┴┬┴┤ (・ _├┬┴┬┴
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"))
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.
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 preheelestWhat 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? 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.
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"));
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 () .
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())
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);
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 , :
- 16
- Exception
- - doOnNext() «2», Exception
…
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' .