🚀 ニフティ’s Notion

Androidアプリ開発入門#4

第4章 画面作成

4.1 Activity

画面描画にはActivityを使用します。

Activityは画面を管轄するコンポーネントです。具体的には以下のような役割を担っています。

  • レイアウトファイルを読み込み、画面を作成
  • 画面の書き換え
  • 画面イベント(ボタンクリックなど)の処理

今回は1つのActivity上で、複数のComposeを書き換えていくことで各画面を作っていきます。

4.1.1 TaskとActivity

起動したActivityは、 Task という単位で管理されます。「■ボタン (タスク一覧)」を押したときに現れる、アプリ一覧のようなものの1つ1つがTaskです。

image block

Taskの中はスタック構造になっており、その中にActivityが積み上がる形になっています。一番表にあるActivityだけがユーザに見えることによって、画面遷移が実現されます。

このとき、一番上以外のActivityは停止状態になることで、リソースの消費を抑えています。

image block

Icon
このActivity遷移、実は遷移先が他のアプリであっても同じ挙動になります。つまり、 1つのTaskに複数アプリが乗る 可能性があるのです。

ある操作をして、別のアプリに行って、戻ってくる、この一連の動作を「タスク」と捉え、1つの作業単位とするのがAndroidの考え方です。この辺りの思想も、最初からアプリ連携を前提としたプラットフォームならではのものと言えます。

ただし動作が分かりにくくなるという面もあるので、「別アプリは別Taskで起動する」ようなオプションも存在します。

4.2 ライフサイクル

Activityは OSにより起動され、OSにより管理されます 。したがって、アプリ側で起動・停止のタイミングを自由に記述することができません。

このため、OSが何かをするタイミングで処理を差し込む方法が用意されています。これをActivityの ライフサイクル と呼び、特定タイミングで呼ばれるメソッドを ライフサイクルメソッド と言います。適切な場所に適切な処理を入れる必要があります。

image block

最低限覚えておく必要があるのは以下です。

onCreate()

Activityが作成されたときに呼び出されます。ここでレイアウトファイルをもとに画面を作成します。

画面オブジェクトを後から操作する必要がある場合、そのオブジェクトの取得も同時に行います。

今回使用するUIツールキットのJetpackComposeのComposeの呼び出しはこのonCreateで行います。

onResume() / onPause()

Activityが裏面に回るなどして停止状態になるとonPause()、再開するとonResume()が呼ばれます。

画面の更新処理は必ずActivityが起動状態であるときに行わなければなりません。停止状態のActivityを操作してしまうと、 NullPointerExceptionでエラー落ち します。

したがって、画面を更新するようなイベントはonResume()で開始、onPause()で停止させます。

ComposeとCoroutineを適切に使えば自力で停止させる処理を書く必要がなくなりますが、適切に使わないとNullPointExceptoinでエラー落ちします。

onDestroy()

Activityが破棄される前に呼び出されます。次のいずれかの理由でシステムによって呼ばれます。

  1. Activityが終了する
    1. ユーザがActivityを完全に閉じる
    2. Activityに対して finish() が呼び出される
  2. デバイスの向きの変更やマルチウィンドウモードなどの構成の変更に伴い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
    )
}
/*・・・*/
MainActivity.kt
プレビュー

パラメータのないコンポーズ可能な関数またはデフォルトパラメータを含む関数に  @Preview アノテーションを付けて、プロジェクトをビルドします。

/*・・・*/

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    StrongestNewsTheme {
        Greeting("Android")
    }
}
MainActivity.kt

右上のSplitレイアウトで表示することで、プレビューを見ることができます。(ビルドしていないと表示されません)

image block
4.4 記事一覧画面を作成する

次のような画面を作成します

image block

4.4.1 スクリーンを置く用のパッケージを作成
  1. ui配下に com.example.strongestnews.ui.screens パッケージを新規作成します。
    image block
    image block
  2. 作成したscreensパッケージに、さらに com.example.app.strongestnews.ui.screens.articles パッケージを追加します。
    image block
4.4.2 記事一覧のコンポーネントを作成する

まずは以下のコンポーネントを作ります。

image block

このコンポーネントでは記事のサムネイル画像、作成日、タイトルを表示します。

4.4.3 ファイル作成
  1. ui.screens.articles 配下に com.example.strongestnews.ui.screens.articles.components パッケージを新規作成します。
    image block
  2. components パッケージに ArticleCard.kt を作成します。
    image block
