🤔

ゆめみ社のAndroidインターンに参加してきたので学んだことをまとめておく(アプリのアーキテクチャ編)

2022/09/07に公開

ゆめみ社の Android インターンに参加しました。
なかなかにありがたい経験をさせてもらえたので、アウトプットします 👍

  • 基本的には課題を進めていく 🚶
  • 課題は AndroidView で開発する前提になっていたが、Jetpack Compose の方が得意という旨を伝えると、Compose を使って UI を作っていくことに。柔軟に対応していただくことができた。感謝 😊

アプリのアーキテクチャ

参考:
公式ドキュメント
https://developer.android.com/jetpack/guide?hl=ja#common-principles
Codelab
https://developer.android.com/codelabs/basic-android-kotlin-training-repository-pattern?hl=ja#0


上記ドキュメントより引用

上記ドキュメントより引用

レポジトリ?ViewModel?レイヤー?なんだなんだ?意味がわからん www

要は「アプリの役割を分割(=いろんなクラスに役割分担)しようぜ」と理解しました。

Android アプリの一般的なアーキテクチャではUI Element (Compose の各 UI) と ViewModelRepository の 3 つに分解すると理解しました。
それぞれの以下のような役割などがあります。

要素 役割
UI Element ユーザに UI を表示する
ViewModel UI Element の状態(=UiState)を保持したり、変更したりする
Repository 外部から情報を取得したり、外部にアクションを起こしたりする、いわばアプリ外部との窓口(これはアプリから見れば情報の溜まり場に当たるのでレポジトリという名前なのかな)

例えば DB に数字を保存するカウンターアプリは次のように実装されるでしょう。

UI Element
@Composable
fun Counter(counterViewModel:CounterViewModel = viewModel()){
  val count = counterViewModel.uiState.collectAsState().count  // 伏線1
  Text("数字:$count")
  Button(onClick = { counterViewModel.countUp() }){ Text("クリック") }
}
ViewModel
class CounterViewModel : ViewModel(){
  // ViewModel内で使用するためのUiState
  private val _uiState : MutableStateFlow(
    UiState(
      count = 0,
    )
  )
  // ViewModel外で使用するためのUiState -> 伏線1
  val uiState = _uiState.asStateFlow()

  fun countUp(){
    _uiState.update { it.copy(count = it.count+1) }
    // DBの同期
    syncDb()
  }
  fun syncDb(){
    val db = Db()
    db.save("count",_uiState.value.count)
  }
}

ここでなぜ 3 つに分けるのかを順に理解してきます。

1. 1つにまとめるのはなぜだめか?

そんなことをしたら 1 ファイルに数百行... そんなの良いわけないでしょう!

すべて1ファイルにまとめると...
@Composable
fun Counter(){
  val count by remember(0)
  fun countUp(){
    count += 1
    syncDb()
  }
  Text("$count")
  Button(onClick = {countUp()}){ Text("クリック") }
}

2. 2つにどう分けるか

1 つがダメなら 2 つに分けようと考えます。どういった基準で分けるか、 「見た目」「状態」 に分けます。
見た目は UI Element、状態は ViewModel に任せることにします。

UI Element
@Composable
fun Counter(counterViewModel:CounterViewModel = viewModel()){
  // ViewModelが管理してくれている状態を取得する
  val count = counterViewModel.uiState.collectAsState().count
  Text("$count")
  Button(onClick = {
    counterViewModel.countUp()  // UI ElementはViewModelに状態の変更を依頼する
  }){
    Text("クリック")
  }
}
ViewModel
class CounterViewModel : ViewModel() {
  // ViewModelが状態を管理する
  // つまりViewModelは自分以外が無闇に状態を変更させないように
  //   状態(uiState)を読み取り専用にする
  private val _uiState = MutableStateFlow(
    UiState(
      count = 0,
    )
  )
  val uiState = _uiState.asStateFlow()

  fun countUp(){
    this._uiState.update { it.copry(count = it.count+1) }
    syncDb()
  }
}
気づき

// ViewModel が状態を管理する
// つまり ViewModel は自分以外が無闇に状態を変更させないように
// 状態(uiState)を読み取り専用にする

ん?これカプセル化ってやつか!?

こんな感じで ViewModel に

  • 状態そのものをとして ViewModel のプロパティ(フィールド)、
  • それを変更するための方法をメソッドとして、

それぞれ定義することで状態を UI Element から抜き出すことができました!

3. いよいよ3つに分ける

ここでプロジェクトが大規模になってきた時に起こり得ることを考えます。

先ほど

状態を変更するための方法をメソッドとして、

