たくさんの自由帳

Jetpack Compose の例

投稿日 : | 0 日前

文字数(だいたい) : 22515

AndroidKotlinJetpackCompose
Twitterで共有GitHubで開く

追記:2021/07/15:1.0.0-rc02が出てます。

追記:2021/07/14:1.0.0-rc01が出てます。 正式リリースも近い。あとなんかAndroid Studioのプレビュー機能が4んでるみたいだから、ui-toolingだけはBeta09のままがいいらしい?
詳細:https://stackoverflow.com/questions/68224361/jetpack-compose-cant-preview-after-updating-to-1-0-0-rc01

composeOptions {
    kotlinCompilerVersion '1.5.10'
    kotlinCompilerExtensionVersion '1.0.0-rc01'
}

追記:2021/06/26:Beta09が出てます。
それと、現状?AndroidStudioのテンプレートからEmpty Compose Activityを選択してそのままの状態で実行すると起動しません。KotlinとJetpackComposeのバージョンを上げる必要があります。初見殺しだろこれ

java.lang.NoSuchMethodError: No static method copy-H99Ercs$default

build.gradle(appフォルダではない方)

buildscript {
    ext {
        compose_version = '1.0.0-beta09' // beta09へ
    }
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:7.0.0-beta04"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10" // 1.5.10へ

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

app/build.gradle(appフォルダの方のbuild.gradle)

    composeOptions {
        kotlinCompilerExtensionVersion compose_version
        kotlinCompilerVersion '1.5.10' // 1.5.10へ
    }

これで実行できると思います。

追記:2021/06/06:知らん間にJetpack Compose Beta08がリリースされました。Kotlinのバージョンを1.5.10にする必要があります。

composeOptions {
    kotlinCompilerVersion '1.5.10'
    kotlinCompilerExtensionVersion '1.0.0-beta08'
}

あと Compose版マテリアルデザインライブラリ のリファレンスが親切になっていました。Google本気やん https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#overview

【割と破壊的仕様変更】beta08からSurface()Card()のクリックイベントはModifier経由ではなく、onClickの引数を使うようになりました。

  • beta07まで
Surface(modifier = Modifier.clickable {  }) {

}
  • beta08以降
Surface(onClick = { }) {

}

そしてSurface()Card()onClick引数を使う際は、@ExperimentalMaterialApiをComposeな関数に付ける必要があります。

@ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
        Surface(onClick = { }) {
        
        }    
    }

}

追記:2021/05/22:Jetpack Composeがいつの間にかbeta07まで進んでました。
あとGoogle I/Oで言ってたけどホーム画面のウイジェットもJetpack Composeで書けるようになるとかなんとか

追記:2021/04/11:Beta04がリリースされました。多分Kotlinのバージョンを上げる必要があります。

composeOptions {
    kotlinCompilerVersion '1.4.32'
    kotlinCompilerExtensionVersion '1.0.0-beta04'
}

追記:2021/03/25:Beta03がリリースされました。AndroidViewの問題が修正されています。
ついでに、Android 7以前?で起きてたスクロール時にAndroidViewがずれる問題も直ってました。

追記:2021/03/13:Beta02がリリースされました。一緒にKotlinのバージョンを1.4.31にする必要があります。
ComposeViewの問題が修正されました(多分)。

(なお今度はスクロール時にAndroidView()がずれるようになった模様)

composeOptions {
    kotlinCompilerVersion '1.4.31'
    kotlinCompilerExtensionVersion '1.0.0-beta02'
}

また、AppCompatFragmentのライブラリのバージョンをそれぞれ1.3以上にする必要があります。

dependencies {
    implementation 'androidx.fragment:fragment-ktx:1.3.1'
    implementation 'androidx.appcompat:appcompat:1.3.0-beta01'
}

追記 2021/03/02: Jetpack Compose Beta がリリースされました!。ついに(待望の)ベータ版になります

追記 2021/02/15:Jetpack Composeのバージョンがalpha 12になりました。
影響があったといえば、

  • vectorResourceが非推奨。painterResourceを使うように。
    • よってIconへDrawableを渡すときの引数はimageVectorではなく、painterになります。
Icon(
    painter = painterResource(R.drawable.ic_outline_open_in_browser_24px),
    contentDescription = "ブラウザ起動"
)	
  • Context取得時に使う、AmbientContext.currentLocalContext.currentに変更になりました。

  • Android Studioが対応してないのか、Kotlinのバージョンを1.4.30にしても、バージョンが古いので利用できませんってIDEに言われます。

    • エラー出るけど実行はできる。修正待ち
    • 'padding(Dp): Modifier' is only available since Kotlin 1.4.30 and cannot be used in Kotlin 1.4
    • 解決方法は、設定を開き、Languages & Frameworksへ進み、Kotlinを選び、Update ChannelEarly Access Preview 1.4.xにしてInstallを押せばいいらしい。

