📜 ⬆️ ⬇️

Turns the screen on Android without pain

image

Important!
Initially, the article had an implementation with an error. Corrected the error, corrected the article a little.

Foreword


A true understanding of the problems of each platform comes after you try to write for another platform / in another language. And just after I got acquainted with the development under iOS, I thought about how terrible the implementation of the screen turns in Android. From the moment I thought about solving this problem. Along the way, I began to use reactive programming wherever possible and can no longer even imagine how to write applications differently.

And so I found out about the last missing part - Data Binding. Somehow this library passed by me in due time, and all the articles that I read (that in Russian, that in English) did not tell exactly what I needed. And now I want to tell you about the implementation of the application, when you can forget about the turns of the screens in general, all data will be saved without our direct intervention for each activation.

When did the problems start?


I really felt the problem, when in one project I had a screen with 1500 lines xml, by design and TK there were a whole bunch of different fields that became visible under different conditions. It turned out 15 different layouts, each of which could be visible or not. Plus there was a bunch of different objects whose values ​​affect the view. You can imagine the level of problems at the time of screen rotation.
')

Possible Solution


Immediately make a reservation, I am against the fanatical adherence to the precepts of any approach, I try to make universal and reliable decisions, regardless of how it looks from the point of view of any pattern.

I will call it MVVM reactive. Absolutely any screen can be represented as an object: TextView - the String parameter, the visibility of the object or ProgressBar - the Boolean parameter, etc ... As well as absolutely any action can be represented as an Observable: pressing a button, entering text into EditText, etc. P…

Here I advise you to stop and read a few articles about Data Binding, if you are not familiar with this library yet, thank you, there are plenty of them in Habré.

Let the magic begin


Before we start creating our activites, let's create base classes for activations and ViewModel'li, where all the magic will take place.

Update!
After talking in the comments, I realized my mistake. The bottom line is that in my first implementation, nothing is serialized, but everything works when the screen is rotated, and even when minimized, maximized the screen. In the comments below, be sure to read why this is happening. Well, I will correct the code and correct the comments to it.

First, let's write the base ViewModel:

public abstract class BaseViewModel extends BaseObservable { private CompositeDisposable disposables; //    private Activity activity; protected BaseViewModel() { disposables = new CompositeDisposable(); } /** *     */ protected void newDisposable(Disposable disposable) { disposables.add(disposable); } /** *       */ public void globalDispose() { disposables.dispose(); } protected Activity getActivity() { return activity; } public void setActivity(Activity activity) { this.activity = activity; } public boolean isSetActivity() { return (activity != null); } } 

Did I already say that anything can be represented as Observable? And the RxBinding library does it perfectly, but the trouble is, we do not work directly with objects such as EditText, but with parameters like ObservableField. To enjoy life even further, we need to write a function that will make an Observable RxJava2 we need from an ObservableField:

 protected static <T> Observable<T> toObservable(@NonNull final ObservableField<T> observableField) { return Observable.fromPublisher(asyncEmitter -> { final OnPropertyChangedCallback callback = new OnPropertyChangedCallback() { @Override public void onPropertyChanged(android.databinding.Observable dataBindingObservable, int propertyId) { if (dataBindingObservable == observableField) { asyncEmitter.onNext(observableField.get()); } } }; observableField.addOnPropertyChangedCallback(callback); }); } 

Everything is simple, we pass the ObservableField to the input and we get Observable RxJava2. That is why we inherit the base class from BaseObservable. Add this method to our base class.

Now we’ll write a base class to activate:

 public abstract class BaseActivity<T extends BaseViewModel> extends AppCompatActivity { private static final String DATA = "data"; //   private T data; //,       ViewModel @Override protected void onCreate(@Nullable Bundle savedInstanceState) { if (savedInstanceState != null) data = savedInstanceState.getParcelable(DATA); //     else connectData(); //  -   setActivity(); //   ViewModel (   Dagger) super.onCreate(savedInstanceState); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (data != null) { Log.d("my", " "); outState.putParcelable(DATA, (Parcelable) data); } } /** *  onDestroy      ,      *     ,    . */ @Override public void onDestroy() { super.onDestroy(); Log.d("my", "onDestroy"); if (isFinishing()) destroyData(); } /** *         DI. *  ,       -   preferences  DB */ private void setActivity() { if (data != null) { if (!data.isSetActivity()) data.setActivity(this); } } /** *   * * @return  ViewModel,      */ public T getData() { Log.d("my", " "); return data; } /** *  ViewModel   * * @param data */ public void setData(T data) { this.data = data; } /** *  ,      Rx */ public void destroyData() { if (data != null) { data.globalDispose(); data = null; Log.d("my", " "); } } /** *  ,  ,        */ protected abstract void connectData(); } 

I tried to comment on the code in detail, but I’ll focus on a few things.
Activate, when you rotate the screen is always destroyed. Then, when restoring, the onCreate method is called again. That's just in the onCreate method and we need to restore the data, after checking whether we saved any data. Data is saved in the onSaveInstanceState method.

When you rotate the screen, we are interested in the order of the method calls, and it is this (that which interests us):

1) onDestroy
2) onSaveInstanceState

