インタラクティブな Dice Roller アプリを作成する

1. 始める前に

この Codelab では、ユーザーが Button コンポーザブルをタップしてサイコロを振る、インタラクティブな Dice Roller アプリを作成します。サイコロを振った結果は Image コンポーザブルで画面に表示されます。

Kotlin で Jetpack Compose を使用してアプリ レイアウトを作成し、Button コンポーザブルがタップされたときの動作を処理するビジネス ロジックを作成します。

前提条件

  • Android Studio で基本的な Compose アプリを作成して実行できること。
  • アプリで Text コンポーザブルを使用する方法を熟知していること。
  • アプリの翻訳や文字列の再利用のために、テキストを文字列リソースに抽出する方法を理解していること。
  • Kotlin プログラミングの基礎知識があること。

学習内容

  • Compose を使用して Android アプリに Button コンポーザブルを追加する方法。
  • Compose を使用して Android アプリの Button コンポーザブルに動作を追加する方法。
  • Android アプリの Activity コードを開いて変更する方法。

作成するアプリの概要

  • ユーザーにサイコロを振らせてその結果を表示する、Dice Roller というインタラクティブな Android アプリ。

必要なもの

  • Android Studio がインストールされているパソコン

この Codelab が完了すると、アプリは次のようになります。

3e9a9f44c6c84634.png

2. ベースラインを確立する

プロジェクトを作成する

  1. Android Studio で、[File] > [New] > [New Project] をクリックします。
  2. [New Project] ダイアログで [Empty Activity] を選択し、[Next] をクリックします。

39373040e14f9c59.png

  1. [Name] フィールドに「Dice Roller」と入力します。
  2. [Minimum SDK] フィールドでメニューから API レベル 24(Nougat)以上を選択し、[Finish] をクリックします。

8fd6db761068ca04.png

3. レイアウト インフラストラクチャを作成する

プロジェクトをプレビューする

プロジェクトをプレビューするには:

  • [Split] ペインまたは [Design] ペインで [Build & Refresh] をクリックします。

9f1e18365da2f79c.png

[Design] ペインにプレビューが表示されます。表示が小さくても、レイアウトを変更すると表示も変化するため心配する必要はありません。

b5c9dece74200185.png

サンプルコードを再構成する

Dice Roller アプリのテーマに近づけるために、生成されたコードの一部を変更する必要があります。

完成したアプリのスクリーンショットには、サイコロの画像と、サイコロを転がすためのボタンがありました。このアーキテクチャを反映するように、コンポーズ可能な関数を構成します。

サンプルコードを再構成するには:

  1. GreetingPreview() 関数を削除します。
  2. @Composable アノテーションを付けて DiceWithButtonAndImage() 関数を作成します。

このコンポーズ可能な関数はレイアウトの UI コンポーネントを表し、また、ボタンクリックと画像表示のロジックを保持します。

  1. Greeting(name: String, modifier: Modifier = Modifier) 関数を削除します。
  2. @Preview アノテーションと @Composable アノテーションを付けて DiceRollerApp() 関数を作成します。

このアプリはボタンと画像のみで構成されているため、このコンポーズ可能な関数がアプリそのものだと考えてください。そのため、DiceRollerApp() 関数という名前になっています。

MainActivity.kt

@Preview
@Composable
fun DiceRollerApp() {

}

@Composable
fun DiceWithButtonAndImage() {

}

Greeting() 関数を削除したため、DiceRollerTheme() ラムダ本文の Greeting("Android") の呼び出しが赤色でハイライト表示されます。これは、コンパイラがその関数への参照を見つけられなくなったためです。

  1. onCreate() メソッドにある setContent{} ラムダ内のコードをすべて削除します。
  2. setContent{} ラムダ本体で DiceRollerTheme{} ラムダを呼び出し、DiceRollerTheme{} ラムダ内で DiceRollerApp() 関数を呼び出します。

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        DiceRollerTheme {
            DiceRollerApp()
        }
    }
}
  1. DiceRollerApp() 関数で、DiceWithButtonAndImage() 関数を呼び出します。