追記:Iconの増えてるのでコピペじゃ動かなくなりました。
contentDescriptionという文字列を入れる引数が増えてます。ので、コピペしたらIconの引数を足してください。以下のように

Icon(
    imageVector = Icons.Outlined.Home,
    contentDescription = "アイコンの説明。なければnullでもいい"
)

この続きです。そのうち追記しに来る

https://takusan.negitoro.dev/posts/android_jc/

Snackbar表示

Scaffold { }で囲ってあげる必要があります。
Snackbar表示以外でもアプリバーとかドロワーの表示でも使うので置いておいて損はないはず。

また、Compose内で利用できるコルーチン(rememberCoroutineScope())を利用する必要があります。

@ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        MaterialTheme {
            val state = rememberScaffoldState()
            val scope = rememberCoroutineScope()
            Scaffold(
                scaffoldState = state,
                topBar = {
                    TopAppBar() {
                        Column(
                            horizontalAlignment = Alignment.CenterHorizontally,
                            verticalArrangement = Arrangement.Center,
                            modifier = Modifier.fillMaxHeight().padding(10.dp),
                        ) {
                            Text(text = "ブログ一覧")
                        }
                    }
                }
            ) {
                OutlinedButton(onClick = {
                    scope.launch {
                        val result = state.snackbarHostState.showSnackbar(
                            message = "Snackbar表示",
                            actionLabel = "押せます",
                            duration = SnackbarDuration.Short,
                        )
                        // 押せたかどうか
                        if (result == SnackbarResult.ActionPerformed) {
                            Toast.makeText(this@MainActivity, "押せました!", Toast.LENGTH_SHORT).show()
                        }
                    }
                }) {
                    Text(text = "Snackbar表示")
                }
            }
        }
    }
}

実行結果

Imgur

参考にしました:https://gist.github.com/gildor/82ec960cc0c5873453f024870495eab3

Context取得

リソース取得等は用意されてるけど、それ以外でContextを使いたい場合はこうです!

@Composable
fun needContext() {
    val context = LocalContext.current
}

コルーチンは?

なんかコルーチン、2種類あるっぽい。

LaunchedEffect vs rememberCoroutineScope

なんか警告出てたので追記(2021/04/19)

LaunchedEffect

これは@Composableがついた関数内でしか呼べません。
Button()Text()を置く感じで使うことになります。

@Composable
fun TimerText() {
    val timerCount = remember { mutableStateOf(0) }
    /**
     * ここはコンポーザブルのスコープ内
     *
     * Button()やText()が設置可能
     * */
    LaunchedEffect(key1 = Unit, block = {
        while (true) {
            timerCount.value += 1
            delay(1000)
        }
    })
    Text(text = "${timerCount.value} 秒経過")
}

また、key1の中身が変わると、今のコルーチンはキャンセルされ、新しいコルーチンが起動するようになっています。

@Composable
fun TimerText() {
    val timerCount = remember { mutableStateOf(0) }
    val isRunning = remember { mutableStateOf(false) }
    /**
     * Button()やText()が設置可能な場所
     *
     * Composable内で利用できるコルーチン
     *
     * key1が変更されると、既存のコルーチンはキャンセルされ、新しくコルーチンが起動する
     * */
    LaunchedEffect(key1 = isRunning.value, block = {
        timerCount.value = 0
        while (isRunning.value) {
            timerCount.value += 1
            delay(1000)
        }
    })
    Button(onClick = {
        isRunning.value = !isRunning.value
    }) {
        if (isRunning.value) {
            Text(text = "${timerCount.value} 秒経過")
        } else {
            Text(text = "タイマー開始")
        }
    }
}

rememberCoroutineScope

じゃあrememberCoroutineScopeはなんだよって話ですが、これはComposableな関数ではないところで使うのが正解らしいです。
(例えば、ボタンを押したときに呼ばれる関数はComposableな関数ではない)

@Composable
fun RememberCoroutine() {
    val scope = rememberCoroutineScope()
    val context = LocalContext.current

    Button(onClick = {
        // ここはComposableな関数ではない
        scope.launch {
            // Toast表示が終わるまで一時停止する
            suspendToast(context)
            println("終了")
        }
    }) {
        Text(text = "rememberCoroutineScope")
    }
}