4.4.4 ArticleCardコンポーザブルを実装
  1. 必要なライブラリを導入する

    画像表示には外部ライブラリである coil-compose AsyncImage() を用います。

    ライブラリの導入方法にはGUIを使った方法とbuild.gradleを使った方法の2種類あります。どちらの方法も記載しているので、好きな方でライブラリを追加してください。

    coil-composeの追加

    • GUIで追加

      ツールバー > Project Structure… > Dependencies > app > + > Library Dependency から検索画面を開く

      image block

      org.jetbrains.kotlinx:kotlinx-serialization-json で検索し、 2.4.0 を選択してOKを押す

      image block

      入れたらApplyしてからOKで閉じる

    • app/build.gradleから追加

      app/build.gradleのdependenciesに以下を追記

      implementation("io.coil-kt:coil-compose:2.4.0")
      app/build.gradle

      File > Sync Project with Gradle Files を実行

      image block

  2. アプリのインターネット通信を許可する
    1. 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
  3. 関数を作成し、引数でタイトル、サムネイル画像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
    ){
        
    }
    ArticleCard.kt
  4. 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()
            )
        }
    }
    ArticleCard.kt

    様々なサイズでの表示も確認するため横幅を設定した複数の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()
            )
        }
    }
    ArticleCard.kt
  5. 画像のplaceholderを準備します
    1. 左上からResourceManagerを開き、+ボタンで Drawable Resource File を選択
      image block
    2. File nameに placeholder と入力してOKを押します
      image block
    3. 先ほど作成した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>
      placeholder.xml
    4. Colorタブを開き、+ボタンからColor Valueを選択
      image block
      image block
    5. New Color Valueに以下の通りに入力し、OK
      • Resource name: placeholder_gray
      • Resource value: #FFC7C7C7
      image block
  6. 見た目を実装します
    1. まずはカードっぽい見た目にしたいので、そのまま Card というコンポーザブルを使います

      また、横幅いっぱいに広がって欲しいので、 Modifier fillMaxWidth を指定します

      Icon
      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()
          ) {
      
          }
      
      }
      
      /* ... */
      ArticleCard.kt
    2. 次に、画像やタイトルなどが縦に並んでほしいので、 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 {
      
              }
          }
      
      }
      
      /* ... */
    3. カードの上部にサムネイル画像を表示させたいので、 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
                  )
              }
          }
      }
      
      /* ... */
    4. カードの下部には日付とタイトルを余白を持って縦に並びたいので、 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
                      )
                  }
              }
      		}
      }
      
      /* ... */
    5. ビルドするとプレビューを確認できます
      image block

      いい感じにできているでしょうか?プレビューでは画像が表示されないので、プレースホルダーが表示されています

      もし血染めコードになっている場合、同じ名前の違うコンポーザブルをimportしている場合もあるので、確認してみてください

  7. 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 を作成します。

image block
  1. ui.screens.articles 配下に ArticlesScreen.kt を新規作成します
    image block
  2. 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()
        }
    }
    ArticlesScreen.kt

    ここで最上位にある Scaffold とは、トップバーやナビゲーションなどを表示する際に使う、アプリの足場となるものを提供してくれるコンポーザブルです。

    今回は topBar を指定することで、トップバーを作成しています。

  3. 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()
                    }
                }
            }
        }
    }
    MainActivity.kt

    アプリをビルドして実行してみます。 Run ‘app’ で実行できます。

    image block

    以下のようになっていたらOKです。(色味は端末によって違う可能性があります)

    image block

  4. 記事のダミーデータを作成し、記事の数だけAritcleコンポーザブルが表示されるように変更します。表示されるデータもダミーデータを参照するように修正します。
    1. まずはダミーデータに使用するデータクラスを作成します。(とりあえずMainActivity.ktに宣言して、後々リファクタしていきます。)
      /*・・・*/
      
      data class Article(
          val id: String,
          val title: String,
          val imageURL: URL,
          val createdAt: Date
      )
      
      class MainActivity : ComponentActivity() {
      /*・・・*/
      MainActivity.kt
    2. ダミーデータを作成します
      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() {
      /* ... */
      MainActivity.kt
    3. ArticlesContainer でダミーデータを受け取るようにします
      /* ... */
      import com.example.strongestnews.Article
      
      @Composable
      fun ArticlesContainer(
      	articles: List<Article>,
      ) {
      /* ... */
      ArticlesScreen.kt
    4. ArticlesContainer の引数が増えたので、呼び出し元であるPreviewとMainActivityを修正します
      /* ... */
      import com.example.strongestnews.articles
      
      /* ... */
      
      @Preview(showBackground = true)
      @Composable
      fun ArticlesPreview() {
          StrongestNewsTheme {
              ArticlesContainer(
                  articles = articles
              )
          }
      }
      ArticlesScreen.kt
      /* ... */
      
      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

  5. 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 が縦に並ぶようになりました。

    image block
  6. 画面がギチギチなので、 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 = {}
                    )
                }
            }
        }
    }
    ArticlesScreen.kt
  7. スクロールできるようにする

    スクロール判定が 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 = {},
    								)
                }
            }
        }
    }
  8. ビルドすると、記事一覧画面が表示され、スクロールができるようになりました
    image block

4.5 記事詳細画面を作成 (演習)

次のような画面を作成します

