🤔

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

2022/09/06に公開約5,300字

ゆめみ社の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やネットワーク関連)

があります。

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

Discussion

ログインするとコメントできます