/** Toast表示が終わるまで一時停止する関数 */
suspend fun suspendToast(context: Context) {
    Toast.makeText(context, "rememberCoroutineScope", Toast.LENGTH_SHORT).show()
    delay(2 * 1000) // 2秒ぐらい
}

あってるか分からないので、詳しくは公式で
https://developer.android.com/jetpack/compose/lifecycle

テーマとか文字の色にカラーコードを使いたい!

Color.parseColor()がそのままでは使えないので、androidx.compose.ui.graphicsの方のColorの引数に入れてあげます。

Text(
    text = AnniversaryDate.makeAnniversaryMessage(anniversary),
    color = Color(android.graphics.Color.parseColor("#252525"))
)

ダークモード

まずはThemeColor.ktみたいな色だけを書いておくクラスを作ってはりつけ
なんかisDarkMode@Composableを付ける理由はわかりません。サンプルコードがそうなってたので便乗
@ComposableをつけるとLocalContext等へアクセスできる。
つけない場合だと(今回の場合は)引数にContextが必要になる。なるほどなあ

// 引数にContextが必要
fun isDarkMode(context: Context) {}

// いらない
@Composable
fun isDarkMode() {
    val context = LocalContext.current
}

ここから例です

/**
 * [MaterialTheme]に渡すテーマ。コードでテーマの設定ができるってマジ?
 * */

/** ダークモード。OLED特化 */
val DarkColors = darkColors(
    primary = Color.White,
    secondary = Color.Black,
)

/** ライトテーマ */
val LightColors = lightColors(
    primary = Color(android.graphics.Color.parseColor("#757575")),
    primaryVariant = Color(android.graphics.Color.parseColor("#494949")),
    secondary = Color(android.graphics.Color.parseColor("#a4a4a4")),
)

/** ダークモードかどうか */
@Composable
fun isDarkMode(): Boolean {
    // ComposableをつけるとLocalContext等、Composable内でしか呼べない関数を呼べる(それはそう)
    // 今回はContextを引数に取らなくてもLocalContextを使うことが出来た
    val context = LocalContext.current
    val conf = context.resources.configuration
    val nightMode = conf.uiMode and Configuration.UI_MODE_NIGHT_MASK
    return nightMode == Configuration.UI_MODE_NIGHT_YES // ダークモードなら true
}

そしたらMaterialTheme { }に渡してあげます。if文を一行で書く

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme(
                // 色の設定
                colors = if (isDarkMode(AmbientContext.current)) DarkColors else LightColors
            ) {
                Scaffold {
                    Column(
                        modifier = Modifier.fillMaxWidth().fillMaxHeight(),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally,
                    ) {
                        // この2つ、ダークモードなら白色、それ以外なら黒色になるはず
                        Text(text = "もじのいろ")
                        Icon(imageVector = Icons.Outlined.Home)
                    }
                }
            }
        }
    }
}

ちゃんと動けばダークモードのときは真っ暗になると思います。AOD

Imgur

ちなみに黒基調にするとIcon()等が勝手に検知してアイコンの色を白色に変更してくれるそうです。ダークモード対応の手間が減る

タブレイアウト

見つけたので報告しますね。そんなに難しくない。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme(
                // 色の設定
                colors = if (isDarkMode(AmbientContext.current)) DarkColors else LightColors
            ) {
                Scaffold {
                    Column(
                        modifier = Modifier.fillMaxWidth().fillMaxHeight(),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally,
                    ) {

                        // 選択中タブ
                        var selectTabIndex by remember { mutableStateOf(0) }

                        TabLayout(
                            selectTabIndex = selectTabIndex,
                            tabClick = { index -> selectTabIndex = index }
                        )
                        Text(text = "選択中:$selectTabIndex")

                    }
                }
            }
        }
    }
}


/**
 * タブレイアウト
 *
 * @param selectTabIndex 選択するタブを入れてね
 * @param tabClick タブを押した時
 * */
@Composable
fun TabLayout(selectTabIndex: Int, tabClick: (Int) -> Unit) {
    TabRow(
        modifier = Modifier.padding(10.dp),
        selectedTabIndex = selectTabIndex,
        backgroundColor = Color.Transparent,
    ) {
        Tab(selected = selectTabIndex == 0, onClick = {
            tabClick(0)
        }) {
            Icon(imageVector = Icons.Outlined.Android)
            Text(text = "Android 9")
        }
        Tab(selected = selectTabIndex == 1, onClick = {
            tabClick(1)
        }) {
            Icon(imageVector = Icons.Outlined.Android)
            Text(text = "Android 10")
        }
        Tab(selected = selectTabIndex == 2, onClick = {
            tabClick(2)
        }) {
            Icon(imageVector = Icons.Outlined.Android)
            Text(text = "Android 11")
        }
    }
}

