はじめに
はじめまして、@KiYo-GSと申します。
株式会社 現場サポートで唯一のモバイルエンジニアを名乗らせて貰っています。が、実際にはほとんどAndroidエンジニアです(;'∀')
本記事は社内でエンジニア向けに発表したLTをまとめ直したものです。Android開発にあまり詳しくない方向けに興味を持って貰おうということをテーマにしました。
さて、本題です。現在、Android開発はReactやVue.jsのようなモダンな宣言的UIによる開発ができるようになりました。ですが、そこに至るまでには紆余曲折がありました。
本記事では、Android開発の歴史を振り返ることで、現在のAndroid開発がいかに便利になっているかを感じとって貰いたいと思っています。
ところで、Androidは「画面回転で即破棄される」「メモリ不足で突然死する」といった制約が多く、その結果、Webよりもはるかに「UI状態をどう保持するか」が重要でした。
そこで、この記事ではその課題がどのように解消されてきたかをライフサイクル・状態管理・UIの分離 の観点で振り返ります。
1. 黎明期:God Activity
Android開発の黎明期は、ActivityとXMLという2つの要素を軸にアプリを作っていました。
Activityとは
Activityは画面を管理するコントローラーの役割を担うクラスです。一般的に、各画面ごとに1つのActivityを作っていました。
当時の開発スタイルは以下のようなものでした。
-
UI
XMLでレイアウトを作成し、Activityから呼び出していました。 -
UIロジック
Activity(Java)からXML内のViewのidを紐づけ、ActivityにUIロジックを記述していました。
Activity時代の課題
この時代には、次のような課題がありました。
-
UIの使い回しが難しい
XMLとActivityが密接に結びついていたため、UIを使い回すことが困難でした。 -
God Activity(神クラス)
UIロジックはもちろん、ビジネスロジック(通信処理やDB操作など)や状態管理(ボタンのオン・オフの情報などのUIのプロパティ)など、すべてをActivityに記述していました。その結果、Activityが肥大化し、まるで神のようだということでGod Activityと呼ばれました。 -
ライフサイクルの管理が煩雑
ライフサイクルとは、インスタンスが生成され、ユーザーが操作可能になり、最終的に破棄されるまでの一連の過程です。その過程の節目で特定のメソッドが呼び出されます(参考: アクティビティのライフサイクル)。
例えば、画面を回転させるだけでActivityはすぐに死に(破棄され)、UI状態を破棄します。その後、システムが自動的に新しいActivityインスタンスとして復活させるのですが、別インスタンスのため、手動でUI状態の保存と復元をしなければなりませんでした。
2. 発展期:UIコンポーネント化(Fragment)と「ライフサイクル地獄」
Activityの課題に対して、Fragmentというクラスが登場しました。
Fragmentの役割
FragmentはActivityと同様にXMLを呼び出してUIを作成できます。
-
コンポーネント化
ActivityからFragmentを呼び出すことで、UIをコンポーネント化し複雑なレイアウトを分割できるようになりました。また、画面間でコンポーネントを使い回したり、1つの画面内に複数のコンポーネントを配置したりすることが可能になりました。 -
God Activityの解消
ロジックや状態管理を分割されたFragmentへと委譲することで、Activityの肥大化が解消されました。
始まった「ライフサイクル地獄」
しかし、Fragmentの登場はライフサイクル地獄の始まりでもありました。
-
複雑怪奇なライフサイクル
次の図を見てみてください。

