🚋

東急線アプリ Android版の現在とこれから 2025

に公開

東急線アプリのAndroid版の開発を担当しているkazyです。同じくAndroidを担当しているKyokoと共同で執筆しています。
本記事では東急線アプリ Android版の現在の採用している技術についてご紹介します。

これまで東急線アプリの開発体制や採用している技術については、外部に十分お伝えできていませんでした。その点を踏まえ、本記事では東急線アプリにおける開発の全体像についてご紹介できればと思います。

東急線アプリとは

東急線アプリは、東京・神奈川を中心とした首都圏を走行する東急電鉄・東急バスの運行情報をリアルタイムで提供する公式アプリです。
2022年に東急URBAN HACKS(東急のDX推進 / 内製化組織)によってリニューアルされて現在の開発体制となりました。

モバイルアプリの開発体制

開発チームには現在Androidが2名、iOSが1名のモバイルエンジニアが在籍しています。どちらのプラットフォームもネイティブアプリとして独立したコードベースで開発されています。

Androidアプリのプロジェクト環境

項目 バージョン
targetSdkVersion 36
minSdkVersion 30
Kotlin 2.2.21
AGP 8.13.1

東急線アプリでは、targetSdkVersion を最新の状態に保つため、毎年計画的に更新を行っています。あわせて minSdkVersion についても OS バージョンの利用状況を踏まえながら、定期的に見直しています。

モジュール構成

東急線アプリは、現在 app module のみで構成されたシングルモジュール構成を採用しています。
モジュール構成については、アプリの規模や開発人数などを踏まえ、状況に応じて適切な選択を行うことが重要だと考えています。

これまでの開発フェーズにおいては、シングルモジュール構成で十分に開発を進めることができており、マルチモジュール化の優先度は高くありませんでした。

一方で、今後数年先を見据えるとアプリの規模拡大が見込まれるため、将来的に機能ごとに feature module で開発できる体制を整えることを目的として、core module の切り出しを計画しています。

Jetpack Compose 全面移行

開発当初はAndroidView(XML + Fragment)で開発されていた東急線アプリでしたが、継続的なリファクタリングにより、99%のコンポーネントがJetpack Composeへ移行完了しました。
現在は画面遷移をFragmentベースのJetpack NavigationからNavigation 3への置き換え作業に鋭意取り組んでいます。
Jetpack Composeに全面移行出来たことで後述のAdaptive Layoutへの対応も取り組みやすくなりました。

Adaptive Layout

リリース当初から東急線アプリは縦画面固定でしたが、Android 16で orientation: portrait が一部廃止された[1]ことをきっかけに、少しずつAdaptive Layout対応を進めています。

現在はコンテンツの最大幅を600dpに制限してコンテンツを中央寄せすることで、最低限の可読性を担保しています。

今のところ最低限の対応に留まっていますが、今後はNavigation 3のSceneやMaterial 3 AdaptiveのNavigationSuiteScaffoldを活用して、タブレットや折りたたみ端末をはじめとした様々な端末で快適に利用していただけるように改善を計画しております。

アーキテクチャ

東急線アプリでは、Android公式のアーキテクチャガイド をベースにしつつ、プロジェクトの特性に合わせて独自のパターンを取り入れています。本章ではその中でも特徴的な点をいくつかご紹介します。

UiState

画面の状態は sealed interface で定義したUiStateに集約します。基本的にViewModelからUiStateだけが状態として公開される設計を採用していますが、複雑な画面ではリアルタイムな列車の走行位置を管理するStateやボトムナビゲーション内のコンテンツを扱うStateなどのようにStateを分割するケースも許容しています。

sealed interface FooUiState {

    @Stable
    data class Idle(
        val items: List<Item>,
        val isLoading: Boolean = false,
        val isRefreshing: Boolean = false,
    ) : FooUiState

    data class Error(val throwable: Throwable) : FooUiState
}

buildUiState

ViewModel内ではそれぞれの状態をStateFlowで定義しています。複数のStateFlowを結合してCompose側にUiStateとして公開するためにbuildUiState 関数を使用しています。これはDroidKaigi 2023のカンファレンスアプリ[2]の実装を参考にしています。

@HiltViewModel
class FooViewModel @Inject constructor() : ViewModel() {
    private val _items = MutableStateFlow<List<Item>>(emptyList())
    private val _isRefreshing = MutableStateFlow(false)

    val uiState = buildUiState(_items, _isRefreshing) { items, isRefreshing ->
        FooUiState.Idle(items = items, isRefreshing = isRefreshing)
    }
}

UiEvent

UI操作 (ボタンのタップやダイアログを閉じるなど) や画面上で起きるイベントは全てUiEventという形式で定義しています。

sealed interface HomeUiEvent {
    data class OnClickTab(val tabIndex: Int) : HomeUiEvent
    data class ShowMyPage(val tabIndex: Int, val myPageType: MyPageType) : HomeUiEvent
}

UiEvent として定義することで、onButtonPressed のようなコールバックをコンポーネント間で個別に受け渡す必要がなくなります。
複数のイベントを扱う場合でも、onEvent: (UiEvent) -> Unit を引数として受け取るだけで済むためインターフェースをシンプルに保つことができます。