動作結果

Imgur

動的にテーマを変える

MaterialThemecolorsの部分を変えることでテーマを切り替えられるようになりました。これ従来のレイアウトじゃできないからComposeの強みじゃない?

まずは色の情報を置いておくクラスを作成して、以下をコピペします。

ThemeColor.kt

/** ダークモード。OLED特化 */
val DarkColors = darkColors(
    primary = Color.White,
    secondary = Color.Black,
)

/** ライトテーマ */
val LightColors = lightColors(
    primary = Color(android.graphics.Color.parseColor("#FF6200EE")),
    primaryVariant = Color(android.graphics.Color.parseColor("#FF3700B3")),
    secondary = Color(android.graphics.Color.parseColor("#FFFFFF")),
)

/** 青基調 */
val blueTheme = lightColors(
    primary = Color(android.graphics.Color.parseColor("#0277bd")),
    primaryVariant = Color(android.graphics.Color.parseColor("#58a5f0")),
    secondary = Color(android.graphics.Color.parseColor("#004c8c")),
)

/** 赤基調 */
val redTheme = lightColors(
    primary = Color(android.graphics.Color.parseColor("#c2185b")),
    primaryVariant = Color(android.graphics.Color.parseColor("#8c0032")),
    secondary = Color(android.graphics.Color.parseColor("#fa5788")),
)

/** 緑基調 */
val greenTheme = lightColors(
    primary = Color(android.graphics.Color.parseColor("#1b5e20")),
    primaryVariant = Color(android.graphics.Color.parseColor("#003300")),
    secondary = Color(android.graphics.Color.parseColor("#4c8c4a")),
)

/** ダークモードかどうか */
@Composable
fun isDarkMode(context: Context): Boolean {
    val conf = context.resources.configuration
    val nightMode = conf.uiMode and Configuration.UI_MODE_NIGHT_MASK
    return nightMode == Configuration.UI_MODE_NIGHT_YES // ダークモードなら true
}

その後に書いていきます。ダークモードを一緒に書いた人はこっから掛けばいいです。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {

            // デフォルト
            val defaultTheme = if (isDarkMode(AmbientContext.current)) DarkColors else LightColors

            // 色を保持する
            val themes = remember { mutableStateOf(defaultTheme) }

            MaterialTheme(
                // 色の設定
                colors = themes.value
            ) {
                Scaffold(
                    topBar = {
                        TopAppBar() {
                            Column(
                                horizontalAlignment = Alignment.CenterHorizontally,
                                verticalArrangement = Arrangement.Center,
                                modifier = Modifier.fillMaxHeight().padding(10.dp),
                            ) {
                                Text(text = "動的テーマ")
                            }
                        }
                    }
                ) {
                    // テーマ切り替え
                    DynamicThemeButtons(themeClick = { themes.value = it})
                }
            }
        }
    }
}

/**
 * 動的にテーマを切り替える。
 *
 * @param themeClick ボタンを押したときに呼ばれる。引数にはテーマ([Colors])が入ってる
 * */
@Composable
fun DynamicThemeButtons(
    themeClick: (Colors) -> Unit,
) {

    // デフォルト
    val defaultTheme = if (isDarkMode(context = AmbientContext.current)) DarkColors else LightColors

    Column(
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Button(
            modifier = Modifier.padding(5.dp),
            onClick = { themeClick(blueTheme) }
        ) {
            Text(text = "青")
        }
        Button(modifier = Modifier.padding(5.dp),
            onClick = { themeClick(redTheme) }
        ) {
            Text(text = "赤")
        }
        Button(modifier = Modifier.padding(5.dp),
            onClick = { themeClick(greenTheme) }
        ) {
            Text(text = "緑")
        }
        Button(modifier = Modifier.padding(5.dp),
            onClick = { themeClick(defaultTheme) }
        ) {
            Text(text = "デフォルト")
        }
    }

}

実行結果

ボタンを押すと色が切り替わると思います。

Imgur

Imgur

表示、非表示をアニメーションしてほしい

AnimatedVisibilityってのがあります。

class MainActivity : AppCompatActivity() {
    
    @ExperimentalAnimationApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {

            // デフォルト
            val defaultTheme = if (isDarkMode(AmbientContext.current)) DarkColors else LightColors

            // 色を保持する
            val themes = remember { mutableStateOf(defaultTheme) }

            MaterialTheme(
                // 色の設定
                colors = themes.value
            ) {
                VisibilityAnimationSample()
            }
        }
    }
}

