第6章 アーキテクチャ
6.1 アーキテクチャについて
Jetpack ComposeはデータもViewも全部Kotlinで作成します。
そのため、データをViewをしっかり分離しておかないとスパゲッティコードが出来上がり、コードの維持保守が難しくなる可能性があります。
6.2 オーバーエンジニアリングを防ぐ
使わない機能を作り込んでしまうと、時間の無駄である上に、可読性の低下、運用コストの増加を引き起こします。
大切なのは リファクタリング です。
変化に応じて、規模に応じて、仕様を変えずに設計を変えていくことです。そして仕様を変えないことを保証するには テスト が必要です。
つまり、 テストをしやすいコードを維持することが最も重要 です。
テストがしやすい設計は、大抵パターンが存在します。
まずは、既存パターンの中から適切なパターンを選択できるようにしましょう。
6.3 レイヤードアーキテクチャ
UIのあるシステムは基本的に次のような処理をします。
- UIが入力を受ける
- 入力イベントをシステムが解釈し、処理
- 処理結果をUIに描画
UIとシステムには明確な関心の違いがあります。
関心が違うならレイヤーとして切り分けるべきだと思いますよね?
アプリケーションを設計するとき、全体をいくつかの役割を持ったレイヤー(層)に分けて考える、ということがよく行われます。このような設計手法を レイヤードアーキテクチャ と言います。
6.3.1 レイヤードアーキテクチャのメリット
- 単純に理解しやすい
-
重複コードの排除
- 別々の画面でロジックを共通化したり、見た目が同じ画面に異なる情報を表示したり、使いまわせるようになる
-
分業がしやすい
- 画面作成、データ取得・整形などがファイルやクラスから分かれているので、コンフリクトを起こしにくくなる
-
テスタビリティの向上
- ユニットテストを書きやすい
6.3.2 レイヤーの分け方
レイヤーの分け方はいろいろありますが、少ない分け方だと以下のようなものがあります。
-
プレゼンテーション層
- UIの表示、ユーザ入力の受け付けに責任を持つ
- ビジネスロジックを持たず、画面にのみ責任を持つ
-
ドメイン層
- ドメインロジックを持つ
- 単純なアプリでは存在しないこともある
-
データ層
- データの取得・更新に責任を持つ
- DBやHTTPでのデータ取得・更新など
「ドメイン」「プレゼンテーション」は「Model」「View」とほぼ同じ意味と捉えて問題ありません。
プレゼンテーション / View:UIに関係するロジック
ドメイン / Model:システム本来の関心領域(UIに関係しない処理全て)
6.3.3 依存性逆転の原則
ドメイン層はデータ層に依存しており、ドメインのデータ構造が永続化のためのデータ構造に引きずられてしまいます。
そうすると以下のような問題が起こります。
- ドメイン層のテストに当たって、データベースの中身を書き換えて環境を整える必要がある
- データ層の実装を別の永続化機構(RDBからオブジェクト指向DBなど)に移行することができない
- データ層がデータ構造を定義していることでドメイン知識がデータ層に漏出
これを解決するために依存性逆転の原則を利用します。
ドメイン層は自分自身のために、永続化機構の実装に影響されない形で「データ層が従うべきインターフェイス」を定義します。
6.4 GUIアーキテクチャ
プレゼンテーションとドメインを分けるためのアーキテクチャをGUIアーキテクチャと呼びます。
ビジネスロジックとプレゼンテーションロジックをUIから分離すると、テスト、維持補修、コードのリサイクルの効果が期待できます。
今回はいくつかあるGUIアーキテクチャの中からMVVMパターンを取り扱います。
6.5 MVVMパターン
MVVMは以下の3要素から構成されます。
Model
UIに依存しない純粋なドメインロジックやそのデータを持つ。
Model自身は他のコンポーネントに依存しない=ViewやViewModelがなくてもビルドできる。
View
ユーザ操作の受付と画面表示を担当する。
ViewModelが保持する状態とデータバインディングし、ユーザ入力に応じてViewModelが保持するデータを加工・更新することでバインディングした画面表示を更新する。
ViewModel
ViewとModelの仲介役。以下の責務を担う。
- Viewに表示するためのデータを保持
- Viewからイベントを受け取り、Modelの処理を呼び出す
- Viewからイベントを受け取り、加工して値を更新
ビジネスロジックと独立した、画面表示のために必要な状態とロジックを担う。
6.5.1 データバインディング
2つのデータの状態を監視し同期する仕組みです。
片方のデータ変更をもう一方が検知して、データを自動的に更新します。
ViewとViewModelはデータバインディングによって関連付けられています。
ViewModelの状態変更に同期してViewの状態も更新され、画面に反映されます。
宣言的なバインディングによりViewModel自身の状態を更新するだけで、Viewの描画処理が発火するため、ViewModel内にViewに対する手続的な描画指示を書く必要がなくなります。
ViewとViewModelは完全に疎結合となり、具体的なViewが存在しなくてもプレゼンテーションロジックをテストしやすくなります。
6.5.2 MVVMのメリット
-
維持・改修の容易さ
- 理想としては最初からいい企画、デザイン、コードを作ることですが、アプリが定める環境は常に多変しているので現実的に仕様の変更、企画変更、デザイン変更、バグ修正が常に起こります。MVVMパターンではビューモデルがビューとモデルの間でAdapterみたいな役割を果たすため、変更が生じる際にその変更の内容を最小限に抑えることができます。
-
モデルとビューモデルがビューから独立している
- ビューモデルとモデルをプラットホーム非依存に開発できます
- テストが容易になります。
-
開発プロジェクト中、開発者とデザイナーが並列で作業できる
- UIデザインが出なくても、すでに定義されたモデルとビューモデルを先に開発できるため並列的な業務プロセスが可能です。
6.5.3 MVVMのデメリット
- 大きい、複雑なアプリケーションの作成のために考案されたデザインパターンなので小規模のアプリ制作で使用することになったらオーバーヘッドが大きくなり、変えて非効率的になる可能性があります。
- アプリがあまりにも膨大になるとData bindingのためにメモリの消費が大きくなる可能性があります。
6.6 MVVMで記事一覧画面を実装してみる
それでは実際に、記事一覧画面のダミーデータをModelに持たせ、ViewModelを通してViewに描画されるように直してみましょう。
6.6.1 Modelの作成
-
com.example.strongestnews
パッケージの下にmodels
パッケージを新規作成し、Article.kt
ファイルを作成します。 -
MainActivity.kt
に記述していたArticle
を先ほど作成したmodels/Article.kt
に移動します(MainActivity.kt
からは削除)package com.example.strongestnews.models import java.net.URL import java.util.Date data class Article( val id: String, val title: String, val imageURL: URL, val createdAt: Date, val detail: String )
6.6.2 ViewModelの作成
-
package com.example.strongestnews.ui.screens.articles
パッケージに画面の状態を定義するためのArticlesViewState.kt
を作成します
-
記事一覧画面では記事のリストを持っていたいので、
ArticleViewState
データクラスにそのように定義します。package com.example.strongestnews.ui.screens.articles import com.example.strongestnews.models.Article data class ArticlesViewState ( val articles: List<Article> )
-
com.example.strongestnews.ui.screens.articles
パッケージに状態と実行すべき機能を管理するためArticlesViewModel.kt
を作成します。 -
ArticlesViewModel.kt
に状態トラッキングされる値をダミーデータで初期化して宣言しますダミーデータは
MainActivity.kt
から引越ししています(MainActivity.kt
からは削除)package com.example.strongestnews.ui.screens.articles import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.example.strongestnews.models.Article import java.net.URL import java.util.Date class ArticlesViewModel { var viewState by mutableStateOf<ArticlesViewState>( ArticlesViewState( articles = listOf( Article( id = "20230101", title = "すごいニュース", createdAt = Date(), imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), detail = "ニフティから超絶爆裂に早い回線がリリースされました!やばい!すごい!スゴツヨ!" ), Article( id = "20230102", title = "やばいニュース", createdAt = Date(), imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), detail = "ニフティから超絶爆裂に早い回線がリリースされました!やばい!すごい!スゴツヨ!" ), Article( id = "20230103", title = "えぐいニュース", createdAt = Date(), imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), detail = "ニフティから超絶爆裂に早い回線がリリースされました!やばい!すごい!スゴツヨ!" ), Article( id = "20230104", title = "しぶいニュース", createdAt = Date(), imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), detail = "ニフティから超絶爆裂に早い回線がリリースされました!やばい!すごい!スゴツヨ!" ), ) ) ) }
-
MainApp.kt
を開き、ArticlesViewModel()
を作成しますpackage com.example.strongestnews.ui import androidx.compose.runtime.Composable import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.example.strongestnews.MainRoute import com.example.strongestnews.ui.screens.articleDetail.ArticleDetailScreen import com.example.strongestnews.ui.screens.articles.ArticlesScreen import com.example.strongestnews.ui.screens.articles.ArticlesViewModel import com.example.strongestnews.ui.screens.comments.CommentsScreen @Composable fun MainApp() { val navController = rememberNavController() NavHost( navController = navController, startDestination = MainRoute.Articles.route ) { composable(MainRoute.Articles.route) { val viewModel = ArticlesViewModel() ArticlesScreen( articles = articles, onNavigateToArticleDetail = { articleID -> navController.navigate("${MainRoute.ArticleDetail.route}/$articleID") } ) } /* ... */
6.6.3 ViewModelの状態をViewに伝播させる
-
ArticlesScreen
で、ダミーデータを受け取っていた部分を削除し、ViewModel
を受け取れるように変更します/* ... */ @Composable fun ArticlesScreen( // articles: List<Article>, viewModel: ArticlesViewModel, onNavigateToArticleDetail: (String) -> Unit, ){ ArticlesContainer( articles = articles, onClickSeeArticleDetail = onNavigateToArticleDetail ) } /* ... */
-
ViewModel
からViewState
を取り出し、さらにViewState
からarticles
を取り出してArticlesContainer
コンポーザブルに渡します/* ... */ @Composable fun ArticlesScreen( viewModel: ArticlesViewModel, onNavigateToArticleDetail: (String) -> Unit, ){ val viewState = viewModel.viewState // viewStateを取り出す ArticlesContainer( articles = viewState.articles, onClickSeeArticleDetail = onNavigateToArticleDetail ) } /* ... */
-
プレビューにダミーデータを直接渡すように変更します
/* ... */ @Preview(showBackground = true) @Composable fun ArticlesPreview() { StrongestNewsTheme { ArticlesContainer( articles = listOf( Article( id = "20230101", title = "すごいニュース", createdAt = Date(), imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), detail = "ニフティから超絶爆裂に早い回線がリリースされました!やばい!すごい!スゴツヨ!" ), Article( id = "20230102", title = "やばいニュース", createdAt = Date(), imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), detail = "ニフティから超絶爆裂に早い回線がリリースされました!やばい!すごい!スゴツヨ!" ) ), onClickSeeArticleDetail = {} ) } }
-
MainApp.kt
を開き、viewModel
をArticlesScreen
に渡すように変更します/* ... */ composable(MainRoute.Articles.route) { val viewModel = ArticlesViewModel() ArticlesScreen( // articles = articles, viewModel = viewModel, onNavigateToArticleDetail = { articleID -> navController.navigate("${MainRoute.ArticleDetail.route}/$articleID") } ) } /* ... */
6.7 MVVMで記事詳細画面を実装してみる
記事一覧と同様に記事詳細もMVVM構成に変更しましょう。記事詳細ではArticleのModelを使い回すため作成不要です。
-
ArticleDetailViewState
を作成します実装例
package com.example.strongestnews.ui.screens.articleDetail import com.example.strongestnews.models.Article data class ArticleDetailViewState ( val article: Article? )
-
ArticleDetailViewModel
を作成します実装例
package com.example.strongestnews.ui.screens.articleDetail import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.example.strongestnews.models.Article import java.net.URL import java.util.Date class ArticleDetailViewModel { var viewState by mutableStateOf<ArticleDetailViewState>( ArticleDetailViewState( article = Article( id = "20230101", title = "すごいニュース", createdAt = Date(), imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), detail = "ニフティから超絶爆裂に早い回線がリリースされました!やばい!すごい!スゴツヨ!" ) ) ) }
-
ArticleDetailScreenでViewModelを受け取って表示するように変更します
-
viewStateには、ArticleのModelをオプショナル型で定義しているため、
viewState.article?.hoge
のように呼び出します。 -
Textにはオプショナル型を渡せないため、以下の様にnullの時は空文字を渡すように場合分けをしています
text = article?.title ?: ""
実装例
package com.example.strongestnews.ui.screens.articleDetail import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Share import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest import com.example.strongestnews.R import com.example.strongestnews.models.Article import com.example.strongestnews.ui.theme.StrongestNewsTheme import java.net.URL import java.util.Date @Composable fun ArticleDetailScreen( articleID: String, viewModel: ArticleDetailViewModel, onNavigateToBack: () -> Unit, onNavigateToComment: (String) -> Unit, ) { val viewState = viewModel.viewState ArticleDetailContainer( article = viewState.article, onNavigateToBack = onNavigateToBack, onNavigateToComment = onNavigateToComment, ) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun ArticleDetailContainer( article: Article?, onNavigateToBack: () -> Unit, onNavigateToComment: (String) -> Unit, ){ // Composeによる追跡がされる値 var favoriteCount = remember { mutableStateOf(0) } Scaffold( topBar = { MediumTopAppBar( title = { Text(stringResource(R.string.articles_screen_title)) }, navigationIcon = { IconButton( onClick = onNavigateToBack ) { Icon( Icons.Filled.ArrowBack, contentDescription = "Navigation Back" ) } } ) }, ) { innerPadding -> Column( modifier = Modifier .verticalScroll(rememberScrollState()) .fillMaxWidth() .padding(innerPadding) .padding(20.dp), verticalArrangement = Arrangement.spacedBy(15.dp) ) { AsyncImage( modifier = Modifier .fillMaxWidth() .aspectRatio(16f / 9f) .clip(RoundedCornerShape(10.dp)), contentScale = ContentScale.FillWidth, model = ImageRequest .Builder(LocalContext.current) .data(article?.imageURL.toString()) .crossfade(true) .placeholder(R.drawable.placeholder) .error(R.drawable.placeholder) .build(), contentDescription = article?.title ?: "" ) Column { Text( text = article?.createdAt.toString(), style = MaterialTheme.typography.labelLarge ) Spacer(modifier = Modifier.height(5.dp)) Text( text = article?.title ?: "", style = MaterialTheme.typography.titleLarge ) } Row( horizontalArrangement = Arrangement.spacedBy(10.dp) ) { AssistChip( onClick = { favoriteCount.value++ }, label = { Text(text = favoriteCount.value.toString())}, leadingIcon = { Icon( Icons.Filled.Favorite, contentDescription = "favorite", Modifier.size(AssistChipDefaults.IconSize) ) } ) AssistChip( onClick = {}, label = { Text(text = "Share") }, leadingIcon = { Icon( Icons.Filled.Share, contentDescription = "Share", Modifier.size(AssistChipDefaults.IconSize) ) } ) } OutlinedButton( modifier = Modifier .fillMaxWidth(), onClick = { onNavigateToComment(article?.id!!) } ) { Text(text = "コメントを表示") } Text( text = article?.detail ?: "", style = MaterialTheme.typography.bodyLarge ) } } } @Preview(showBackground = true) @Composable fun ArticleDetailScreenPreview() { StrongestNewsTheme { ArticleDetailContainer( article = Article( id = "20230101", title = "すごいニュース", createdAt = Date(), imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), detail = "ニフティから超絶爆裂に早い回線がリリースされました!やばい!すごい!スゴツヨ!" ), onNavigateToBack = {}, onNavigateToComment = {} ) } }
-
viewStateには、ArticleのModelをオプショナル型で定義しているため、
-
MainApp.ktでviewModelを作成し、ArticleDetailScreenに渡すように変更します
実装例
package com.example.strongestnews.ui import androidx.compose.runtime.Composable import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.example.strongestnews.MainRoute import com.example.strongestnews.ui.screens.articleDetail.ArticleDetailScreen import com.example.strongestnews.ui.screens.articleDetail.ArticleDetailViewModel import com.example.strongestnews.ui.screens.articles.ArticlesScreen import com.example.strongestnews.ui.screens.articles.ArticlesViewModel import com.example.strongestnews.ui.screens.comments.CommentsScreen @Composable fun MainApp() { val navController = rememberNavController() NavHost( navController = navController, startDestination = MainRoute.Articles.route ) { composable(MainRoute.Articles.route) { val viewModel = ArticlesViewModel() ArticlesScreen( viewModel = viewModel, onNavigateToArticleDetail = { articleID -> navController.navigate("${MainRoute.ArticleDetail.route}/$articleID") } ) } composable( "${MainRoute.ArticleDetail.route}/{articleID}", arguments = listOf( navArgument("articleID") { type = NavType.StringType } ) ) { backStackEntry -> val receivedArticleID = backStackEntry.arguments?.getString("articleID")!! val viewModel = ArticleDetailViewModel() ArticleDetailScreen( articleID = receivedArticleID, viewModel = viewModel, onNavigateToBack = { navController.popBackStack() }, onNavigateToComment = { articleID -> navController.navigate("${MainRoute.Comments.route}/$articleID") } ) } composable( "${MainRoute.Comments.route}/{articleID}", arguments = listOf( navArgument("articleID") { type = NavType.StringType } ) ){ backStackEntry -> val receivedArticleID = backStackEntry.arguments?.getString("articleID")!! CommentsScreen( articleID = receivedArticleID, onNavigateToBack = { navController.popBackStack() } ) } } }
ここまでできたらビルドしてみましょう。現在はViewModelで決め打ちの値を初期値として設定しているので、どの記事を選択しても同じ記事詳細が表示されます。
6.8 MVVMでコメント画面を実装してみる
コメント画面もMVVMで実装してみましょう。
-
CommentのModelを作成する
実装例
package com.example.strongestnews.models import java.util.Date data class Comment( val id: String, val name: String, val comment: String, val createdAt: Date )
-
CommentsViewState
を作成する実装例
package com.example.strongestnews.ui.screens.comments import com.example.strongestnews.models.Comment data class CommentsViewState( val comments: List<Comment> )
-
CommentsViewModel
を作成する実装例
package com.example.strongestnews.ui.screens.comments import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.example.strongestnews.models.Comment import java.util.Date class CommentsViewModel { var viewState by mutableStateOf<CommentsViewState> ( CommentsViewState( comments = listOf( Comment( id = "20230101", name = "ニフ山", comment = "すごい!", createdAt = Date() ), Comment( id = "20230102", name = "山田", comment = "やばい!", createdAt = Date() ), Comment( id = "20230103", name = "田中", comment = "えぐい!", createdAt = Date() ), Comment( id = "20230104", name = "中村", comment = "しぶい!", createdAt = Date() ), ) ) ) }
-
CommentsScreenでViewModelを受け取って表示するように変更する
実装例
package com.example.strongestnews.ui.screens.comments import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Send import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.example.strongestnews.R import com.example.strongestnews.models.Comment import com.example.strongestnews.ui.screens.comments.components.CommentItem import com.example.strongestnews.ui.theme.StrongestNewsTheme import java.util.Date @Composable fun CommentsScreen( viewModel: CommentsViewModel, articleID: String, onNavigateToBack: () -> Unit, ) { val viewState = viewModel.viewState CommentsContainer( comments = viewState.comments, onNavigateToBack = onNavigateToBack ) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun CommentsContainer( comments: List<Comment>, onNavigateToBack: () -> Unit, ){ var text by rememberSaveable( stateSaver = TextFieldValue.Saver ) { mutableStateOf(TextFieldValue("", TextRange(0, 7))) } Scaffold( topBar = { MediumTopAppBar( title = { Text(stringResource(R.string.articles_screen_title)) }, navigationIcon = { IconButton( onClick = onNavigateToBack ) { Icon( Icons.Filled.ArrowBack, contentDescription = "Navigation Back" ) } } ) }, ) {innerPadding -> Column( modifier = Modifier .fillMaxWidth() .padding(innerPadding) ) { // コメントリスト Column( modifier = Modifier .verticalScroll(rememberScrollState()) .weight(1f) .padding(20.dp) ) { comments.forEach {comment -> CommentItem(comment = comment) } } // 下の部分 Column { Divider() TextField( modifier = Modifier .fillMaxWidth() .padding(20.dp), value = text, onValueChange = { text = it }, placeholder = { Text(text = "Comment") }, trailingIcon = { IconButton( onClick = {}, enabled = text.text.isNotEmpty() ) { Icon( Icons.Filled.Send, contentDescription = "Send" ) } }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Send ), keyboardActions = KeyboardActions(onSend = {}) ) } } } } @Preview(showBackground = true) @Composable fun CommentsScreenPreview(){ StrongestNewsTheme { CommentsContainer( comments = listOf( Comment( id = "20230101", name = "ニフ山", comment = "すごい!", createdAt = Date() ), Comment( id = "20230102", name = "山田", comment = "やばい!", createdAt = Date() ) ), onNavigateToBack = {} ) } }
Commentコンポーザブルも修正する
実装例
package com.example.strongestnews.ui.screens.comments.components import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import com.example.strongestnews.models.Comment import com.example.strongestnews.ui.theme.StrongestNewsTheme import java.util.Date @OptIn(ExperimentalMaterial3Api::class) @Composable fun CommentItem( comment: Comment ){ ListItem( overlineText = { Text(comment.createdAt.toString()) }, headlineText = { Text(text = comment.name) }, supportingText = { Text(text = comment.comment) } ) } @Preview @Composable fun CommentPreview(){ StrongestNewsTheme { CommentItem( comment = Comment( id = "20230101", name = "ニフ山", comment = "すごい!", createdAt = Date() ) ) } }
-
MainApp.ktでviewModelを作成し、CommentScreenに渡すように変更する
実装例
/* ... */ composable( "${MainRoute.Comments.route}/{articleID}", arguments = listOf( navArgument("articleID") { type = NavType.StringType } ) ){ backStackEntry -> val receivedArticleID = backStackEntry.arguments?.getString("articleID")!! val viewModel = CommentsViewModel() CommentsScreen( viewModel = viewModel, articleID = receivedArticleID, onNavigateToBack = { navController.popBackStack() } ) } /* ... */
- ビルドして、データが表示されることを確認します