Blog株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。2021-02-09AndroidアプリにおけるUIの状態保存と復元について調べてみたこんにちは。2020年新卒でAndroidエンジニアの宮本です。クラウド確定申告アプリの開発を担当しています。マネーフォワードでは、全社のAndroidエンジニアが集い、月一で社内勉強会を開催しています。(最近はオンラインにて開催)この勉強会では、毎回Android開発に関するテーマを1つ決めて深堀り、発表・ディスカッションしています。本記事では、2021年1月の勉強会で私が発表した『AndroidアプリにおけるUIの状態保存と復元』について紹介します。はじめにAndroidアプリにおいて、ユーザーが端末の戻るボタンを押してActivityを閉じたり、「最近の画面」でスワイプしてアプリを終了するなどしてActivityの破棄が行われると、ユーザーが行った操作などによって変化したUIの状態は初期化され、再度ActivityやFragmentを開いた際は変更前のクリーンな状態から開始されます。引用)そのため、多くのアプリでは画面表示に必要なデータをAPIから都度取得したり、データベースに保存できるようにしていることが多いでしょう。一方で、AndroidのシステムによってActivityが破棄されてUIの状態が初期化されることもあります。そのため、AndroidフレームワークではUIの状態が保持されることを保証するために、必要なデータを保存・復元するための仕組みが用意されています。今回はどのようなときにシステムによってUIの状態が初期化されるのか、UIの状態を保持・復元するべきケースやその方法について解説します。システムによってUIの状態は初期化されることがあるはじめに、システムがどのようなときにActivityを破棄してUIの状態を初期化するのかについて紹介します。画面回転などによる構成変更が起きたときユーザーはアプリの構成変更が行われた場合でもUIの状態が変わらないことを期待しています。構成変更というのは例えば画面回転やマルチウィンドウモードへの切り替えのことです。このような構成変更が起きるとシステムはActivityを破棄してUIの状態を初期化してしまいます。構成変更のトリガーに何があるかについてはリファレンスに定数の一覧が載っています。引用)リファレンスには構成変更が起きた際にActivityを破棄する理由として以下のように書かれています。これらの構成変更はすべて、アプリケーションが参照するリソースの値に影響を与える可能性があります。そのため、一般的には、構成変更を正しく処理するために、ビューレイアウトや画像などを含むすべてのリソースを再取得する必要があります。AndroidManifest.xml内でアプリ内の全ての画面を縦画面固定にしていたとしても、他の様々な要因で構成変更が起きる可能性があり、そういった際にUIの状態が初期化される場合があることを認識しておくとよいでしょう。アプリのプロセスがシステムによって強制終了されたときユーザーは「最近の画面」等を通して一時的に別のアプリに切り替えた後、元のアプリに再度戻ってきたときもUIの状態が変わらないことを期待しています。システムはユーザーがアプリから離れてActivityが停止している間に、Activityが実行されているプロセスを強制終了する場合があります。このプロセスを破棄する条件ですが、Activityのリファレンスを見ると以下のように書かれていました。あるアクティビティが他のアクティビティによって完全に見えなくなった場合、そのアクティビティは停止または非表示になります。この段階ではまだすべての状態と変数などのメンバ情報を保持していますが、ユーザーにはすでに見えていないので、ウィンドウは隠され、メモリが他の場所で必要になるとシステムによって終了されることがよくあります。システムがメモリ解放を行う際にプロセスを強制終了する場合があるということです。システムがプロセスを強制終了する可能性は、その時点でのプロセスの状態によって異なります。興味がある方は以下の表と公式ドキュメントを参考にしてみてください。引用)UIの状態が初期化されることによって、どのような影響があるかActivityが破棄されてUIの状態が初期化されるとアプリではどのような影響が起こりうるか考えてみました。ME」の入出金明細画面を見てみましょう。ユーザーがある明細の金額を1000円から500円に変更したいとします。アプリを操作して金額を500と入力した後に、もしユーザーがアプリを離れて別のアプリを操作している最中にMEアプリのプロセスが終了されるとどうなるでしょうか。以下に、後述するActivity破棄時の挙動の確認方法を使ってアプリの挙動を確認したgifを示しています。左側のgifはUIの状態を保持する処理を入れた場合、右側のgifはその処理を入れなかった場合の挙動となっています。UIの状態を保持する処理を入れた場合UIの状態を保持する処理を入れなかった場合UIの状態を保持している場合は、再度アプリに戻ってきてActivityが再生成されても、入力した金額の500が残っています。UIの状態を保持していない場合は、入力前の金額の1000に戻ってしまっています。ユーザーはせっかく入力した項目をもう一度入力し直す必要がでてきてしまいます。このような問題が起こりうる可能性があることから、主にユーザーから何か入力をするような機能を持つ画面に関してはUIの状態を保持することをおすすめします。UIの状態を保持する方法では、どうすればUIの状態を保持することができるのでしょうか。公式ドキュメントでは3つの方法が紹介されています。引用)本記事ではViewModelとonSaveInstanceStateメソッドについて紹介します。ActivityやFragmentのライフサイクルを意識したViewModel独自のライフサイクル管理を行っています。そのため、ActivityやFragmentのライフサイクルが終了するまでメモリ上にデータが保持されるので高速にアクセスでき、画面回転などの構成変更が起きたとしてもViewModelは初期化されません。引用)注意する点として、ViewModelはプロセスの終了時に破棄されるため、復帰後に状態を復元したい場合はonSaveInstanceStateメソッドや永続ストレージと組み合わせてデータを保管し、再読み込みできるようにする必要があります。View#onSaveInstanceState()onSaveInstanceStateメソッドはシステムがActivityやFragmentを破棄する際に、復帰後にUIの状態を再読み込みするためのデータを保存する役割を持っています。データの保存はBundleオブジェクトを使ってkey-value形式で行います。データの復元を行うには、onCreateメソッドやonRestoreInstanceStateメソッドから受け取れるBundleオブジェクトから取得することで可能です。注意する点として、Bundleオブジェクトを使ったトランザクションには受け渡しのデータにサイズ制限があるということです。公式ドキュメントによると、Activity単位ではなくアプリのプロセス単位でのトランザクションで1MBと記載されています。TransactionTooLargeExceptionが発生します。以下ではlogcatに警告が表示されるのみです)SavedStateHandleを使ってUIの状態を保存・復元するViewModelでは構成変更に対してもUIの状態を保持できるので画面回転などの心配をする必要がありません。プロセスの終了には耐えうることができないため、通常はonSaveInstanceStateメソッドを使ってUIの状態を保存・復元する必要があります。これを行うにはBundleオブジェクトを使ってデータを保存したり復元する処理が必要で、複雑な実装になってしまいがちです。SavedStateHandleは、そのような複雑なボイラープレートコードを書く必要なく簡単にUIの状態保存・復元が行える仕組みです。onSaveInstanceStateメソッドと同様にkey-value形式で値の保存と取得ができ、この値はシステムによってプロセスが終了された後も保持されます。使用するには、バージョン1.2.0以降のFragment、バージョン1.1.0以降のActivityをプロジェクトの依存関係に追加してください。SavedStateHandleの詳しい使い方については割愛しますが、公式ドキュメントにはサンプルコードも載っているので参考にしてみてください。LiveDataにも対応しているので扱いやすいです。Daggerと併用するには一工夫必要なので、以下の記事を参考に導入するとよいでしょう。Dayan一部のViewは自前で状態を保持する以下のgifはEditTextにおいて、画面回転をしたときに状態が保持されているかを確認したものです。ViewModelを使って値の管理をしたり、onSaveInstanceStateメソッドをoverrideして値の保存は行っていません。構成変更の一つである画面回転が行われているにも関わらず、画面回転後もEditTextに入力したテキストが保持されています。いう仕組みになっているのか、EditTextの親クラスであるTextViewクラスの実装を少し見てみましょう。onSaveInstanceStateメソッドが実装されており、freezesTextというフラグがtrueになっている場合は状態を保持するように実装されています。このように内部でUI状態を保存する処理が実装されているため、入力したテキストが画面回転後も保持されます。freezesTextはEditTextの場合はデフォルトでtrueが設定されているので状態を保持してくれていますが、TextViewはデフォルトでfalseが設定されています。指定すればTextViewも状態保持させることができます)公式ドキュメントにもあるように、Androidフレームワークで提供されているViewはonSaveInstanceStateメソッドとonRestoreInstanceStateメソッドの固有の実装があるので、興味がある方は内部実装を見てみるとよいでしょう。注意点として、EditTextやTextViewのUI状態を保持するためには以下のようにViewに対してidを定義する必要があります。idが定義されていないと状態の保持を行ってくれません。/>実際のアプリではViewModelのメンバ変数に定義しているLiveDataなどを参照していることが多いと思うので、必要に応じてそれらの値をonSaveInstanceStateメソッドやSavedStateHandleを利用して保持するのがよいでしょう。Activityを保持しない」という設定をONにすれば、Activityから離れた際にすぐに破棄されるので、アプリに戻ってきたときにUIの状態がどうなっているか確認できます。Componentを使っているプロジェクトでよくあるのが、一つのActivityに対して複数のFragmentを持っているパターンです。一つのActivityに対して複数のFragmentを持っている場合だと、再生成時にどのFragmentを使えば良いのか知っている必要があります。ComponentではどのようにUIの状態保持を行っているのか、内部実装を見てみましょう。注目するのはNavHostFragmentの実装です。}NavHostFragmentは複数のFragmentを切り替えるためのコンテナのような役割を持っています。ユーザーがアプリを操作して画面遷移するたびにFragmentを切り替えています。しているただのFragmentです。NavHostFragmentにはonSaveInstanceStateメソッドがoverrideされています。ここでは以下の3つのKEYを使ってデータが保存されています。KEY_NAV_CONTROLLER_STATEKEY_DEFAULT_NAV_HOSTKEY_GRAPH_IDここでKEY_NAV_CONTROLLER_STATEをbundleのkeyとして保存している値は、NavController#saveState()の返り値であるBundle型の値です。この値にはNavigatorの状態やバックスタックの情報が入っています。NavHostFragmentのonCreateメソッドでBundleに値があれば復元し、バックスタックの情報をもとに画面を表示していました。おわりにAndroidのシステムによってUIの状態が初期化される場合があること、アプリのUIの状態を保持・復元するべきケースやその方法について解説してきました。社内勉強会での発表後、社内のエンジニアからは以下のコメントを頂きました。保存するのがバランスとしては良さそう。アプリ内のデータやUIの状態を適切に管理して、より良いユーザー体験を提供できるようにしていきましょう。マネーフォワードでは、エンジニアを募集しています。ご応募お待ちしています。updatesを使って最高のアップデート体験を実現するこんにちは。Androidエンジニアのsyarihuです。読者になるこのブログについてはてなブログをはじめよう!moneyforwardDevさんは、はてなブログを使っています。ブログを報告する引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。