@ExperimentalAnimationApi
@Composable
fun VisibilityAnimationSample() {
    // 表示するか
    val isShow = remember { mutableStateOf(false) }
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        AnimatedVisibility(visible = isShow.value) {
            // この中に書いたやつがアニメーションされながら表示される
            Column {
                // 10個ぐらい
                repeat(10) {
                    Icon(imageVector = Icons.Outlined.Android, modifier = Modifier.rotate(90f * it))
                }
            }
        }
        // 表示、非表示切り替え
        Button(onClick = { isShow.value = !isShow.value }) {
            Text(text = "アニメーションさせながら表示")
        }
    }
}

画像じゃわからんけど、ちゃんとアニメーションされてます。

Imgur

右寄せ

android:gravity="right"をJetpack Composeでもやりたいわけですね。重要な点はfillMaxWidth()を使うところです

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            GravityRight()
        }
    }
}

@Composable
fun GravityRight() {
    Column {
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.End,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Text(text = "右に寄ってる Row")
            IconButton(onClick = { /*TODO*/ }) {
                Icon(imageVector = Icons.Outlined.Adb)
            }
        }
        Divider() // 区切り
        Column(
            modifier = Modifier.fillMaxWidth(),
            horizontalAlignment = Alignment.End,
            verticalArrangement = Arrangement.Center
        ) {
            Text(text = "右に寄ってる Column")
            IconButton(onClick = { /*TODO*/ }) {
                Icon(imageVector = Icons.Outlined.Adb)
            }
        }
    }
}

こうなるはず

Imgur

均等に並べる

LinearLayoutと同じようにweightを設定すればできます。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            WeightSample()
        }
    }
}

@Composable
fun WeightSample() {
    Row(
        modifier = Modifier.fillMaxWidth()
    ) {
        Button(modifier = Modifier.weight(1f).padding(5.dp), onClick = { /*TODO*/ }) {
            Text(text = "ぼたんだよー")
        }
        Button(modifier = Modifier.weight(1f).padding(5.dp), onClick = { /*TODO*/ }) {
            Text(text = "ぼたんだよー")
        }
        Button(modifier = Modifier.weight(1f).padding(5.dp), onClick = { /*TODO*/ }) {
            Text(text = "ぼたんだよー")
        }
    }
}

こうなるはず

Imgur

余りのスペースを埋める

埋めたい部品に対してweight(1f)を足してあげることで、他の部品の事を考えながら埋めたい部品で埋めてくれます。
こっちも親要素にfillMaxWidth()を指定してあげる必要があります。(画面いっぱい使うなら)

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MessageSendUI()
        }
    }
}

@Composable
fun MessageSendUI() {
    Row(
        modifier = Modifier.fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically
    ) {
        val message = remember { mutableStateOf("") }
        // この部品を最大まで広げたい
        OutlinedTextField(
            value = message.value,
            onValueChange = { message.value = it },
            modifier = Modifier
                .padding(5.dp)
                .weight(1f)
        )
        IconButton(
            onClick = { /*TODO*/ },
            modifier = Modifier.padding(5.dp)
        ) {
            Icon(imageVector = Icons.Outlined.Send)
        }
    }
}

こうなるはず

Imgur

もっとサンプル書け!

Jetpack Composeを書いてる人のサンプルには勝てないというわけでソースコードの探し方でも。

1.Android Studioで使いたいUI部品のソースコードを開く

ButtonとかOutlinedButtonとかMaterialThemeの部分でCtrl 押しながら クリックすることで飛べます。

こんなのが出ると思う

Imgur

2.ブラウザで

https://cs.android.com/

を開きます。

3.@sample の部分を探します。

さっきの画像だと、

@sample androidx.compose.material.samples.OutlinedButtonSample

のところですね

4.検索欄に入れる

で、何を検索欄に入れればいいんだって話ですが、さっき見つけた@sample ~の部分、
最後から.までの部分を入力します。

@sample androidx.compose.material.samples.OutlinedButtonSample

だと、OutlinedButtonSampleがそうです。

早速検索欄に入れましょう

Imgur

5.コードを読み解く

検索欄に入れたらおそらく一番最初のサジェストが使い方の例になってると思います。

あとは読んでいくしか無いです。

Imgur

サンプルアプリ

今までやったこと(だいたい)を一つのアプリにしてみました。どうぞ。

https://github.com/takusan23/JetpackComposeSampleApp

ダウンロード

https://github.com/takusan23/JetpackComposeSampleApp/releases/tag/1.0