痛みのないAndroid画面の回転

画像






重要!

最初は、記事にエラー実装がありました。 エラーを修正し、記事を少し修正しました。



まえがき



各プラットフォームの問題についての真の理解は、別のプラットフォーム/別の言語で記述しようとすると始まります。 そして、iOSの開発に精通した直後に、Androidでの画面回転の実装がどれほどひどいものかを考えました。 その瞬間から、私はこの問題を解決することを考えていました。 その過程で、アプリケーションの書き方を想像することさえできなかった場合でも、リアクティブプログラミングを使い始めました。



そして、最後に欠けていた詳細-データバインディングについて学びました。 どういうわけか、このライブラリはやがて私を過ぎ去り、私が読んだすべての記事(ロシア語、英語)は、私が必要としていることをまったく語っていませんでした。 そして今、私はアプリケーションの実装についてお話したいと思います、あなたが一般的に画面の回転を忘れることができるとき、すべてのデータは各活動のための直接の介入なしで保存されます。



問題はいつ始まりましたか?



1つのプロジェクトで1,500のxml行を含む画面を取得したときに、設計とTKによって、さまざまな条件下で表示されるさまざまなフィールドが大量にあったときに、本当に深刻な問題を感じました。 15種類のレイアウトが判明し、それぞれが表示される場合と表示されない場合がありました。 さらに、値がビューに影響するさまざまなオブジェクトがまだたくさんありました。 画面が回転した瞬間の問題のレベルを想像できます。



可能な解決策



すぐに留保し、私はあらゆるアプローチの教訓を熱狂的に順守することに反対し、パターンの観点からどのように見えるかに関係なく、普遍的で信頼できる決定をしようとします。



リアクティブMVVMと呼びます。 すべての画面をオブジェクトとして表すことができます:TextView-パラメーター文字列、オブジェクトの可視性またはProgressBar-パラメーターブールなど。また、絶対にすべてのアクションをObservableとして表すことができます:ボタンをクリックする、EditTextにテキストを入力するなど。 n ...



ここで、データバインディングに関するいくつかの記事を停止して読むことをお勧めします。幸いなことに、このライブラリにまだ慣れていない場合は、多くの記事がハブにあります。



魔法を始めましょう



アクティビティの作成を開始する前に、すべての魔法が発生するアクティビティとViewModelの基本クラスを作成します。



更新!

コメントで話した後、私は自分の間違いに気付きました。 一番下の行は、私の最初の実装では何もシリアル化されていませんが、画面が回転するとき、そして画面を最小化、最大化するときでもすべてが機能するということです。 以下のコメントで、これが起こる理由を必ず読んでください。 さて、コードを修正し、コメントを修正します。



はじめに、基本的な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); } }
      
      





私は何かがオブザーバブルとして想像できることを言及しましたか? RxBindingライブラリはそれを完璧に行いますが、問題は、EditTextのようなオブジェクトではなく、ObservableFieldのようなパラメーターで直接作業することです。 将来の生活を楽しむには、ObservableFieldから必要なObservable RxJava2を作成する関数を作成する必要があります。



 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); }); }
      
      





ここではすべてが簡単です。ObservableFieldを入力に渡し、Observable RxJava2を取得します。 このために、BaseObservableから基本クラスを継承します。 このメソッドを基本クラスに追加します。



次に、アクティビティの基本クラスを作成します。



 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(); }
      
      





コードについて詳しくコメントしようとしましたが、いくつかのことに焦点を当てます。

画面を回すと、アクティビティは常に破壊されます。 次に、回復時に、onCreateメソッドが再度呼び出されます。 これはonCreateメソッドにあり、データを保存したかどうかを確認した後、データを復元する必要があります。 データはonSaveInstanceStateメソッドに保存されます。



画面を回転させるとき、メソッド呼び出しの順序に興味があり、これは次のようなものです(興味のあること):



1)onDestroy

2)onSaveInstanceState



不要になったデータを保存しないように、チェックを追加しました。



  if (isFinishing())
      
      





実際、isFinishingメソッドがtrueを返すのは、アクティビティでfinish()メソッドを明示的に呼び出した場合、またはOS自体がメモリ不足によりアクティビティを破壊した場合のみです。 これらの場合、データを保存する必要はありません。



アプリケーションの実装



条件付きタスクを想像してください。1つのEditText、1つのTextView、および1つのボタンがある画面を作成する必要があります。 ユーザーがEditTextに数字の7を入力するまで、ボタンをクリックできないようにする必要があり、ボタン自体がクリック数をカウントし、TextViewで表示します。



更新!

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); } }
      
      





更新する

これが最大の問題があった場所です。 開発者の設定で「アクティビティを保持しない」パラメータがオンになるまで、すべてが古い実装でも機能していました。



すべてが正常に機能するためには、ViewModelのParcelableインターフェイスを実装する必要があります。 実装については何も書きません。別のポイントを明確にします。

 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)); }
      
      





データを返しますが、Observableを失います。 したがって、別のメソッドに出力し、すべてのコンストラクターで呼び出す必要がありました。 これは問題に対する非常に迅速な解決策であり、よく考える時間はなく、エラーを示す必要がありました。 誰かがこれをより良く実装する方法についてアイデアを持っているなら、共有してください。



次に、このモデルビューについて記述します。



 <?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>
      
      





さて、今、私たちは活動を書いています:



 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 } }
      
      





アプリケーションを起動します。 ボタンはクリック可能ではありません、カウンターは0を示します。数字7を入力し、必要に応じて電話を回します。2秒後にボタンがアクティブになり、ボタンをつつくとカウンターが大きくなります。 番号を消去し、電話を再びオンにします-ボタンは2秒後にクリックできず、カウンターはリセットされません。



それだけです。データを失うことなく、痛みのない画面回転を実装できました。 この場合、ObservableFieldなどが保存されるだけでなく、オブジェクト、配列、intなどの単純なパラメーターも保存されます。



ここで準備ができて修正されたコード



All Articles