MainActivity.kt

@Preview
@Composable
fun DiceRollerApp() {
    DiceWithButtonAndImage()
}

修飾子を追加する

Compose は、Compose UI 要素の動作を装飾または変更する要素のコレクションである Modifier オブジェクトを使用します。これを使用して Dice Roller アプリのコンポーネントの UI コンポーネントをスタイル設定します。

修飾子を追加するには:

  1. Modifier 型の modifier 引数を受け入れるように DiceWithButtonAndImage() 関数を変更し、デフォルト値 Modifier を代入します。

MainActivity.kt

@Composable
fun DiceWithButtonAndImage(modifier: Modifier = Modifier) {
}

このコード スニペットではわかりにくい可能性があるため、詳しく説明します。この関数では、modifier パラメータを渡すことができます。modifier パラメータのデフォルト値は Modifier オブジェクトであるため、メソッド シグネチャに = Modifier 部分があります。このパラメータのデフォルト値により、今後このメソッドを呼び出す場合、パラメータの値を渡すかどうかを決めることができます。独自の Modifier オブジェクトを渡せば、UI の動作と装飾をカスタマイズできます。Modifier オブジェクトを渡さない場合は、デフォルトの値(プレーンな Modifier オブジェクト)であるとみなされます。この手法はどのようなパラメータにも適用できます。デフォルトの引数の詳細については、Default arguments をご覧ください。

  1. DiceWithButtonAndImage() コンポーザブルに修飾子パラメータが設定されたため、コンポーザブルが呼び出されたときに修飾子を渡します。DiceWithButtonAndImage() 関数のメソッド シグネチャが変更されたため、呼び出されたときに、必要な装飾を行った Modifier オブジェクトを渡す必要があります。Modifier クラスは、DiceRollerApp() 関数でコンポーザブルの装飾(動作の追加)を行います。この場合、DiceWithButtonAndImage() 関数に渡す Modifier オブジェクトに重要な装飾を追加します。

デフォルトがあるのに、どうしてわざわざ Modifier 引数を渡す必要があるのか、と思うかもしれません。これは、コンポーザブルが再コンポーズされる可能性があるためです。つまり実質、@Composable メソッドのコードブロックが再実行されます。Modifier オブジェクトがコードブロック内で作成された場合、再作成される可能性があるため、効率的ではありません。再コンポーズについては、この Codelab で後ほど説明します。

MainActivity.kt

DiceWithButtonAndImage(modifier = Modifier)
  1. Modifier オブジェクトに fillMaxSize() メソッドを連結して、レイアウトが画面全体に表示されるようにします。

このメソッドは、利用可能なスペースをコンポーネントで埋めることを指定します。この Codelab ではこれまでに、完成した Dice Roller アプリの UI のスクリーンショットを確認しました。注目すべき特徴は、サイコロとボタンが画面の中央に配置されていることです。wrapContentSize() メソッドは、利用可能なスペースが少なくとも内部のコンポーネントと同じ大きさである必要があるということを指定します。ただし fillMaxSize() メソッドを使用しているため、利用可能なスペースよりレイアウト内部のコンポーネントが小さい場合は、Alignment オブジェクトを wrapContentSize() メソッドに渡して、利用可能なスペース内でコンポーネントをどのように配置するかを指定できます。

MainActivity.kt

DiceWithButtonAndImage(modifier = Modifier
    .fillMaxSize()
)
  1. wrapContentSize() メソッドを Modifier オブジェクトに連結し、コンポーネントを中央に配置するための引数として Alignment.Center を渡します。Alignment.Center は、コンポーネントを縦方向と横方向の両方で中央に配置することを指定します。

MainActivity.kt

DiceWithButtonAndImage(modifier = Modifier
    .fillMaxSize()
    .wrapContentSize(Alignment.Center)
)

4. 縦向きレイアウトを作成する

Compose では、縦向きレイアウトは Column() 関数を使用して作成します。

