2021/07/06

UI ComponentにJetpackComposeを採用したアプリを開発してみました

kotlinjetpack-compose

Jetpack Composeをメインで使ったアプリをリリースしました。

Jetpack Composeとは

Jetpack Composeは宣言的なコードでUIを記述するためのフレームワークです。
簡単にいうと、Htmlみたいな感じでUIを作ることができます。

SwiftUIやFlutterなんかもその流れをくんでいるので、メインストリームになってきているんだと思います。

私はWebアプリの開発の経験が多く、Html(最近はNuxtjsがメイン)のような感じでUI構築ができる点で、Jetpack Composeはとてもうれしい存在です。

従来のxmlを使ったレイアウトの開発は、何かデザインツール(photoshopとかsketchとか)をいじっている感じがして苦手でした。

今回Jetpack Composeを使ってみてとても良かったので、(私は)今後のAndroidアプリ開発では必ず、Jetpack Composeを採用しようと考えています。

・・ですが、まだたまにLayout周りで原因不明のクラッシュがあったりするので、(大きめのアプリやクラッシュにシビアなタイプのアプリケーションであれば)安定版がリリースされるまでJetpack Composeの採用は先延ばしにした方が良いと思います。

Jetpack Composeを使ったUI Componentの例

ついですが、今回はJetpack Composeで、ステータスアイコン付きのアバターUIコンポーネントの作り方を紹介します。

以下のような見た目のAvatarコンポーネントを作ってみます。

avatar-min.png

完成版

以下のようなソースコードになりました。

import android.content.res.Configuration
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.google.accompanist.coil.rememberCoilPainter

@Composable
fun Avatar(
    imageUrl: String,
    size: Dp = 50.dp,
    statusColor: Color? = null,
) {
    val darkTheme: Boolean = isSystemInDarkTheme()
    Box(contentAlignment = Alignment.TopEnd) {
        Image(
            painter = rememberCoilPainter(imageUrl),
            contentDescription = null,
            modifier = Modifier
                .size(size)
                .clip(CircleShape)
                .border(1.dp, if (darkTheme) Gray600 else Gray200, CircleShape),
            contentScale = ContentScale.Crop
        )
        statusColor?.let { color ->
            val statusColorSize = size / 5
            Canvas(
                modifier = Modifier
                    .size(statusColorSize)
                    .offset(x = -statusColorSize / 4, y = statusColorSize / 4)
                    .border(1.dp, if (darkTheme) Gray800 else Gray50, CircleShape)
            ) {
                val statusColorSizePx = statusColorSize.toPx()
                drawCircle(
                    color = color,
                    radius = statusColorSizePx / 2f,
                )
            }
        }
    }
}

@Preview("avatar default")
@Preview("avatar dark theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun AvatarPreview() {
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = if (isSystemInDarkTheme()) Gray800 else Gray50
    ) {
        Box(contentAlignment = Alignment.Center) {
            Avatar(
                imageUrl = "https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80",
                statusColor = Green500,
            )
        }
    }
}

ちょっとした解説

step1 Boxで囲む

まずは外側をBoxで囲みます。

Box(contentAlignment = Alignment.TopEnd) {
  // content
}

これはSwiftUIでいえばZStackで、htmlだったらdiv(ちょっと違う?)みたいな感じでしょうか。

このBoxにstyelをつけるような感じで、contentAlignmentTopEndを指定します。(ステータスアイコンを右上隅に配置したい)すると、中身が右上隅から配置されるような感じになります。

step2 円形の画像を配置する

先程作成したBoxの中に画像を配置します。

Box(contentAlignment = Alignment.TopEnd) {
    // Imageを追加
    Image(
        painter = rememberCoilPainter(imageUrl),
        contentDescription = null,
        modifier = Modifier
            .size(size)
            .clip(CircleShape)
            .border(1.dp, if (darkTheme) Gray600 else Gray200, CircleShape),
        contentScale = ContentScale.Crop
    )
}

rememberCoilPaintercoilというgoogle製のライブラリの関数で、画像のurlを元に非同期で画像データをインターネットから取得し、描画までしてくる優れものです。

ここで重要なのが、modifierで、styleを細かく変更するときに使います。

今回は、以下のような感じで指定しています。

  • Modifier.size(size): アバター画像の表示サイズを設定
  • Modifier.clip(CircleShape): 円形に切り取り
  • Modifier.border(1.dp, Gray200, CircleShape): グレーの色で、1pxの円形のボーダーを設定

また、以下のプロパティも設定しておきます。

  • contentDescriptionでaltのようなものの設定
  • contentScale = ContentScale.Cropで表示サイズにあうように切り取ってくれる設定

step3 ステータスアイコンを追加

Box内のImageの後に追加します。

(Box内のコンテンツで後の方に記述されているものが、より上に表示されるため、画像の上にステータスアイコンが乗っかって表示されます。)

Box(contentAlignment = Alignment.TopEnd) {
    Image(...)
    // 以下を追加
    statusColor?.let { color ->
        val statusColorSize = size / 5
        Canvas(
            modifier = Modifier
                .size(statusColorSize)
                .offset(x = -statusColorSize / 4, y = statusColorSize / 4)
                .border(1.dp, if (darkTheme) Gray800 else Gray50, CircleShape)
        ) {
            val statusColorSizePx = statusColorSize.toPx()
            drawCircle(
                color = color,
                radius = statusColorSizePx / 2f,
            )
        }
    }
}
  • statusColorが設定されていれば(nullでなければ)、円形のステータスアイコンを表示します。
  • CanvasdrawCircleを使って、statusColorで指定された色のアイコンを描画します。
  • Modifier.offsetをうまく使い、少し枠にかぶるくらいの位置に表示します。

使い方

最後に、Previewのコードにもある通り、Avatarを呼び出して、Avatarコンポーネントを使うことができるようになります。

@Preview("avatar default")
@Preview("avatar dark theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun AvatarPreview() {
    // 画面サイズ全体に背景色をつける
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = if (isSystemInDarkTheme()) Gray800 else Gray50
    ) {
        
        Box(contentAlignment = Alignment.Center) {
            // 作成したAvatar画像の関数を使って、Avatarを表示(画像URL及びステータスアイコンの色を指定)
            Avatar(
                imageUrl = "https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80",
                statusColor = Green500,
            )
        }
    }
}

注意事項

  • Gray600とかGreen500とかの色の定義はこちらのものを使っています。
  • 事前にaccompanist-coilのライブラリを依存関係に追加しておく必要があります。
dependencies {
    final def accompanist_version = '0.11.0'
    implementation "com.google.accompanist:accompanist-coil:$accompanist_version"
}

リリースしたアプリ内でもソースコードを公開しています。

Avatarコンポーネントだけでなく、いくつか紹介していますので、ご興味のある方は、ぜひインストールして使ってみてください。

以上です。