カンカクでAndroidエンジニアをやっている @haru067です。好きな焼肉は牛タンです。
今回はJetpack Composeの設計の話をします。
よくある実装
アプリ開発で非常によくあるパターンとして、リスト画面があり、タップすると詳細画面に遷移する、というのがあります。例えば、記事の一覧があり、タップすると記事の詳細に遷移するような画面を作るとしましょう。
記事に対応するUIステートをArticle
とし、
data class Article( val id: String, val title: String, )
記事一覧のUIをArticleList
、一覧の子要素をArticleItem
とします。
@Composable fun ArticleItem( article: Article, onClick: ..., // ここをどうするか modifier: Modifier = Modifier, ) { Column(modifier.clickable { ... }) { Text(text = article.title) } } @Composable fun ArticleList( articles: List<Article>, navigateToArticleDetail: (String) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn(modifier) { items(articles) { article -> ArticleItem( article = article, onClick = ... ) } } }
navigateToArticleDetail
は記事IDを引数にとり、記事の詳細に遷移する関数とします。
このとき、タップ時のイベントであるonClick
の型はどうすべきでしょうか?
深く考えなければArticleList
の引数navigateToArticleDetail
をそのまま渡してしまうのが楽そうです。これを実装Aとします。
// 実装A @Composable fun ArticleItem( article: Article, onClick: (String) -> Unit, // String: 記事ID modifier: Modifier = Modifier, ) { Column(modifier.clickable { onClick(article.id) }) { // ここでIDを指定 Text(text = article.title) } } @Composable fun ArticleList( articles: List<Article>, navigateToArticleDetail: (String) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn(modifier) { items(articles) { article -> ArticleItem( article = article, onClick = navigateToArticleDetail, // そのまま渡す ) } } }
一方、記事のIDを外部から注入する形にして、()-> Unit
で実装することもできます。これを実装Bとします。
// 実装B @Composable fun ArticleItem( article: Article, onClick: () -> Unit, // 引数にあったStringを削除 modifier: Modifier = Modifier, ) { Column(modifier.clickable { onClick() }) { // ここも削除 Text(text = article.title) } } @Composable fun ArticleList( articles: List<Article>, navigateToArticleDetail: (String) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn(modifier) { items(articles) { article -> ArticleItem( article = article, onClick = { navigateToArticleDetail(article.id) }, // 注入する ) } } }
さて、実装Aと実装B、どちらで実装すべきでしょうか?
依存性を考える
両者の違いの一つに依存性があります。
ArticleItem
単体の実装を考えたとき、実装AはArticleList
に依存しているが、実装Bは依存していないと言えます。
🐦 < つまり、どういうこと?
実装Aでは、ArticleItem
内で、記事のIDをonClickに渡していました。
Column(modifier.clickable { onClick(article.id) }) { …
記事のIDを渡す必要が何故あるのか考えてみると、「リスト内のどの要素が押されたのか区別したい」からです。「リスト内の」という言葉にあるように、これはArticleList
の実装の都合で、ArticleItem
単体で見たときには関係のない話です。従ってArticleItem
はこれに依存すべきではないでしょう。
例えば、タップしたときに画面遷移するのではなく、記事名を点滅させたくなった場合を考えてみます。実装AではArticleList
とArticleItem
の両方に変更が必要になりますが、実装BではArticleList
を変更するだけで対応できるはずです。
// 実装B @Composable fun ArticleList( articles: List<Article>, - navigateToArticleDetail: (String) -> Unit, // String: 記事ID + blink: (String) -> Unit, // String: 記事タイトル modifier: Modifier = Modifier, ) { LazyColumn(modifier) { items(articles) { article -> ArticleItem( article = artice, - onClick = { navigateToArticleDetail(article.id) }, + onClick = { blink(article.title) }, ) } } }
このように、親に依存しないよう設計することで、変更に強く、再利用の高いComposable functionを作ることができます。従って、依存性の観点では実装Bが望ましい、という結論になります。
いかがでしたか?
些細な部分ではありますが、割と気にせず実装している人も多いのではないでしょうか。 カンカクでは、こうした些細な部分に気を使って実装できるAndroidエンジニアを募集しています。 興味のある方はご連絡ください。