Column() 関数は、子を縦方向に並べて配置するコンポーザブル レイアウトです。想定されるアプリデザインでは、次のようにサイコロの画像が [Roll] ボタンの上方に表示されます。

7d70bb14948e3cc1.png

縦向きレイアウトを作成するには:

  1. DiceWithButtonAndImage() 関数に Column() 関数を追加します。
  1. DiceWithButtonAndImage() メソッド シグネチャから modifier 引数を Column() の修飾子引数に渡します。

modifier 引数により、Column() 関数内のコンポーザブルが、modifier インスタンスで呼び出される制約に従うようになります。

  1. horizontalAlignment 引数を Column() 関数に渡し、Alignment.CenterHorizontally の値に設定します。

これにより、列内の子が幅に対してデバイス画面の中央に配置されます。

MainActivity.kt

fun DiceWithButtonAndImage(modifier: Modifier = Modifier) {
    Column (
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {}
}

5. ボタンを追加する

  1. strings.xml ファイルに文字列を追加し、Roll 値に設定します。

res/values/strings.xml

<string name="roll">Roll</string>
  1. Column() のラムダ本体に Button() 関数を追加します。
  1. MainActivity.kt ファイルで、関数のラムダ本体の Button()Text() 関数を追加します。
  2. roll 文字列の文字列リソース ID を stringResource() 関数に渡し、結果を Text コンポーザブルに渡します。

MainActivity.kt

Column(
    modifier = modifier,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Button(onClick = { /*TODO*/ }) {
        Text(stringResource(R.string.roll))
    }
}

6. 画像を追加する

このアプリに不可欠なもう一つのコンポーネントは、ユーザーが [Roll] ボタンをタップすると結果を表示する、サイコロの画像です。Image コンポーザブルを使用して画像を追加しますが、画像リソースが必要です。そのため、まずはこのアプリのために用意された画像をダウンロードする必要があります。

サイコロの画像をダウンロードする

  1. こちらの URL を開いて、サイコロの画像を ZIP ファイル形式でパソコンにダウンロードします。ダウンロードが完了するまで待機します。

パソコンに保存したファイルを見つけます。通常は Downloads フォルダにあります。

  1. ZIP ファイルを解凍すると、1~6 の目を持つサイコロの画像ファイルが 6 つ入った、新しい dice_images フォルダが作成されます。

アプリにサイコロの画像を追加する

  1. Android Studio で、[View] > [Tool Windows] > [Resource Manager] をクリックします。
  2. [+] > [Import Drawables] をクリックしてファイル ブラウザを開きます。

12f17d0b37dd97d2.png

  1. 6 つのサイコロの画像フォルダを見つけて選択し、アップロードに進みます。

アップロードされた画像は次のようになります。

4f66c8187a2c58e2.png

  1. [Next] をクリックします。

688772df9c792264.png

[Import drawables] ダイアログが表示され、ファイル構造内のリソース ファイルの移動先が表示されます。

  1. [Import] をクリックして、6 つの画像をインポートすることを確認します。

画像が [Resource Manager] ペインに表示されます。

c2f08e5311f9a111.png

お疲れさまでした。次のタスクでは、これらの画像をアプリで使用します。

Image コンポーザブルを追加する

サイコロの画像は [Roll] ボタンの上に表示されます。Compose は、その性質上、UI コンポーネントを順番に配置します。言い換えると、最初に宣言されたコンポーザブルが最初に表示されます。つまり、最初に宣言されたコンポーザブルが、その後に宣言されたコンポーザブルの上または前に表示されます。Column コンポーザブル内のコンポーザブルは、デバイス上で上下に重なり合って表示されます。このアプリでは、Column を使用してコンポーザブルを縦に積み重ねます。そのため、Column() 関数内で最初に宣言されたコンポーザブルは、同じ Column() 関数内で後に宣言されたコンポーザブルより前に表示されます。

Image コンポーザブルを追加するには:

  1. Column() 関数本体で、Button() 関数の前に Image() 関数を作成します。

MainActivity.kt

Column(
    modifier = modifier,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Image()
    Button(onClick = { /*TODO*/ }) {
      Text(stringResource(R.string.roll))
    }
}
  1. Image() 関数に painter 引数を渡し、ドローアブル リソース ID 引数を受け入れる painterResource 値を代入します。ここでは、リソース ID R.drawable.dice_1 引数を渡します。

MainActivity.kt

Image(
    painter = painterResource(R.drawable.dice_1)
)
  1. アプリで画像を作成するときは、必ず「コンテンツの説明」を用意してください。コンテンツの説明は、Android 開発における重要な要素です。ユーザー補助を強化するために、それぞれの UI コンポーネントに説明を付けます。コンテンツの説明について詳しくは、各 UI 要素について説明するをご覧ください。コンテンツの説明をパラメータとして画像に渡すことができます。

MainActivity.kt

Image(
    painter = painterResource(R.drawable.dice_1),
    contentDescription = "1"
)

これで、必要な UI コンポーネントがすべて揃いました。しかし、ButtonImage が少し詰まっています。

54b27140071ac2fa.png

  1. これを修正するには、Image コンポーザブルと Button コンポーザブルの間に Spacer コンポーザブルを追加します。Spacer は、パラメータとして Modifier を受け取ります。この場合、ImageButton の上にあるため、両者の間に縦方向のスペースが必要です。そのため、Modifier の高さを設定して Spacer に適用できます。高さを 16.dp に設定してみます。通常、dp ディメンションは 4.dp 単位で変更されます。

MainActivity.kt

Spacer(modifier = Modifier.height(16.dp))
  1. [Preview] ペインで、[Build & Refresh] をクリックします。

次の画像のように表示されます。

73eea4c166f7e9d2.png

7. サイコロを振るロジックを作成する

必要なコンポーザブルがすべて揃ったため、ボタンをタップするとサイコロが振られるようにアプリを変更します。

ボタンをインタラクティブにする

  1. DiceWithButtonAndImage() 関数で、Column() 関数の前に result 変数を作成し、1 値と等しくなるように設定します。
  2. Button コンポーザブルを見てみると、内部にコメント /*TODO*/ を含む中かっこのペアに設定された onClick パラメータが渡されています。ここでは、この中かっこはラムダというものを表します。中かっこの内部がラムダ本体です。関数を引数として渡す場合については、「コールバック」とも呼ばれます。

MainActivity.kt

Button(onClick = { /*TODO*/ })

ラムダは関数リテラルです。他の関数と同様に機能しますが、fun キーワードで個別に宣言するのではなく、インラインで記述し、式として渡します。Button コンポーザブルは、関数が onClick パラメータとして渡されることを想定しています。ここはラムダを使用するために最適です。このセクションではラムダ本体を記述します。

  1. Button() 関数で、onClick パラメータのラムダ本体の値からコメント /*TODO*/ を削除します。
  2. サイコロの出目はランダムです。それをコードに反映させるには、正しい構文を使用して乱数を生成する必要があります。Kotlin では、数値範囲に対して random() メソッドを使用できます。onClick ラムダ本体で、result 変数を 1~6 の範囲に設定し、その範囲に対して random() メソッドを呼び出します。Kotlin では、範囲の最初の数字と最後の数字の間にピリオドを 2 つ入れることで範囲を指定することに留意してください。

MainActivity.kt

fun DiceWithButtonAndImage(modifier: Modifier = Modifier) {
    var result = 1
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Image(
            painter = painterResource(R.drawable.dice_1),
            contentDescription = "1"
        )
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { result = (1..6).random() }) {
            Text(stringResource(R.string.roll))
        }
    }
}

