この記事は
アプリの設計は大切
最近私がコツコツやっている勉強方法を紹介させてください。
Androidに特化しているというよりかは、アプリの設計を考える練習です。
ここでいう設計とはMVVMとかクリーンアーキテクチャなどのアーキテクチャとはまた異なる概念です。
どういうデータ構造にしているとデータを画面に表示しやすくなるかや、どのメソッドをどのレイヤーで管理するかなどを考えます。
アプリをつくる時にどのような設計にするかは、アプリの今後に大きく影響します。
設計がめちゃくちゃだと、たとえばホーム画面とプロフィール画面でいいねの数が違ってしまったりします。
表示されるデータに一貫性がなく、画面によって異なる情報が提示されるのはバグです。
アプリの設計を疎かにするとバグを生みやすくなり、機能追加などがむずかしくなります。
アプリの設計を考える勉強法
新卒で超スタートアップに行って全部一人でつくっている方などは例外ですが、基本的に設計などの上流工程を考えるにはチーム内で一番えらくなる必要があると思っています。若手エンジニアにとって実際の業務で設計を考えるチャンスは少ないものです。
ではチャンスが与えられるまでの間は一人でこっそり設計について想いを巡らせ、練習します。
私は、既存のアプリの一部分を自分が実装するとしたらどういう設計にするかについて考えています。
この勉強方法で大切なのは実装は大体でいいということです。
ついつい、わからないところは調べ上げて完璧な動くコードを書いてしまいたくなりますが、その衝動を抑えてまずは全体の設計にだけ焦点を当てて考えます。
なのでコードは動かなくていいのです。
全体の設計が終わった時にアプリをつくるイメージが湧ききって、「やばい!いますぐこのアプリを作りたい!!」と思えるくらいがちょうどいいのです。
ちなみに私はこの勉強方法が楽しすぎて平日の22:00から朝の4:00までやってしまうことがしばしばあります。
楽しいので、気をつけて取り組んでください。
Netflixの設計を考えてみよう!
今回はNetfixのホーム画面をどのように設計するかについて考えていきます。
🚨コードは実際に動くものではありません!設計をイメージするための最低限のものという認識でお願いします!
View
データを表示するための画面を構成していきます。
また、UIはJetpackComposeで実装することを想定します。
今回はNetflixのホーム画面の設計を考えるので、ホーム画面のデザインがどうなっているか見てみましょう。
ホーム画面1 | ホーム画面2 |
---|---|
ホーム画面を開くと、ホーム画面1がまず表示されてスクロールするとホーム画面2が表示されていきます。
それでは、適当に画面構成していきましょう。
大体こんな感じになるかな〜と思います。
// === HomeScreen ===
LazyColumn {
PickUpContents() // ホーム画面1に表示されるでかいコンテンツ(キャプチャだと「セブン」てやつ)
WatchingContents() // 視聴中のコンテンツ
CategorySection() // 視聴中のコンテンツより下のコンテンツ全て
}
private fun WatchingContents() {
LazyCulumn {
WatchingContentsItem()
}
}
private fun WatchingContentsItem() { }
private fun PickUpContents() {
Column {
Image()
Row { CategoryTag() }
Row{
MyListButton()
ReplayButton()
InfoButton()
}
}
}
private fun CategorySection(categoryList: List<Category>) {
Column {
// categoryListに対してforeachをぶん回し、データの数だけCategoryを表示
categoryList.foreach {Category(/*categoryListの中にあるデータを渡す*/)}
}
}
private fun Category(/*categoryListの中にあるデータをもらってTextやImageに表示*/) {
Text() // Categoryのタイトル
// カテゴリ内にあるコンテンツの画像
LazyRow {
Image()
}
}
CategorySectionの型
そんなに複雑なところはないかと思いますが、CategorySectionの型についてだけ説明させてください。
CategorySectionはキャプチャのホーム画面2の視聴中のコンテンツ
より下にある、表示されている全てのコンテンツをさします。画像の黄色い枠の部分です。
タイトル(漫画が原作のTVアニメ
, 人気急上昇の作品
など)とコンテンツ分の画像のひとかたまりのビューが縦に並ぶことになるので次のような構造になります。
// Columnで縦に並べる
Column {
Category()
Category()
.
.
.
}
fun Category() {
Text() // `漫画が原作のTVアニメ`とかを表示する
// コンテンツ画像を横に並べる
LazyRow{
Image()
Image()
.
.
}
}
サンプルコードからもわかるとおり、データの数だけComposableを繰り返す必要が出てきそうです。
では、どのような形で(どのような型で)データを渡してあげれば、効率よく上記のようなUIを実装できそうでしょうか。
CategorySectionは各Category(漫画が原作のTVアニメ
, 人気急上昇の作品
など)を持っており、各々のCategoryはそのCategoryに従ったアイテム(CategoryItem
)を複数持っています。
キャプチャの場合だと、漫画が原作のTVアニメ
というCategoryはサマータイムレンダ
、ブラッククローバー
、推しの子
などのアイテム(CategoryItem
)を持っていますね。
つまり、次の図の右側にあるようなデータの構造となると考え、CategoryのListという形(型でいうとList<Category>
)でCategorySection()
にデータを渡してあげれば、あとはこのリストに対してforeachをかけてComposableを繰返しできそうです。
ポイント
ここでのポイントは
どういう画面構成にするか → どういう型のデータを作れば考えた画面にデータを表示させやすいか
という順番で考えることです。多少行ったり来たりしてもいいかなとは思います。
さて、ここまでで「どういう型のデータを作ればいいか」の先っちょまで考えることができました。
次はよりその部分を深掘りしていきます。
「どういう型のデータを作ればいいか」というのはつまり、UiStateのことです。
どういうUiStateを作ろうか考えていきましょう。
UiState
先ほど作成したViewに取得してきたデータを表示するにはどのような形(データ構造)にすれば表示させやすいかを考えて実装します。
Home画面は
- PickupContents
- WhatchingContents
- CategorySection
の3つで構成されています
参考:
// === HomeScreen ===
LazyColumn {
PickUpContents()
WatchingContents()
CategorySection()
}
それぞれのUiStateを考えていきましょう。
PickupContents
PickupContents
を構成する情報を書きます。
data class PickupContents(
val id: String,
val image: String, // コンテンツの画像
val contentsTags: List<String>, // 「ゾクっとする、人間の心理に迫る」などの部分
) {}
WhatchingContents
WhatchingContents
を構成する情報を書きます。
data class WatchingContents(
val id: String,
val image: String, // コンテンツの画像
val watchingPercentage: Float, // 「パート2-5」とかインジケータとか、"どこまで見たか"を表示するのに必要な情報
) {}
CategorySection
CategorySectionに関しては先にも触れましたが、次の図の通り、Categoryはそのカテゴリのタイトル(categoryTitle
)とカテゴリのコンテンツ(categoryItemList
)で構成されています。
従ってCategoryはList<Category>
という型で表現できます。
また、categoryItemList
はそのコンテンツのid
や画像(image
)で構成されています。
UiStateとして表現すると次の通りです。
data class Category(
val categoryTitle: String,
val categoryItemList: List<CategoryItem>,
) {}
data class CategoryItem(
val id: String,
val image: String,
) {
// Repositoryから取得してきたデータをViewに表示しやすい形(CategoryItem)に変換する
companion object {
// categoryはEntityとする
fun from(category): CategoryItem {
return CategoryItem(
// categoryのデータをCategoryItemに突っ込む
)
}
}
}
Home画面全体のUiState
Home画面全体のUiStateは次のようになります。
data class HomeUiState(
val shouldShowSnackBar: Boolean // Home画面にSnackBarとか出す時のことを考えて
val pickupContents: PickupContents,
val watchingContents: List<WatchingContents>,
val categorySection: List<Category>
) {}
ViewModel
先ほど作成したUiStateを型にしてサーバーやDBから取得してきたデータを更新します。
少し力付きているのでサーバから取得してきたPickupContents
のデータを更新するところだけ 本当に適当に 書いています。
この辺りは正直、実際に実装してみないとどういうコードになるか分かりにくいので、ここでデータを更新するということだけ分かればいいかなと考えています。
class HomeViewModel: viewModel() {
private val _uiState: MutableStateFlow<HomeUiState>
val uiState: StateFlow = _uiState
init {
val result = usecase.hoge() // resultの形はAPIによる
// PickupContentsを更新する
_uiState.value = HomeUiState(
pickupContents = PickupContents.from(result.pickupContent)
)
}
}
おわり と おわび
実際に動くわけではない、ほぼイメトレレベルのコードなので、命名がおかしかったり、コードに違和感があるところも多々あるかもしれません。分かりにくく、申し訳ございません。
本記事では設計の大切さと設計を考えるための具体的なトレーニング方法について説明させていただきましたが、いかがでしたでしょうか。
少しでも皆さんの日々の勉強のtipsになれば幸いに思います。