第4章 画面作成
4.1 Activity
画面描画にはActivityを使用します。
Activityは画面を管轄するコンポーネントです。具体的には以下のような役割を担っています。
- レイアウトファイルを読み込み、画面を作成
- 画面の書き換え
- 画面イベント(ボタンクリックなど)の処理
今回は1つのActivity上で、複数のComposeを書き換えていくことで各画面を作っていきます。
4.1.1 TaskとActivity
起動したActivityは、 Task という単位で管理されます。「■ボタン (タスク一覧)」を押したときに現れる、アプリ一覧のようなものの1つ1つがTaskです。
Taskの中はスタック構造になっており、その中にActivityが積み上がる形になっています。一番表にあるActivityだけがユーザに見えることによって、画面遷移が実現されます。
このとき、一番上以外のActivityは停止状態になることで、リソースの消費を抑えています。
ある操作をして、別のアプリに行って、戻ってくる、この一連の動作を「タスク」と捉え、1つの作業単位とするのがAndroidの考え方です。この辺りの思想も、最初からアプリ連携を前提としたプラットフォームならではのものと言えます。
ただし動作が分かりにくくなるという面もあるので、「別アプリは別Taskで起動する」ようなオプションも存在します。
4.2 ライフサイクル
Activityは OSにより起動され、OSにより管理されます 。したがって、アプリ側で起動・停止のタイミングを自由に記述することができません。
このため、OSが何かをするタイミングで処理を差し込む方法が用意されています。これをActivityの ライフサイクル と呼び、特定タイミングで呼ばれるメソッドを ライフサイクルメソッド と言います。適切な場所に適切な処理を入れる必要があります。
最低限覚えておく必要があるのは以下です。
onCreate()
Activityが作成されたときに呼び出されます。ここでレイアウトファイルをもとに画面を作成します。
画面オブジェクトを後から操作する必要がある場合、そのオブジェクトの取得も同時に行います。
今回使用するUIツールキットのJetpackComposeのComposeの呼び出しはこのonCreateで行います。
onResume() / onPause()
Activityが裏面に回るなどして停止状態になるとonPause()、再開するとonResume()が呼ばれます。
画面の更新処理は必ずActivityが起動状態であるときに行わなければなりません。停止状態のActivityを操作してしまうと、 NullPointerExceptionでエラー落ち します。
したがって、画面を更新するようなイベントはonResume()で開始、onPause()で停止させます。
ComposeとCoroutineを適切に使えば自力で停止させる処理を書く必要がなくなりますが、適切に使わないとNullPointExceptoinでエラー落ちします。
onDestroy()
Activityが破棄される前に呼び出されます。次のいずれかの理由でシステムによって呼ばれます。
-
Activityが終了する
- ユーザがActivityを完全に閉じる
-
Activityに対して
finish()
が呼び出される
- デバイスの向きの変更やマルチウィンドウモードなどの構成の変更に伴いActivityが一時的に破棄される
Activityが破棄状態に移行すると、Activityのライフサイクルに関連付けられているライフサイクル対応コンポーネントは
ON_DESTROY
イベントを受け取ります。この時点で、Activityが破棄される前にクリーンアップする必要があるデータをライフサイクル コンポーネントがクリーンアップできます。
以前のコールバック(
onStop()
など)によって解放されていないすべてのリソースは
onDestroy()
コールバックによって解放されます。
4.3 Jetpack Compose
Jetpack ComposeはUI 開発を簡素化するために設計されたツールキットです。
完全な宣言型であるため、データを UI 階層に変換する一連の関数を呼び出すことにより UI を記述します。基になるデータが変更されると、フレームワークは自動的にそれらの関数を再実行して UI 階層を更新します。
Composable
と呼ばれる各々のUIコンポーネントを拡張・合成(Composition)することで全体的なUIコンポーネントを構成します。
Jetpack Composeを使う利点は大きく3つあります。
-
コードを削減
- Android ビューシステムを使用する場合と比べて、少ないコードで多くのことができる。
- 作成するコードは Kotlin でのみ記述され、Kotlin と XML に分割されることはありません。
-
直感的
- Compose では宣言型 API を使用するので必要なのは UI を記述することだけであり、残りの処理は Compose が行います。
-
開発体験の向上
- XMLベースだと存在しないKotlinのコンパイラーが効く
- コンポーネント化により重複・依存関係をXMLベースより処理しやすい
それでは、実際に画面を作成してみましょう。
4.3.1 コンポーズ可能な関数
コンポーズ可能な関数とは、
@Composable
アノテーションが付いている通常の関数です。
これにより、関数は内部で他の
@Composable
関数を呼び出すことができるようになります。
アプリはコンポーズ可能な関数を呼び出すことにより、データをUIに変換します。
いわゆるコンポーネントのこと。
マニフェストファイルに定義されている通り、ユーザがアプリを開くとMainActivityが起動されます。
レイアウト定義は
setContent
内でコンポーズ可能な関数を呼び出すことで行います。
/*・・・*/
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StrongestNewsTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
/*・・・*/
プレビュー
パラメータのないコンポーズ可能な関数またはデフォルトパラメータを含む関数に
@Preview
アノテーションを付けて、プロジェクトをビルドします。
/*・・・*/
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
StrongestNewsTheme {
Greeting("Android")
}
}
右上のSplitレイアウトで表示することで、プレビューを見ることができます。(ビルドしていないと表示されません)
4.4 記事一覧画面を作成する
次のような画面を作成します
4.4.1 スクリーンを置く用のパッケージを作成
-
ui配下に
com.example.strongestnews.ui.screens
パッケージを新規作成します。 -
作成したscreensパッケージに、さらに
com.example.app.strongestnews.ui.screens.articles
パッケージを追加します。
4.4.2 記事一覧のコンポーネントを作成する
まずは以下のコンポーネントを作ります。
このコンポーネントでは記事のサムネイル画像、作成日、タイトルを表示します。
4.4.3 ファイル作成
-
ui.screens.articles
配下にcom.example.strongestnews.ui.screens.articles.components
パッケージを新規作成します。 -
components
パッケージにArticleCard.kt
を作成します。
4.4.4 ArticleCardコンポーザブルを実装
-
必要なライブラリを導入する
画像表示には外部ライブラリである coil-compose の
AsyncImage()
を用います。ライブラリの導入方法にはGUIを使った方法とbuild.gradleを使った方法の2種類あります。どちらの方法も記載しているので、好きな方でライブラリを追加してください。
coil-composeの追加
-
GUIで追加
ツールバー > Project Structure… > Dependencies > app > + > Library Dependency から検索画面を開く
org.jetbrains.kotlinx:kotlinx-serialization-json
で検索し、2.4.0
を選択してOKを押す入れたらApplyしてからOKで閉じる
-
app/build.gradleから追加
app/build.gradleのdependenciesに以下を追記
implementation("io.coil-kt:coil-compose:2.4.0")
File >
Sync Project with Gradle Files
を実行
-
GUIで追加
-
アプリのインターネット通信を許可する
-
AndroidManifest.xmlに
<uses-permission android:name="android.permission.INTERNET" />
を追加<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.INTERNET" /> <application <!-- ... -->
-
AndroidManifest.xmlに
-
関数を作成し、引数でタイトル、サムネイル画像URL、作成日を受け取ります
package com.example.strongestnews.ui.screens.articles.components import androidx.compose.runtime.Composable import java.net.URL import java.util.Date @Composable fun ArticleCard( title: String, imageURL: URL, createdAt: Date ){ }
-
Previewを作ります
@Composable fun ArticleCard( /*...*/ } @Preview() @Composable fun ArticleCardPreview() { StrongestNewsTheme { ArticleCard( title = "なんかすごい驚きのニュース", imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), createdAt = Date() ) } }
様々なサイズでの表示も確認するため横幅を設定した複数のPreviewも作成します
widthDp
を指定することで様々な横幅のPreviewを作成できます@Composable fun ArticleCard( /*...*/ } @Preview(widthDp = 200) @Composable fun ArticleCardPreview200() { StrongestNewsTheme { ArticleCard( title = "なんかすごい驚きのニュース", imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), createdAt = Date() ) } } @Preview(widthDp = 300) @Composable fun ArticleCardPreview300() { StrongestNewsTheme { ArticleCard( title = "なんかすごい驚きのニュース", imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), createdAt = Date() ) } } @Preview(widthDp = 400) @Composable fun ArticleCardPreview400() { StrongestNewsTheme { ArticleCard( title = "なんかすごい驚きのニュース", imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), createdAt = Date() ) } }
-
画像のplaceholderを準備します
-
左上からResourceManagerを開き、+ボタンで
Drawable Resource File
を選択 -
File nameに
placeholder
と入力してOKを押します -
先ほど作成したplaceholderを開き、ファイルの中身を以下に置き換える
<?xml version="1.0" encoding="utf-8"?> <shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android"> <size android:width="800dp" android:height="533dp" /> <solid android:color="@color/placeholder_gray" /> </shape>
- Colorタブを開き、+ボタンからColor Valueを選択
-
New Color Valueに以下の通りに入力し、OK
-
Resource name:
placeholder_gray
-
Resource value:
#FFC7C7C7
-
Resource name:
-
左上からResourceManagerを開き、+ボタンで
-
見た目を実装します
-
まずはカードっぽい見た目にしたいので、そのまま
Card
というコンポーザブルを使いますまた、横幅いっぱいに広がって欲しいので、
Modifier
にfillMaxWidth
を指定しますModifierとはブロック要素のUI表示を装飾・制御したい時に利用するKotlinのオブジェクト(CSSモジュールのようなもの)
たとえば背景、paddingなどで、行やテキスト、ボタンなどを装飾したり動作を追加したりします。
Modifier .fillMaxWidth() .padding(10.dp) .aspectRatio(16f / 7f)
package com.example.strongestnews.ui.screens.articles.components import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Card import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.example.strongestnews.ui.theme.StrongestNewsTheme import java.net.URL import java.util.Date @OptIn(ExperimentalMaterial3Api::class) @Composable fun ArticleCard( title: String, imageURL: URL, createdAt: Date ){ Card( modifier = Modifier.fillMaxWidth() ) { } } /* ... */
-
次に、画像やタイトルなどが縦に並んでほしいので、
Column
を使いますpackage com.example.strongestnews.ui.screens.articles.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Card import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.example.strongestnews.ui.theme.StrongestNewsTheme import java.net.URL import java.util.Date @OptIn(ExperimentalMaterial3Api::class) @Composable fun ArticleCard( title: String, imageURL: URL, createdAt: Date ){ Card( modifier = Modifier.fillMaxWidth() ) { Column { } } } /* ... */
-
カードの上部にサムネイル画像を表示させたいので、
AsyncImage
を使います横幅いっぱいかつ、アスペクト比が16:7になるように指定します。
Modifier
は.
を繋げることで複数指定することができます。AsyncImage
の詳細については以下のドキュメントを参考にして下さい。package com.example.strongestnews.ui.screens.articles.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Card import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import coil.compose.AsyncImage import coil.request.ImageRequest import com.example.strongestnews.R import com.example.strongestnews.ui.theme.StrongestNewsTheme import java.net.URL import java.util.Date @OptIn(ExperimentalMaterial3Api::class) @Composable fun ArticleCard( title: String, imageURL: URL, createdAt: Date ){ Card( modifier = Modifier.fillMaxWidth() ) { Column { AsyncImage( modifier = Modifier .fillMaxWidth() .aspectRatio(16f / 7f), contentScale = ContentScale.FillWidth, model = ImageRequest .Builder(LocalContext.current) .data(imageURL.toString()) .crossfade(true) .placeholder(R.drawable.placeholder) .error(R.drawable.placeholder) .build(), contentDescription = title ) } } } /* ... */
-
カードの下部には日付とタイトルを余白を持って縦に並びたいので、
Text
コンポーザブルをColumn
で入れ子にしますタイトルは最大行数を2行にし、表示しきれない場合はEllipsis(
…
)を表示するよう設定しています。package com.example.strongestnews.ui.screens.articles.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow 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.ui.theme.StrongestNewsTheme import java.net.URL import java.util.Date @OptIn(ExperimentalMaterial3Api::class) @Composable fun ArticleCard( title: String, imageURL: URL, createdAt: Date ){ Card( modifier = Modifier.fillMaxWidth() ) { Column { AsyncImage( modifier = Modifier .fillMaxWidth() .aspectRatio(16f / 7f), contentScale = ContentScale.FillWidth, model = ImageRequest .Builder(LocalContext.current) .data(imageURL.toString()) .crossfade(true) .placeholder(R.drawable.placeholder) .error(R.drawable.placeholder) .build(), contentDescription = title ) Column( modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(5.dp) ) { Text( text = createdAt.toString(), style = MaterialTheme.typography.labelSmall ) Text( text = title, style = MaterialTheme.typography.titleMedium, maxLines = 2, overflow = TextOverflow.Ellipsis ) } } } } /* ... */
-
ビルドするとプレビューを確認できます
いい感じにできているでしょうか?プレビューでは画像が表示されないので、プレースホルダーが表示されています
もし血染めコードになっている場合、同じ名前の違うコンポーザブルをimportしている場合もあるので、確認してみてください
-
まずはカードっぽい見た目にしたいので、そのまま
-
Cardをタップできるようにします
将来的にカードをタップしたら詳細画面に遷移します
タップした時の挙動をcallbackにするため
onClick
を追加します@OptIn(ExperimentalMaterial3Api::class) @Composable fun ArticleCard( title: String, imageUrl: URL, createdAt: Date, onClick: () -> Unit, ) { Card( modifier = Modifier.fillMaxWidth(), onClick = onClick, ) { /*...*/ } }
全てのPreviewにそれぞれ
onClick
を追加します。Previewでは動作を確認しないため、onClickは空で定義します。
@Preview(widthDp = 200) @Composable fun ArticleCardPreview200() { StrongestNewsTheme { ArticleCard( title = "赤楚衛二&町田啓太の表情に注目!八十路男性のリアルな恋模様とは『家、ついて』", createdAt = Date(), imageUrl = URL("https://dogatch.jp/prod/kanren_news/20220327/2e76157d5bb7f66b199d7ae5c756c7d6.jpg"), onClick = {}, ) } }
4.4.5 ArticlesContainerを作成
記事一覧を表示する
ArticlesContainer
を作成します。
-
ui.screens.articles
配下にArticlesScreen.kt
を新規作成します -
ArticlesContainer
コンポーザル関数の雛形を作成しますPreviewも作りましょう
package com.example.strongestnews.ui.screens.articles import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.example.strongestnews.ui.theme.StrongestNewsTheme @OptIn(ExperimentalMaterial3Api::class) @Composable fun ArticlesContainer() { Scaffold( topBar = { MediumTopAppBar( title = { Text("StrongestNews") }, ) }, ) { innerPadding -> Column( modifier = Modifier.padding(innerPadding), ) { Text("Hello") } } } @Preview(showBackground = true) @Composable fun ArticlesPreview() { StrongestNewsTheme { ArticlesContainer() } }
ここで最上位にある
Scaffold
とは、トップバーやナビゲーションなどを表示する際に使う、アプリの足場となるものを提供してくれるコンポーザブルです。今回は
topBar
を指定することで、トップバーを作成しています。 -
ArticleContainer
を見れるようにMainActivityを修正する現在はAndroidManifest.xmlで最初に表示されるコンポーザブルが
MainActivity
に指定されています。さらに
MainActivity
はGreeting
コンポーザブルを表示するようになっているので、ArticlesContainer
を表示するように差し替えます。Greeting
コンポーザブルは使用しないので、プレビューも合わせて削除します。package com.example.strongestnews import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import com.example.strongestnews.ui.screens.articles.ArticlesContainer import com.example.strongestnews.ui.theme.StrongestNewsTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { StrongestNewsTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { ArticlesContainer() } } } } }
アプリをビルドして実行してみます。
Run ‘app’
で実行できます。以下のようになっていたらOKです。(色味は端末によって違う可能性があります)
-
記事のダミーデータを作成し、記事の数だけAritcleコンポーザブルが表示されるように変更します。表示されるデータもダミーデータを参照するように修正します。
-
まずはダミーデータに使用するデータクラスを作成します。(とりあえずMainActivity.ktに宣言して、後々リファクタしていきます。)
/*・・・*/ data class Article( val id: String, val title: String, val imageURL: URL, val createdAt: Date ) class MainActivity : ComponentActivity() { /*・・・*/
-
ダミーデータを作成します
package com.example.strongestnews import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import com.example.strongestnews.ui.screens.articles.ArticlesContainer import com.example.strongestnews.ui.theme.StrongestNewsTheme import java.net.URL import java.util.Date data class Article( val id: String, val title: String, val imageURL: URL, val createdAt: Date ) val articles = listOf( Article( id = "20230101", title = "すごいニュース", createdAt = Date(), imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), ), Article( id = "20230102", title = "やばいニュース", createdAt = Date(), imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), ), Article( id = "20230103", title = "えぐいニュース", createdAt = Date(), imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), ), Article( id = "20230104", title = "しぶいニュース", createdAt = Date(), imageURL = URL("https://images.pexels.com/photos/1071882/pexels-photo-1071882.jpeg"), ), ) class MainActivity : ComponentActivity() { /* ... */
-
ArticlesContainer
でダミーデータを受け取るようにします/* ... */ import com.example.strongestnews.Article @Composable fun ArticlesContainer( articles: List<Article>, ) { /* ... */
-
ArticlesContainer
の引数が増えたので、呼び出し元であるPreviewとMainActivityを修正します/* ... */ import com.example.strongestnews.articles /* ... */ @Preview(showBackground = true) @Composable fun ArticlesPreview() { StrongestNewsTheme { ArticlesContainer( articles = articles ) } }
/* ... */ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { StrongestNewsTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { ArticlesContainer( articles = articles ) } } } } }
-
まずはダミーデータに使用するデータクラスを作成します。(とりあえずMainActivity.ktに宣言して、後々リファクタしていきます。)
-
ArticlesContainer
でダミーデータの数だけArticleCard
をforEach
で生成するようにしますArticleCard
の引数onClick
はとりあえず空にしておきます。@Composable fun ArticlesContainer() { Scaffold( topBar = { MediumTopAppBar( title = { Text("StrongestNews") }, ) }, ) { innerPadding -> Column( modifier = Modifier.padding(innerPadding), ) { articles.forEach { article -> ArticleCard( title = article.title, imageURL = article.imageURL, createdAt = article.createdAt, onClick = {} ) } } } }
ビルドして実行してみましょう。
ArticleCard
が縦に並ぶようになりました。 -
画面がギチギチなので、
Column
に余白をつけていきます-
両サイドにスペースを作るために、
.padding(30.dp)
を追加します -
記事間にスペースを作るために、
verticalArrangement = Arrangement.spacedBy(20.dp)
を追加します
package com.example.strongestnews.ui.screens.articles import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.example.strongestnews.Article import com.example.strongestnews.articles import com.example.strongestnews.ui.screens.articles.components.ArticleCard import com.example.strongestnews.ui.theme.StrongestNewsTheme @OptIn(ExperimentalMaterial3Api::class) @Composable fun ArticlesContainer( articles: List<Article> ) { Scaffold( topBar = { MediumTopAppBar( title = { Text("StrongestNews") }, ) }, ) { innerPadding -> Column( modifier = Modifier .padding(innerPadding) .padding(30.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { articles.forEach { article -> ArticleCard( title = article.title, imageURL = article.imageURL, createdAt = article.createdAt, onClick = {} ) } } } }
-
両サイドにスペースを作るために、
-
スクロールできるようにする
スクロール判定が
ArticleCard
に取られてるため、Column
で判定されるようにします。Column
のmodifier
に.verticalScroll(rememberScrollState())
を追加します。@Composable fun ArticlesContainer() { Scaffold( topBar = { MediumTopAppBar( title = { Text("StrongestNews") }, ) }, ) { innerPadding -> Column( modifier = modifier .verticalScroll(rememberScrollState()) .padding(innerPadding) .padding(30.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { articles.forEach { article -> ArticleCard( article = article, onClick = {}, ) } } } }
- ビルドすると、記事一覧画面が表示され、スクロールができるようになりました
4.5 記事詳細画面を作成 (演習)
次のような画面を作成します
4.5.1 事前準備
-
screens配下に
articleDetail
パッケージを追加し、ArticleDetailScreen.kt
を作成する -
MainActivity.ktにある
Article
のデータクラスに記事本文のフィールドを追加するdata class Article( val id: String, val title: String, val imageURL: URL, val createdAt: Date, val detail: String )
ダミーデータにも
detail
を追加しますval 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 = "ニフティから超絶爆裂に早い回線がリリースされました!やばい!すごい!スゴツヨ!" ), )
-
空の
ArticleDetailContainer
コンポーザブルを用意するArticle
を受け取る空の関数を作成しますpackage com.example.strongestnews.ui.screens.articleDetail import androidx.compose.runtime.Composable import com.example.strongestnews.Article @Composable fun ArticleDetailContainer( article: Article ){ }
-
ビルドしたら記事詳細画面が表示されるようにする
まだ画面遷移を実装していないので、ビルドしたら記事詳細画面が表示されるように
MainActivity
を書き換えておきます/* ... */ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { StrongestNewsTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { ArticleDetailContainer(article = articles[0]) } } } } }
4.5.2 記事詳細画面を実装してみる
記事一覧画面を参考にしながら、自力で
ArticleDetailContainer
を実装してみましょう
記事を表示する画面のパーツ
- サムネイル画像
-
作成日
- style: labelLarge
-
記事タイトル
- style: titleLarge
- いいねボタン
- シェアボタン
- コメントボタン
-
記事本文
- style: bodyLarge
ヒント1: Prewiew関数の実装例
/* ... */
@Preview(showBackground = true)
@Composable
fun ArticleDetailContainerPreview() {
StrongestNewsTheme {
ArticleDetailContainer(article = articles[0])
}
}
ヒント2: 使うComposable
-
Scaffold
-
CenterAlignedTopAppBar
-
Column
-
AsyncImage
-
Column
-
Text
-
Text
-
-
Row
-
AssistChip
-
AssistChip
-
-
OutlinedButton
-
Text
-
-
ヒント4: サムネイル画像の実装例
RoundedCornerShape
で
clip
(切り抜き)することで、角丸画像にすることができます
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
)
ヒント5: 文字のスタイル指定方法
Text(
text = "テキスト",
style = MaterialTheme.typography.titleLarge,
)
ヒント6 いいねボタン、シェアボタンの実装例
-
Row
で横並びにできます -
アイコン間にスペースが欲しいので
spacedBy
を指定します
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
AssistChip(
onClick = {},
label = { Text(text = "1000")},
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)
)
}
)
}
ヒント7 コメントボタンの実装例
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
onClick = {},
){
Text("Comments")
}
ArticleDetailScreen.ktの実装例
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.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
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.Article
import com.example.strongestnews.R
import com.example.strongestnews.articles
import com.example.strongestnews.ui.theme.StrongestNewsTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ArticleDetailContainer(
article: Article
){
Scaffold(
topBar = {
MediumTopAppBar(
title = { Text("StrongestNews") },
navigationIcon = {
IconButton(
onClick = {}
) {
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 = {},
label = { Text(text = "1000")},
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 = {}
) {
Text(text = "コメントを表示")
}
Text(
text = article.detail,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
@Preview(showBackground = true)
@Composable
fun ArticleDetailContainerPreview() {
StrongestNewsTheme {
ArticleDetailContainer(article = articles[0])
}
}
4.6 再コンポーズ
Compose は Composable が受け取るデータを追跡し、データが変更された時はコンポーネントを再コンポーズし、データの変更の影響を受けないコンポーネントはスキップします。
状態トラッキングをするために、Compose の
State
型と
MutableState
型を使用します。
4.6.1 記事詳細画面のいいねボタンの動作を実装する
いいねボタンは押した回数だけ数字が増えていきます。
-
Composeによる追跡を行う値を初期値0で定義します。
/* ... */ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf @OptIn(ExperimentalMaterial3Api::class) @Composable fun ArticleDetailContainer( article: Article ){ // Composeによる追跡がされる値 val favoriteCount: MutableState<Int> = mutableStateOf(0) // 初期値0 Scaffold( /* ...
-
ボタンをクリックすると1.で定義した値が加算され、ボタンに押された数を表示するようにします。
/* ... */ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf @Composable fun ArticleDetailContainer() { // Composeによる追跡がされる値 val favoriteCount: MutableState<Int> = mutableStateOf(0) // 初期値0 /*...*/ AssistChip( onClick = { favoriteCount++ }, label = { Text(favoriteCount.toString()) }, leadingIcon = { /*...*/ }, ) }
これで再コンポーズのトリガーは正常に機能していますが、再コンポーズの際に
favoriteCount
が再初期化され、0に戻ってしまいます。
トラッキングデータを再コンポーズ後も保持するため、コンポーズ可能なインライン関数
remember
を使用します。
remember
は、
private val
と同様にコンポジション内に単一のオブジェクトを保存でき、保存された値は再コンポーズ後も保持されます。
/* ... */
import androidx.compose.runtime.remember
@Composable
fun ArticleDetailContainer() {
// rememberで囲む
var favoriteCount = remember { mutableStateOf(0) }
/* ... */
AssistChip(
onClick = { favoriteCount.value++ },
label = { Text(text = favoriteCount.value.toString())},
leadingIcon = {
/* ... */
ビルドして、いいねボタンの数が増えることを確認します
値の参照にvalueプロパティを毎回参照するのも面倒なので、Kotlinの委譲プロパティを使用して簡素化することができます。
by
キーワードを使用して
count
を変数として定義します。
委任のゲッターとセッターのインポートを追加すると、毎回
MutableState
の
value
プロパティを明示的に参照せずに、間接的に
count
を読み取り、変更できます。
/* ... */
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@Composable
fun ArticleDetailContainer() {
var favoriteCount by remember { mutableStateOf(0) }
/* ... */
AssistChip(
onClick = { favoriteCount++ },
label = { Text(text = favoriteCount.toString())},
leadingIcon = {
/* ... */
-
Composable
- 画面に表示する
-
コンポジション
- Jetpack Composeがコンポーザブルの実行時に構築するUIの記述
-
初回コンポーズ
- コンポーザブルを初めて実行して、コンポジションを作成すること
-
再コンポーズ
- データ(状態)が変更されたとき、コンポジションを更新するためにコンポーザブルを再実行すること
4.7 コメント一覧画面を作成
次のような画面を作成します
- screens配下にcommentsパッケージを追加し、CommentsScreen.ktを作成する
-
画面通り作ってみましょう!
ヒント
- ダミーデータが別途必要なので、適当なところに持たせます
-
各コメントの数だけ繰り返しているパーツがある
-
forEach
を使ってCommentコンポーネントを作るとよさそうです -
ListItem
コンポーザブルも使えます
-
-
文字入力部分は横幅いっぱいですが、コメントは
padding
がありそうです-
Column
の入れ子が良さそう
-
-
下の文字入力部分の上の横棒は
Divider
といいます -
TextField
コンポーネントを使います-
文字入力には
mutableStateOf
を使いそうです -
Activityを破棄しても保持したい場合、
rememberSaveable
を使用します -
文字を入力していないと送信ボタンが押せないように、
trailingIcon
(これ↓)にIconButton
コンポーザブルを使用し、enabled
にtext.isNotEmpty()
メソッドを与えることでボタンのコントロールをすることができます -
keyboardOptions
で文字入力モードや、imeAction
(どのタイプのアクションを行うか)を指定することができます -
keyboardActions
を使用することで、ユーザがimeAction
によってトリガーされるアクションを指定できます
-
文字入力には
実装例
MainActivity.ktにコメントのダミーデータを作成
/* ... */
data class Comment(
val id: String,
val name: String,
val comment: String,
val createdAt: Date
)
val 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()
),
)
/* ... */
components/Comment.kt にコメントリストアイテムを作成
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.Comment
import com.example.strongestnews.comments
import com.example.strongestnews.ui.theme.StrongestNewsTheme
@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 = comments[0]
)
}
}
CommentsContainer
を実装します
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.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.Comment
import com.example.strongestnews.comments
import com.example.strongestnews.ui.screens.comments.components.CommentItem
import com.example.strongestnews.ui.theme.StrongestNewsTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CommentsContainer(
comments: List<Comment>
){
var text by rememberSaveable(
stateSaver = TextFieldValue.Saver
) {
mutableStateOf(TextFieldValue("", TextRange(0, 7)))
}
Scaffold(
topBar = {
MediumTopAppBar(
title = { Text("StrongestNews") },
navigationIcon = {
IconButton(
onClick = {}
) {
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 = comments
)
}
}
4.8 多言語対応
Android システムでは、アプリを実行する際、デバイスのロケールに応じて自動的に読み込むリソースが選択されます。
リソースは、プロジェクトの
res/
ディレクトリ配下に作成され、言語ごとのサブディレクトリ内に作成します。テキストリソースの場合の例が下になります。
例)
-
日本語:
res/values-ja/strings.xml
-
フランス語:
res/values-fr/strings.xml
-
韓国語:
res/values-kr/strings.xml
-
デフォルト:
res/values/strings.xml
デバイスが日本語に設定されている場合、Android は
res/values-ja/strings.xml
ファイル内の値を使用します。このファイルが存在しない、または値が含まれていない場合、Android はデフォルトにフォールバックし、
res/values/strings.xml
ファイルから値を読み込みます。
実際に多言語対応してみましょう。
-
ArticlesScreen.kt
のAppBar タイトルの文字列にカーソルをあて、alt + enter
(macOS:option + return
)を押す -
Extract string resource
をクリック -
resource nameに
articles_screen_title
と入力する -
【初回のみ】日本語のリソースを作成します
-
Create the resource in directories:
の+
ボタンをクリック -
Local
を選択し、>>
をクリック -
jaを選択し
OK
をクリック
-
-
values
,values-ja
にチェックを入れOKをクリック - 言語ごとにstrings.xmlが作成されます
-
ファイルを開き文字列を変えることができます
試しに、values-ja/strings.xml で「最強ニュース」に変えてみましょう
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="articles_screen_title">最強ニュース</string> </resources>
-
この作業をすべての文字列で行います。今回はScaffoldのタイトルのみ対応します
ArticleDetailContainer
コンポーザブルとCommentContainer
コンポーザブルのトップバーに指定している文字列を書き換えます/* ... */ Scaffold( topBar = { MediumTopAppBar( title = { Text(stringResource(R.string.articles_screen_title)) }, navigationIcon = { IconButton( /* ... */
/* ... */ Scaffold( topBar = { MediumTopAppBar( title = { Text(stringResource(R.string.articles_screen_title)) }, navigationIcon = { IconButton( /* ... */
デバイスの設定を日本にすると最強ニュースと表示されるようになりました!