Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Surviving Screen Rotation without ViewModel: An Experimental Deep Dive into Circuit and Flow

2025年09月13日に「Dev.to」が公開したITニュース「Surviving Screen Rotation without ViewModel: An Experimental Deep Dive into Circuit and Flow」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

Androidアプリで標準のViewModelを使わず、画面回転時もUI状態を維持する実験を紹介。CircuitとKotlin Flowを使い、KMPに適した、ライフサイクル対応かつデータ再取得なしのアーキテクチャを構築した。アプリ寿命のスコープでFlowを共有し、画面回転時のちらつきも防ぐ技術を解説する。

ITニュース解説

スマートフォンアプリ開発において、画面の向きが変わる「画面回転」は、アプリの内部で多くの複雑な処理を引き起こす。Androidアプリでは、この画面回転の際にアプリの状態(例えば、画面に表示されているデータや入力途中の内容)が失われないようにするために、Jetpack ViewModelという標準的な仕組みが広く使われている。ViewModelは、画面が回転しても内部の状態を保持し続け、また不要になった処理を自動的に停止してくれる便利なツールである。

しかし、もしこのViewModelを使わずに、画面回転に強いアプリを構築するとしたらどうなるだろうか。近年、Kotlin Multiplatform (KMP) のように、AndroidだけでなくiOSやWebなど様々なプラットフォームで同じコードを共有する技術が注目されており、特定のプラットフォーム(Android)に特化したライブラリに依存しないアーキテクチャの模索は、ますます重要になっている。この実験は、ViewModelに代わる方法を探ることで、より汎用的で将来性のあるアプリ開発の可能性を追求するものだ。

この実験では、気圧計アプリを例に、以下の目的を達成することを目指した。 一つは、デバイスのセンサーから気圧データを読み取り、画面に表示すること。 次に、画面が回転しても表示中のデータが失われないようにすること。 さらに、アプリの画面が見えていない時にはセンサーの使用を停止し、バッテリーを節約すること。 そして最も重要なのが、画面が回転するたびにセンサーのデータ取得を最初からやり直す(リスナーを再登録する)という無駄な処理を回避することである。

ViewModelが提供する主な機能は二つあった。一つは「インスタンスの存続」で、画面回転のような設定変更があっても、ViewModelのインスタンスが破棄されずに生き残り、内部の状態を保つことだ。もう一つは「viewModelScope」で、これはコルーチン(非同期処理を簡単に記述するための仕組み)を実行するための特別な領域であり、ViewModelが不要になった時にその中のコルーチンも自動的にキャンセルされることで、リソースの無駄遣いを防ぐ。ViewModelを使わない場合、これらの二つの機能を独自の方法で代替する必要があった。その代替として、Circuitというライブラリ、Koinという依存性注入ツール、そしてKotlin Flowという非同期データストリームの仕組みを組み合わせた。

まず、気圧計のセンサーデータを提供する部分、具体的には「BarometricPressureRepository」というデータ層から見ていこう。ここでは、センサーからのデータをKotlin Flowとして公開するが、その公開方法に重要な工夫が凝らされている。通常、Flowはデータを購読する(受け取る)側が増えるたびに、データ取得の処理を最初から開始する「コールドFlow」である。しかし、この実験では、shareInというオペレーターを使って、このコールドFlowを複数の購読者で共有できる「ホットSharedFlow」へと変換した。

shareInを使用する際、scopeとしてアプリケーション全体で生き続ける「CoroutineScope」を指定した。これは、アプリのプロセスが存在する限り活動し続ける「マネージャー」のような役割を担う。そして、startedという引数にはSharingStarted.WhileSubscribed(5000)という特別なルールを設定した。このルールは以下のような動作を指示する。 最初のUIコンポーネントがセンサーデータの購読を開始した時に、初めてセンサーリスナーを登録し、データ取得を開始する。 そして、最後のUIコンポーネントが購読を停止した後も、すぐにセンサーを停止するのではなく、5000ミリ秒(5秒)間待機する。 もしこの5秒間の間に、新しいUIコンポーネントが再びデータを購読し始めた場合、センサーは停止されることなく、そのままデータ提供を継続する。 しかし、5秒間誰も購読しなかった場合に限り、初めてセンサーリスナーを解除し、データ取得処理を完全に停止する。