image block
4.5.1 事前準備
  1. screens配下に articleDetail パッケージを追加し、 ArticleDetailScreen.kt を作成する
    image block
  2. MainActivity.ktにある Article のデータクラスに記事本文のフィールドを追加する
    data class Article(
        val id: String,
        val title: String,
        val imageURL: URL,
        val createdAt: Date,
        val detail: String
    )
    MainActivity.kt

    ダミーデータにも 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 = "ニフティから超絶爆裂に早い回線がリリースされました!やばい!すごい!スゴツヨ!"
        ),
    )
  3. 空の ArticleDetailContainer コンポーザブルを用意する

    Article を受け取る空の関数を作成します

    package com.example.strongestnews.ui.screens.articleDetail
    
    import androidx.compose.runtime.Composable
    import com.example.strongestnews.Article
    
    @Composable
    fun ArticleDetailContainer(
        article: Article
    ){
    
    }
    ArticleDetailScreen.kt
  4. ビルドしたら記事詳細画面が表示されるようにする

    まだ画面遷移を実装していないので、ビルドしたら記事詳細画面が表示されるように 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])
                    }
                }
            }
        }
    }
    MainActivity.kt
4.5.2 記事詳細画面を実装してみる

記事一覧画面を参考にしながら、自力で ArticleDetailContainer を実装してみましょう

image block

記事を表示する画面のパーツ

  • サムネイル画像
  • 作成日
    • 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])
    }
}
ArticleDetailScreen.kt
4.6 再コンポーズ

Compose は Composable が受け取るデータを追跡し、データが変更された時はコンポーネントを再コンポーズし、データの変更の影響を受けないコンポーネントはスキップします。

状態トラッキングをするために、Compose の State 型と MutableState 型を使用します。

4.6.1 記事詳細画面のいいねボタンの動作を実装する

いいねボタンは押した回数だけ数字が増えていきます。

  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(
    
    /* ...
    ArticleDetailScreen.kt
  2. ボタンをクリックすると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 = {
    	           /*...*/
    					},
    				)
              
    }
    ArticleDetailScreen.kt

これで再コンポーズのトリガーは正常に機能していますが、再コンポーズの際に 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 = {

/* ... */
ArticleDetailScreen.kt

ビルドして、いいねボタンの数が増えることを確認します

image block

Icon
Kotlinの委譲プロパティを使用して簡素化する

値の参照に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 = {

/* ... */
ArticleDetailScreen.kt

Icon
用語
  • Composable
    • 画面に表示する
  • コンポジション
    • Jetpack Composeがコンポーザブルの実行時に構築するUIの記述
  • 初回コンポーズ
    • コンポーザブルを初めて実行して、コンポジションを作成すること
  • 再コンポーズ
    • データ(状態)が変更されたとき、コンポジションを更新するためにコンポーザブルを再実行すること
4.7 コメント一覧画面を作成

次のような画面を作成します

image block
image block
  1. screens配下にcommentsパッケージを追加し、CommentsScreen.ktを作成する
    image block
  2. 画面通り作ってみましょう!
    ヒント
実装例

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()
    ),
)

/* ... */
Activity.kt

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]
        )
    }
}
components/Comment.kt

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
        )
    }
}
CommentsScreen.kt
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  ファイルから値を読み込みます。

実際に多言語対応してみましょう。

  1. ArticlesScreen.kt のAppBar タイトルの文字列にカーソルをあて、 alt + enter (macOS: option + return )を押す
  2. Extract string resource をクリック
    image block
  3. resource nameに articles_screen_title と入力する
    image block
  4. 【初回のみ】日本語のリソースを作成します
    1. Create the resource in directories: + ボタンをクリック
    2. Local を選択し、 >> をクリック
    3. jaを選択し OK をクリック
      image block
  5. values , values-ja にチェックを入れOKをクリック
    image block
  6. 言語ごとにstrings.xmlが作成されます
    image block
  7. ファイルを開き文字列を変えることができます
    image block

    試しに、values-ja/strings.xml で「最強ニュース」に変えてみましょう

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <string name="articles_screen_title">最強ニュース</string>
    </resources>
  8. この作業をすべての文字列で行います。今回はScaffoldのタイトルのみ対応します

    ArticleDetailContainer コンポーザブルと CommentContainer コンポーザブルのトップバーに指定している文字列を書き換えます

    /* ... */
    Scaffold(
            topBar = {
                MediumTopAppBar(
                    title = { Text(stringResource(R.string.articles_screen_title)) },
                    navigationIcon = {
                        IconButton(
    
    /* ... */
    ArticleDetailScreen.kt
    /* ... */
    
    Scaffold(
            topBar = {
                MediumTopAppBar(
                    title = { Text(stringResource(R.string.articles_screen_title)) },
                    navigationIcon = {
                        IconButton(
    
    /* ... */
    CommentsScreen.kt

    デバイスの設定を日本にすると最強ニュースと表示されるようになりました!