本記事はアジアクエスト - アジアクエスト Advent Calendar 2025 の記事です。
はじめに
今回、Qiitaのアドベントカレンダー枠で何を書こうかな〜と考えた結果、久々に 「Android開発」 に手を出してみることにしました。
どれくらい久々かというと、5年ぶり。完全に浦島太郎状態です。
学生時代は、「Androidアプリ開発、めっちゃ面白い!」と完全に沼にハマり、数本のアプリを実際に作ってGoogle Playストアにリリースまでしていました。
(出していたAndroidアプリ群 - 現在は保守が行えておらず、すべて非公開)

卒業してプログラマーとして就職すると、業務では別の言語(主にPHP)を使うことになり、そこでプログラミング欲が満たされていました。
平日は仕事でコードを書いて、夜はひたすらゲームをしたい!という、生活を送るうちに、Android Studioの存在はすっかり記憶の奥底へ。
「アドベントカレンダーもあるし、久々にAndroidアプリ開発を通して記事を書いてみるか~~~」という軽いノリで、最新のAndroid Studioをダウンロード。
結果、衝撃を受けました。
今のAndroid開発環境(特にJetpack Compose)にキャッチアップした軌跡を、同じようにブランクがある方や、これから始める方にシェアしたくてこの記事を書いています。
5年前のAndroid開発環境(私の記憶)
まずは、約5年前に私が開発を停止した時点の環境を振り返ります。
当時、私は比較的モダンな技術を採用していましたが、それでも現在の環境と比較すると、多くの手間と工夫が必要でした。
開発言語とフレームワーク
言語:Kotlin
すでにJavaからKotlinに移行し、Null安全や拡張関数などの利点を享受していました。
しかし、当時はまだKotlinの機能がフル活用されておらず、新しい非同期処理(コルーチンなど)はまだ一般的ではありませんでした。
アーキテクチャ:MVVM
Googleが推奨し始めたMVVMパターンを採用していました。
ViewModelとView間でデータを連携させるためにLiveDataを使用していましたが、View層でのデータの監視・更新処理に一定のボイラープレートコード(定型文)が必要でした。
UI構築:XMLレイアウトとの戦い
画面構築はXMLレイアウトが基本でした。
レイアウトの複雑化:複雑なUIを作成する際、XMLファイルが長大化し、視覚的な構造の把握やメンテナンスが困難になることが課題でした。
Viewの参照:findViewByIdは避け、View BindingやKotlin Extensions(当時使用可能だった場合)を使って、コードとXML要素を紐づける作業を行っていました。XMLとKotlinコードを頻繁に行き来する必要がありました。
非同期処理とIDE
非同期処理:RxJava
当時、非同期処理を扱う上での主流はRxJavaでした。
強力なライブラリでしたが、Observableやオペレーターの概念を理解する必要があり、学習コストが比較的高かったと言えます。(今ではそんなに覚えてない)
Mac環境で開発していたため、比較的快適に動作していました。
が、ビルド時間の短縮やコード補完の精度など、現在のIDEに比べると機能面で改善の余地がありました。
ブランク後の開発環境(メイン部分)
公式チュートリアルを行った結果、当時の開発における制約や困難が大幅に解消されており、特に以下の2つの進化は大きな衝撃でした。
- Jetpack Compose
- Android Jetpack
Jetpack Compose
画面に「Hello World!」と表示し、その下にボタンを配置するシンプルなレイアウトを考えた際に、XMLでは以下の2ファイルが必要でした。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/greetingText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textSize="24sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="64dp" />
<Button
android:id="@+id/actionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="クリック"
app:layout_constraintTop_toBottomOf="@+id/greetingText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="32dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// View Binding (または findViewById) で参照を取得
val actionButton = findViewById<Button>(R.id.actionButton)
val greetingText = findViewById<TextView>(R.id.greetingText)
actionButton.setOnClickListener {
// イベント処理...
greetingText.text = "ボタンが押されました!"
}
}
}
5年前は「R.id.XXXXがどれだっけ…?」になりながら実装を行っていた記憶があります。
Composeで実装する場合、Kotlinファイル内(MainActivity.kt)のみで完結します。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// 画面全体のUIを定義
AppScreen()
}
}
}
// 画面を構成する Composable 関数
@Composable
fun AppScreen() {
// 状態を保持する変数 (State)
val greetingMessage by remember { mutableStateOf("Hello World!") }
// 縦に要素を並べる (Column)
Column(
modifier = Modifier
.fillMaxSize() // 画面全体に広げる
.padding(64.dp),
horizontalAlignment = Alignment.CenterHorizontally // 中央揃え
) {
// テキスト表示
Text(
text = greetingMessage,
fontSize = 24.sp,
modifier = Modifier.padding(bottom = 32.dp)
)
// ボタン
Button(
onClick = {
// Stateの更新
greetingMessage = "ボタンが押されました!"
}
) {
Text("クリック")
}
}
}
衝撃ポイント
-
XMLファイルの完全消滅
UIの定義がKotlinコード内に統合され、ファイルの切り替えが不要になりました。 -
Stateの宣言
remember { mutableStateOf(...) }で状態を宣言するだけで、その状態(greetingMessage)が更新されると、関連するTextコンポーネントが自動的に再描画されます。
手動でgreetingText.text = ...とする必要がなくなっていました。 -
Modifierの統合
レイアウト属性(パディング、幅、中央揃え)がModifierとしてチェーンで記述でき、XMLの冗長な属性定義から解放されました。
Android Jetpack
ライフサイクル管理、UIテスト、データ永続化などを解決するためにGoogleが提供するライブラリ群です。
特に、ViewModelとHiltに衝撃を受けたので紹介します。
ViewModel
5年前、ActivityやFragment内でデータを保持しようとすると、画面回転などの構成変更でデータが破棄されてしまう問題がありました。(当時はこれが嫌すぎて、画面回転をさせないように固定したりしていた記憶があります)
ViewModelは、この問題を解決するために設計されたコンポーネントで、
UIコントローラー(Activity/Fragment)のライフサイクルとは独立してデータを保持してくれます。
画面回転などでActivityが破棄され、再生成されても、ViewModelインスタンスは生き残り、UIが必要とするデータを保持し続けることができます。
また、UIロジック(Composeの表示処理)とビジネスロジック(データの取得・保持)を分離ができるため、可読性の向上や、テスト性の向上に役立ちそうでした。
<コード上でざっくり解説>
// 1. ViewModelクラスを定義
class CounterViewModel : ViewModel() {
// 状態を保持する(Flow または LiveData を使用)
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
// ViewModel内でビジネスロジックを実行
_count.value = _count.value + 1
}
}
// 2. Compose側で利用
@Composable
fun CounterScreen(
// hiltViewModel() で自動的にViewModelのインスタンスを取得
viewModel: CounterViewModel = hiltViewModel()
) {
// Flowから値を収集し、Composeの状態として利用
val currentCount by viewModel.count.collectAsState()
Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "カウント: $currentCount", fontSize = 32.sp)
Button(onClick = { viewModel.increment() }) {
Text("増やす")
}
}
}
UI側(CounterScreen)からはViewModel側の関数(increment())を呼び出しているのみで、ロジック部分には関与しなくなっています。
この際、ViewModel側にcountを持っているため、Activityが再生成されても値を保持したままにできています。
Hilt
5年前、依存性注入(DI)といえばDagger 2が主流でしたが、コンポーネントの管理が非常に複雑だった記憶があります。
Hiltは、この部分を簡単にしてくれています。
-
@HiltAndroidAppや@AndroidEntryPointといったアノテーションを数カ所に追加するだけで、必要なコンポーネントが自動生成される -
@HiltViewModelいうアノテーションを付けるだけで、ViewModelへの注入が自動で行われる
使い方もアノテーションをつけるだけと非常に簡単で把握がすぐに行えました。
実際の利用例が以下になります。
// コンストラクタに @Inject を付ける
class UserRepository @Inject constructor(
private val apiService: ApiService
) {
// ...
}
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
// Hiltが自動的にUserRepositoryのインスタンスを作成し、ここに注入してくれる
@Inject lateinit var userRepository: UserRepository
// ...
}
小さな壁
チュートリアルを終え、本格的にアプリ開発を始めようとした際、慣れなかったポイントに遭遇したのでいくつか紹介します。
ポイント1. UI操作のパラダイムシフト(命令的 → 宣言的)
XML時代は、View IDを通じて特定のViewオブジェクトを操作するのが当然でした。
例えば、TextViewの表示内容を変更する際には、textView.text = "..." のように直接プロパティを変更していました。
上の方で説明した通り、現在はViewを直接操作するのではなく、UIは状態(State)の関数として定義されることを再認識しました。
Viewのプロパティを変更するのではなく、「mutableStateOfなどで宣言した状態変数を更新することで変更できる」の感覚になかなか慣れませんでした。
val textView = binding.messageText
textView.text = "新しいメッセージです"
val messageState by remember { mutableStateOf("初期メッセージ") }
Text(text = messageState)
// なにかしらのイベント後
messageState = "新しいメッセージです"
この宣言的な考え方に慣れるまでが最初の小さな壁でした。
ポイント2. 依存関係の管理とビルド設定の現代化
技術的な内容ではありませんが、
環境設定の面で、Gradle設定ファイルがGroovyからKotlin DSL(.kts)に移行していました。
さらに依存ライブラリのバージョンを中央管理するVersion Catalog(libs.versions.toml)が主流になっていた点に戸惑いました。
ただ、新しい記法はIDEが手厚くサポートサポートしてくれたため、把握は容易でした。
まとめ
Android開発は「面倒な部分が自動化され、本当にやりたいロジックの実装に集中できる」環境へと劇的に進化していました。
特にComposeは、Flutterに近い感覚もあり、挫折しがちなXMLを気にしなくて良くなったのはすごい衝撃でした。
ブランクがあっても、今のAndroid開発は驚くほど楽しく、チュートリアルをやればある程度の内容が理解できる程度には簡単になったなと感じました。
この記事が、過去にAndroid開発をしていた方、あるいはこれから始めたい方の背中を押すことができれば幸いです。