と書きましたが、状態を変更するために使用する DB に関するクラス(や関数など)はこの ViewModel だけでなく、他の ViewModel でも共通して使うことが多いでしょう。(以下アコーディオン内を参照)

状態を変更するために使用する DB に関するクラス(や関数など)

「例えば ◯◯ViewModel でも ××ViewModel でも DB を使いたい時に ◯◯ViewModel の中に DB に関する情報を書くのはおかしくないですか?」、ということです。

また実際には DB だけではなくネットワーク関連のクラス・関数などもこれと同じようなことが言えると思います。

これをいちいち ViewModel 内で書いているとコードが重複したりしてしまうでしょう。これを避けるために外部からデータをいじる処理は別のクラスにおいてしまおう、という発想になるわけです。この別のクラスに当たるのがRepositoryになるわけです。

また ViewModel のメソッド(=UI の状態を変更するための方法)と Repository のメソッドは何が違うのでしょうか。答えは

  • 「UI Element の状態を変更するロジックは ViewModel のメソッド」に、
  • 「外部とデータをやりとりしたいといったロジックは Repository のメソッド」に、

おくべきだと思います。

ここからわかるようにViewModel は UI Element と密接な関係にあります。なのでGoogle のドキュメントで出てくる ViewModel(ドキュメント内では State holders と呼んでいます)は UI Element と一緒にUI レイヤというグループにまとめられて紹介されています。


ドキュメントより引用(再喝)

また外部のデータソース(DB やネットワーク)と Repository は密接な関係にあります。もう分かりますね。そうです。Google のドキュメントではData レイヤというグループにまとめられて紹介されています。


ドキュメントより引用(再喝)

まとめると...

アプリのアーキテクチャはUI レイヤData レイヤに分けることができます。

UI レイヤには

  • ユーザに見せる UI そのものであるUI Element
  • それの状態を管理するState Holder(=ViewModel)

があります。

Data レイヤには

  • 外部とアプリをつなぐRepository
  • 外部リソース自身を表すData Sources (DB やネットワーク関連)

があります。



追記:
より正しい意味で捉え直すことができたので、こちらに追記させていただきます。

  • UI レイヤにはデータをどう表示するか、UI の状態、UI から発生したイベントをどう処理するかを記述します。
  • Data レイヤにはデータをどのように取得・更新するかを記述します。
  • Domain レイヤ(必須ではない) は必要に応じて UI レイヤと Data レイヤの橋渡しをします。

UI レイヤでは

  • UIElementsでデータをどう表示するかを定義します。例えばAndroidView(Activity や Fragment,View など)やJetpackComposeなんかがこれを担当することになるでしょう。
  • State Holderで UI の状態(ローディング状態,表示したいエラー情報など)の保持 と UI から発生したイベントをどう処理するかを記述します。ViewModelクラスを使って実装することが多そうです。

一方 Data レイヤでは

  • Repositoryでデータソースとどうやりとりするかを決定づけます。例えば「データを保存したい」時には「ローカルデータベースにアクセスして保存する。一緒にサーバにも送信しておく。」といったように定義します。「◯◯ したい」時(抽象的なやりたいこと)は「×× を使って実現する」(具体的な実装)と定義します。
  • Data Sourceでは外部と実際にやりとりするコードを記述します。ローカル DB を扱うときはRoomを、インターネットへ送受信したい時はRetrofitがこれらの枠割を果たしてくれます。(多くの場合自分で実装することはあまりないと思います)
  • Domain レイヤは UI レイヤと Data レイヤ間に置き、UI レイヤ(の StateHolder)でよく出てくるレポジトリやその操作を共通化します。例えばログイン処理を実装するときは「サーバに ID とパスワードを送る」「トークンをローカルに保存する」といった操作が必要ですが、ログインが必要な画面(StateHolder)全てにこれらを書くのは大変なので共通で使う処理(今回はログイン処理)は一箇所にまとめて使えるようにします。この時にどのレポジトリをどのように使ってログインするかを記述するのが Domain レイヤです◯◯UseCaseという名前をつけがちです。

まとめると

UI レイヤ

どうデータを見せるか -> UI Elements (Android View , Jetpack Compose)
UI の状態とそのイベント処理 -> StateHolder (ViewModel)

Data レイヤ

何をしたいかとその実装 -> Repository (Repositoryクラス)
データベースやサーバとの通信など具体的な実装方法 -> Data Source(RoomRetrofitなど自分で実装することは少ない)

Domain レイヤ

よく使う Repository の組み合わせをまとめて UI レイヤ(の StateHolder)が使いやすいようにする。(UseCaseクラス)



インターンアウトプットシリーズ一覧

Discussion