引用: https://github.com/xxv/android-lifecycle図の右側はActivity、左側はFragmentのライフサイクルイベントを描いたものです。ライフサイクルイベントや矢印が多すぎて、ライフサイクル地獄という言葉が直感的に分かっていただけるのではないでしょうか?当時はこのような図とにらめっこしながら実装をしていました。
-
複雑な状態管理
Fragmentは「インスタンスは残るが View だけ破棄される」という特殊なライフサイクルを持ち、Activityとはタイミングが一致しません。そのため「今このビューは存在するか?」を常に意識してコードを書く必要がありました。
3. 転換期:状態管理クラス (ViewModel) の登場
ライフサイクル地獄の解決策として、UI状態などの変数を保持するViewModelが登場しました。
ViewModelのメリット
-
長いライフサイクル
Activityは画面回転などですぐ死にUIの状態を破棄しましたが、ViewModelはその最中も生存し続け、変数を保持します。ただし、ViewModelが変数を保持し続けるのは画面回転などの構成変更によるActivityの死に対してだけであり、メモリがひっ迫した際のシステムによるプロセス破棄(突然死)には対応できません。
-
コードの整理
ViewModelが「UIに表示するためのデータ」と「その加工ロジック」を担うことでコードが整理されました。
ViewModelの登場より少し前にKotlinとXMLを紐づけるdatabindingや監視可能な変数LiveDataが登場し、MVVM(Model-View-ViewModel)アーキテクチャが主流となりました。それまでは開発者が独自実装し、MVC(Model-View-Controller)やMVP(Model-View-Presenter)など、どのアーキテクチャが良いのかといった議論がありましたが、Googleが一つの正解を示した格好になります。
なお、ネットワーク処理やDB操作といったビジネスロジックはRepositoryに配置するのが推奨され、これにより ViewModel の肥大化を防げます。
残された課題
ライフサイクルの課題を解決したViewModelの登場は革新的で、個人的には、この時代にAndroid開発は一つの完成形にかなり近づいたと感じています。
ですが、まだ課題はありました。
-
KotlinとXMLの混在
状態管理やロジックはKotlin、UIはXMLと記述方法が異なり、可読性が低いため状態を追いにくく、バグの温床になっていました。
例えば『エラー時にメッセージを表示する』という単純な制御です。 Kotlin側の状態はBooleanですが、UIは表示制御をInt(View.VISIBLEやView.GONEといった定数)で行う必要がありました。 この型の不一致を埋めるために、XMLの中にdatabindingの機能を使って三項演算子などの変換ロジックを書く必要があり、「ただ表示・非表示を切り替えるだけ」のためにコードが複雑化・分散していました。 -
ViewModelの肥大化
UIで完結するはずの軽微な状態や、複数のUI操作が絡む表示制御など、XMLでは扱いづらいロジックまでViewModelに寄せざるを得ませんでした。その結果、ViewModelの責務が増えすぎて肥大化していました。 -
ライフサイクル問題
Fragmentのライフサイクルの複雑さやActivityとの二重管理といった問題はいまだに残っていました。
4. さらなる転換期:宣言的UI (Jetpack Compose) の登場
いよいよReactやVue.jsと同じ宣言的UIのフレームワーク、Jetpack Composeの登場です。
Jetpack Composeはこれまでの開発環境を大きく変え、たくさんのメリットをもたらしました。
Jetpack Composeのメリット
-
KotlinでUIを記述
KotlinでUIを記述できるようになって可読性と保守性が向上しました。- KotlinとXMLを行き来する必要がなくなり、頭の切り替えが不要となりました。
- XMLによるViewのid管理が不要となりました。間違った記述をしている場合はコンパイルエラーとなるため、ミスが減りました。
- UIの作成は
Composableという特殊な関数で定義するのですが、UIの状態を引数に取ることができ、どのような変数に依存しているのかが分かりやすくなりました。
-
UIの自動更新(リコンポジション)
状態を監視し、変更があったときだけUIを自動更新します。- 自動更新するため、更新し忘れといったミスが減りました。
- 状態の変更があった部分だけを更新する(リコンポジション)ため、パフォーマンスが向上しました。
-
状態を持てる
rememberという仕組みにより、状態をUI側で持てるようになりました。- 状態とともにUIで完結するロジックもUI側に記述できるため、ViewModelの肥大化が緩和されました。
- 画面回転などをしても状態を保持し続ける仕組み(
rememberSaveable)があり、ライフサイクルを気にする必要がなくなりました。rememberSaveableを使えば、ViewModelではできなかったシステムによるプロセス破棄(突然死)時の状態保存・復元も、裏側で自動的にできるようになりました。
※ 実務では状態は基本的にViewModelで持ち、UI側で持つ状態はUIで完結する軽量なデータに限定することが推奨されます。
-
コンポーネント化が容易
コンポーネント化が容易で、コンポーネントを組み合わせて大きなUIを作ることが簡単になりました。- Fragmentによるコンポーネント化やカスタムViewの作成は手間が大きかったですが、格段に簡単になりました。
- コンポーネントのためのクラスであるFragmentが基本的に不要になりました。
-
Single Activity
Jetpack Composeより以前から推奨されていたSingle Activityアーキテクチャがより簡単に実装できるようになりました。- Jetpack Composeと
Navigation ComponentというAPIとの組み合わせにより、Single Activityアーキテクチャがより簡単に実装できるようになりました。 - 画面ごとにActivityが必要だった時代から変わり、1つのActivityだけで済むようになりました。
- Fragmentが基本的に不要になったことと合わせ、ライフサイクル周りの複雑さは一気に減りました。
- Jetpack Composeと
実際のコードの比較
最後に、カウンターアプリ(ボタンを押すと数字が増える)の実装で、黎明期の実装とJetpack Composeがどう違うのかを比較してみます。
Before: God Activity時代
UI(XML)とロジック(Kotlin)が分断されています。Kotlinの側では、XMLからViewのidを紐づけるため、findViewByIdを大量に書く必要があります。また、「変数を変えたら、画面の文字も手動で更新する」というUIの更新命令が必要です。加えて、保存と復元のメソッドをオーバーライドし、キーを使って手動で管理する必要があります。
コードを確認する(記述量が若干多いです)
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/countText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"/>
<Button
android:id="@+id/countButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Count Up"/>
</LinearLayout>
class CounterActivity : AppCompatActivity() {
private val KEY_COUNT = "count_key"
private var count = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)
// Viewを取得(idで紐付け)
// 実際には大量に書く必要がある
val countText = findViewById<TextView>(R.id.countText)
val countButton = findViewById<Button>(R.id.countButton)
// 【復元処理】
// アプリが再生成された場合、Bundleから取り出してUIにセットし直す
if (savedInstanceState != null) {
count = savedInstanceState.getInt(KEY_COUNT, 0)
countText.text = count.toString()
}
// イベントリスナーを設定
countButton.setOnClickListener {
count++
// 【重要】自分でUIを更新する命令を書かないといけない
countText.text = count.toString()
}
}
// 【保存処理】
// 画面回転やプロセス破棄の直前に呼ばれる
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(KEY_COUNT, count)
super.onSaveInstanceState(outState)
}
}
After: Jetpack Compose時代
UIとロジックを同じKotlin内に書くことができます。状態を監視して自動でUIを更新するため、idの紐づけや更新命令が不要です。また、rememberSaveableを使うことで保存と復元の記述も不要です。※実務では状態やロジックはViewModelに書きますが、説明を簡単にするためUIに記述しています。
class CounterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CounterScreen()
}
}
}
@Composable
fun CounterScreen() {
// 状態の定義
var count by rememberSaveable { mutableStateOf(0) }
Column {
// 【重要】状態を表示。countが変われば自動で再描画される
Text(text = "$count")
// イベント定義
Button(onClick = { count++ }) {
Text("Count Up")
}
}
}
比較すると、Jetpack Composeの方がスッキリして見やすくなっていることがよく分かるのではないでしょうか?
まとめ
Android開発の歴史を振り返ると、ライフサイクル・状態管理・UIの分離の問題が徐々に解消されてきたことがわかります。
-
黎明期:God Activity
Activityに全てを書いていました。 -
発展期:UIコンポーネント化(Fragment)と「ライフサイクル地獄」
UIのコンポーネント化によりGod Activityが解消されましたが、ライフサイクル地獄が始まりました。 -
転換期:状態管理クラス (ViewModel) の登場
ライフサイクル地獄が解消され、コードが整理されました。 -
さらなる転換期:宣言的UI (Jetpack Compose) の登場
状態管理が洗練され、ライフサイクルを気にせずに済むようになりました。
現在ではモダンな宣言的UIが導入され、宣言的UIに慣れ親しんだWebエンジニアの方をはじめ様々な方にとって格段に書きやすくなっていると思います。
ご興味あれば、ぜひAndroid開発を始めてみてください!
参考