これでボタンをタップできるようになりましたが、ボタンをタップしても見た目は変わりません。機能を構築する必要があります。

Dice Roller アプリに条件を追加する

前のセクションでは、result 変数を作成し、1 値にハードコードしました。最終的に、[Roll] ボタンをタップすると result 変数の値がリセットされ、表示される画像が決定されます。

コンポーザブルは、デフォルトではステートレスです。つまり、値は保持されず、システムがいつでも再コンポーズでき、結果的に値がリセットされます。しかし、Compose ではこれを簡単に回避できます。コンポーズ可能な関数は、remember コンポーザブルを使用してオブジェクトをメモリに格納できます。

  1. result 変数を remember コンポーザブルにします。

remember コンポーザブルには、渡す関数が必要です。

  1. remember コンポーザブル本体で、mutableStateOf() 関数を渡してから、その関数に 1 引数を渡します。

mutableStateOf() 関数はオブザーバブルを返します。オブザーバブルについては後ほど詳しく説明しますが、ここでは基本的に、result 変数の値が変更されると再コンポーズがトリガーされ、結果の値が反映されて、UI が更新されます。

MainActivity.kt

var result by remember { mutableStateOf(1) }

ボタンをタップすると result 変数が乱数の値で更新されます。

result 変数を使用して、表示する画像を決定できるようになりました。

  1. result 変数のインスタンス化の下で、不変の imageResource 変数を作成して result 変数を受け入れる when 式に設定し、考えられる結果をそれぞれのドローアブルに設定します。