この「5秒間の待機時間」が、画面回転時の鍵となる。画面が回転すると、一時的に古いUIは購読を停止するが、ほぼ同時に新しいUIが作成されて購読が再開される。この再開は通常5秒以内に行われるため、センサーは一度も停止することなくデータ提供を継続し、結果として画面回転時にセンサーリスナーを再登録する無駄な処理を完全に回避できるのだ。このアプリケーション全体で生きるスコープは、Koinという依存性注入ツールを使って、アプリ起動時に一度だけ作成されるように設定した。

次に、UIに表示するロジックを扱う「Presenter」(これはCircuitというライブラリにおける、ViewModelのような役割を果たすコンポーネントだ)での工夫について見ていこう。ここでの大きな課題は、画面回転時に「Loading」などの状態がちらつくのを防ぎ、ユーザーに途切れることのない体験を提供することだった。

その解決策として、rememberRetainedという機能を使用した。これはCircuitの機能で、画面回転のような設定変更後も、特定の変数の値を記憶し続けることができる。これにより、気圧計の状態(例えば、最後に取得した気圧の値)を画面回転後も保持することが可能になり、ViewModelのインスタンス存続の代替として機能した。

また、データフローを収集するためにcollectAsStateWithLifecycleという機能を使った。これは、UIが画面に表示されている間だけFlowからのデータ収集を自動的に開始し、画面が見えなくなったら自動的に停止するという、リソース効率の良い方法である。これは、ViewModelのviewModelScopeが提供するライフサイクル管理機能の代替となる。

そして、このcollectAsStateWithLifecycleを呼び出す際に、非常に巧妙な工夫が加えられた。それは、initialValueとして先ほどrememberRetainedで保持しておいたbarometerState(最新の気圧値)を渡すことだ。画面が回転して新しいUIが構築される際、collectAsStateWithLifecycleは通常、最初の値として指定されたものを表示する。ここで、最新の気圧値を初期値として与えることで、画面回転直後から以前の気圧値がすぐに表示され、「Loading」のような一時的な表示が挟まれることなく、スムーズに画面が切り替わるのである。新しいデータがFlowから流れてくるまでの間も、ユーザーには常に最新の気圧値が表示され続けるため、途切れることのない体験が提供される。

この実験を通じて、Android固有のJetpack ViewModelに依存しない、しかし非常に堅牢でライフサイクルを意識したアプリのアーキテクチャを構築できることが示された。このアーキテクチャは、Android専用のコンポーネントから解放されるため、将来的にKotlin Multiplatformなどの技術でコードを共有する際にも有利となる。また、センサーのようなシステムリソースを必要に応じて賢く利用・解放するため、バッテリー消費を抑えることができる。さらに、画面回転時にもデータの再初期化や、ちらつきのような不快なユーザー体験を生じさせない「回転に強い」設計を実現した。

この成果は、データを提供する層(リポジトリ)にshareInを使ってデータストリームの管理責任をより多く持たせ、UIロジック層(プレゼンター)でrememberRetainedcollectAsStateWithLifecycleという適切なツールを組み合わせることで達成された。これは、宣言的なUI(UIがどのようにあるべきかを記述する)と、コルーチンによる構造化された並行処理(非同期処理を安全かつ効率的に管理する)の真の力を示すものだ。一見すると複雑に思えるかもしれないが、このアプローチは、アプリの状態管理とライフサイクルへの新しい考え方を提供し、困難な問題を解決するための興味深い選択肢となるだろう。

関連コンテンツ