また、後述する ScreenBuilder 上でイベントハンドリングを一箇所に集約できるようになるため、処理の流れが追いやすくなり結果として可読性の向上にもつながります。

コンポーネントの Preview を作成する際も、必要な表示用データと空実装のラムダ関数を onEvent として渡すだけでよく、実装や確認がしやすくなる点も利点です。

UiEventのアイディアは Handling UI Events in Jetpack Compose: A Clean Approach の記事を参考に採用させていただきました。

ScreenBuilder

Composeの画面実装において、独自のScreenBuilderパターンを採用しています。
ScreenBuilderは各画面の最上位のコンポーネントです。ScreenBuilderではFirebaseにスクリーンイベントを送ったり、各UiEventに対応する画面遷移操作などのロジックを実装します。ScreenBuilderレイヤーにUIロジックの記述をまとめることで、Screen以下のComposeコンポーネントが純粋なUI実装の関数として定義することで出来るようになりました。

@Composable
fun FooScreenBuilder(
    viewModel: FooViewModel = hiltViewModel(),
    navigateToBar: () -> Unit,
) {
    // 状態の収集
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    // イベントハンドリング
    val onEvent: (FooEvent) -> Unit = { event ->
        when (event) {
            FooEvent.NavigateToBar -> navigateToBar()
            is FooEvent.UpdateData -> viewModel.updateData(event.data)
        }
    }

    // 画面描画
    RabbitTheme {
        when (uiState) {
            is FooUiState.Idle -> FooScreen(uiState = uiState, onEvent = onEvent)
            is FooUiState.Error -> NorurunErrorView(error = uiState.error)
        }
    }
}

Composeコンポーネントの分類

東急線アプリでは、Jetpack Compose のコンポーネントを粒度の異なる 4 つの分類に分けて設計しています。これはコンポーネントが肥大化して読みづらくなることを防ぐためのひとつの指標です。

厳密なルールとして運用しているわけではありませんが、コンポーネントの責務が過度に大きくならないよう、この 4 つの分類を意識しながらコンポーネント設計を行っています。

分類 役割
ScreenBuilder ViewModelバインディング、イベントハンドリング、状態管理
Screen 画面全体の描画(uiStateとonEventを引数に取る)
Section 画面を意味のある単位で分割したComposable
Component Sectionを構成するUI部品

Dark Theme対応

東急線アプリでは、Androidのシステム設定に追従したDark Themeに対応しています。
また、アプリ内の設定からも変更することができます。

Theme関連の実装は、主に以下の構成で管理しています。

  • Colors:Light / Darkそれぞれの色定義
  • Theme:Theme切り替えとMaterialThemeへの適用

Material3のColorSchemeに加えて、路線カラーなどアプリ固有の色についてはRabbitColorsとして定義し、CompositionLocal経由で参照できるようにしています。

Themeの適用はRabbitThemeというComposableに集約しており、isSystemInDarkTheme()を利用してシステム設定に応じたLight / Darkの切り替えを行っています。

RabbitTheme {
    // Screen
}

原則としてアプリ全体はDark Themeに対応していますが、視認性やデザイン上の理由から、一部の画面ではLight Themeのみで表示するケースもあります。
その場合はRabbitTheme(darkTheme = false)のように指定し、画面単位でThemeを制御しています。

APIコードの自動生成

東急線アプリのバックエンドには BFF レイヤーが用意されており、BFF との REST API については OpenAPI による定義を行っています。

これまでは公式の OpenAPI Generator を利用していましたが、より理想に近いコード生成を実現するため、現在は自作のコード生成へと切り替えています。
自作することでkotlinx.serialization.SerializableのサポートやEnumやポリモーフィズム型のフォールバック制御などの自由度が高まりました。

その他の外部ライブラリ

東急線アプリでは、その他にもいくつかの外部ライブラリを利用していますが、いずれも比較的定番とされるものが中心です。そのため本記事では個別のライブラリについて詳しくは触れません。

ライブラリ 用途
Dagger Hilt 依存性注入(DI)
Retrofit / OkHttp HTTP通信
Moshi JSONパース
Kotlinx Coroutines 非同期処理
Coil 画像読み込み
Detekt 静的解析・コードフォーマット
DataStore ユーザー設定の永続化
Lottie のるるんアニメーション表示
Accompanist Permissions パーミッション処理
AboutLibraries OSSライセンス管理

今後も開発環境を健全に保つことを意識しつつ、利用者の方にとって価値のある改善を積み重ねていきます。


この記事で紹介した技術やライブラリのバージョンは2025年12月時点のものです。

脚注
  1. https://developer.android.com/guide/topics/manifest/activity-element?hl=ja#screen ↩︎

  2. https://github.com/DroidKaigi/conference-app-2023/blob/f255ed2f6f07f9f6f83bc3b15384b9bcf001d8e8/core/ui/src/commonMain/kotlin/io/github/droidkaigi/confsched2023/ui/UiStateBuilder.kt#L9 ↩︎

東急URBAN HACKS

Discussion