MainActivity.kt

val imageResource = when (result) {
    1 -> R.drawable.dice_1
    2 -> R.drawable.dice_2
    3 -> R.drawable.dice_3
    4 -> R.drawable.dice_4
    5 -> R.drawable.dice_5
    else -> R.drawable.dice_6
}
  1. Image コンポーザブルの painterResource パラメータに渡される ID を R.drawable.dice_1 ドローアブルから imageResource 変数に変更します。
  2. result 変数を toString() で文字列に変換し、contentDescription として渡すことで、result 変数の値を反映するように Image コンポーザブルの contentDescription パラメータを変更します。

MainActivity.kt

Image(
   painter = painterResource(imageResource),
   contentDescription = result.toString()
)
  1. アプリを実行します。

Dice Roller アプリが完全に機能するようになりました。

3e9a9f44c6c84634.png

8. 解答コードを取得する

この Codelab の完成したコードをダウンロードするには、次の git コマンドを使用します。

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dice-roller.git

または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。

解答コードを確認する場合は、GitHub で表示します

  1. プロジェクト用に提供されている GitHub リポジトリ ページに移動します。
  2. ブランチ名が Codelab で指定されたブランチ名と一致していることを確認します。たとえば、次のスクリーンショットでは、ブランチ名は main です。

1e4c0d2c081a8fd2.png

  1. プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ポップアップが表示されます。

1debcf330fd04c7b.png

  1. ポップアップで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ちます。
  2. パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
  3. ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。

Android Studio でプロジェクトを開く

  1. Android Studio を起動します。
  2. [Welcome to Android Studio] ウィンドウで、[Open] をクリックします。

d8e9dbdeafe9038a.png

注: Android Studio がすでに開いている場合は、メニューから [File] > [Open] を選択します。

8d1fda7396afe8e5.png

  1. ファイル ブラウザで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
  2. そのプロジェクト フォルダをダブルクリックします。
  3. Android Studio でプロジェクトが開かれるまで待ちます。
  4. 実行ボタン 8de56cba7583251f.png をクリックして、アプリをビルドし、実行します。期待どおりにビルドされることを確認します。

9. おわりに

Compose を使用して Android 用のインタラクティブな Dice Roller アプリを作成しました。

まとめ

  • コンポーズ可能な関数を定義する。
  • Composition でレイアウトを作成する。
  • Button コンポーザブルでボタンを作成します。
  • drawable リソースをインポートする。
  • Image コンポーザブルを使用して画像を表示する。
  • コンポーザブルでインタラクティブな UI を作成する。
  • remember コンポーザブルを使用して、Composition 内のオブジェクトをメモリに保存する。
  • mutableStateOf() 関数で UI を更新して、オブザーバブルを作成する。

詳細