In order not to save unnecessary data, we added a check:

  if (isFinishing()) 

The fact is that the isFinishing method will return true only if we explicitly called the finish () method in activation, or the OS itself destroyed the activation due to lack of memory. In these cases, we do not need to save data.

Application implementation


Imagine a conditional task: we need to make a screen with 1 EditText, 1 TextView and 1 button. The button should not be clickable until the user enters the number 7 in EditText. The button itself will count the number of clicks on it, displaying them via TextView.

Update!
We write our ViewModel:

 public class ViewModel extends BaseViewModel implements Parcelable { public static final Creator<ViewModel> CREATOR = new Creator<ViewModel>() { @Override public ViewModel createFromParcel(Parcel in) { return new ViewModel(in); } @Override public ViewModel[] newArray(int size) { return new ViewModel[size]; } }; private ObservableBoolean isButtonEnabled = new ObservableBoolean(false); private ObservableField<String> count = new ObservableField<>(); private ObservableField<String> inputText = new ObservableField<>(); public ViewModel() { count.set("0"); //         setInputText(); } protected ViewModel(Parcel in) { isButtonEnabled = in.readParcelable(ObservableBoolean.class.getClassLoader()); inputText = (ObservableField<String>) in.readSerializable(); count = (ObservableField<String>) in.readSerializable(); setInputText(); } private void setInputText() { newDisposable(toObservable(inputText) .debounce(2000, TimeUnit.MILLISECONDS) //     .subscribeOn(Schedulers.newThread()) //     .subscribe(s -> { if (s.contains("7")) isButtonEnabled.set(true); else isButtonEnabled.set(false); }, Throwable::printStackTrace)); } /** *     */ public void addCount() { count.set(String.valueOf(Integer.valueOf(count.get()) + 1)); } public ObservableField<String> getInputText() { return inputText; } public ObservableField<String> getCount() { return count; } public ObservableBoolean getIsButtonEnabled() { return isButtonEnabled; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(isButtonEnabled, flags); dest.writeSerializable(inputText); dest.writeSerializable(count); } } 

Update
Here and there were the biggest problems. Everything worked and with the old implementation, exactly until such time as in the developer settings do not enable the parameter “Don't keep activities”.

For everything to work as it should, you need to implement the Parcelable interface for the ViewModel. As for the implementation, I will not write anything, just to clarify 1 more point:
 private void setInputText() { newDisposable(toObservable(inputText) .debounce(2000, TimeUnit.MILLISECONDS) //     .subscribeOn(Schedulers.newThread()) //     .subscribe(s -> { if (s.contains("7")) isButtonEnabled.set(true); else isButtonEnabled.set(false); }, Throwable::printStackTrace)); } 

We return the data, but we lose Observable. Therefore it was necessary to display in a separate method and call it in all constructors. This is a very quick solution to the problem, there was no time to think better, it was necessary to point out an error. If someone has an idea how to implement it better, please share it.

Now we will write for this model view:

 <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="viewModel" type="com.quinque.aether.reactivemvvm.ViewModel"/> </data> <RelativeLayout xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.quinque.aether.reactivemvvm.MainActivity"> <EditText android:id="@+id/edit_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint=" " android:text="@={viewModel.inputText}"/> <Button android:id="@+id/add_count_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/edit_text" android:enabled="@{viewModel.isButtonEnabled}" android:onClick="@{() -> viewModel.addCount()}" android:text="+"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/add_count_button" android:layout_centerHorizontal="true" android:layout_marginTop="7dp" android:text="@={viewModel.count}"/> </RelativeLayout> </layout> 

Well, now, we write our activism:

 public class MainActivity extends BaseActivity<ViewModel> { ActivityMainBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_main); // view binding.setViewModel(getData()); // ViewModel,    getData,       } //         ViewModel @Override protected void connectData() { setData(new ViewModel()); //     setData } } 

Run the application. The button is not clickable, the counter shows 0. Enter the number 7, turn the phone as we want, after 2 seconds, in any case, the button becomes active, poke the button and the counter grows. Erase the digit, turn the phone over again - the button will not be clickable after 2 seconds, and the counter will not be reset.

All, we got the implementation of a painless screen rotation without data loss. In this case, not only ObservableField and the like will be saved, but also objects, arrays and simple parameters, such as int.

Ready and corrected code here

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


All Articles