こんにちは!Jetpack Compose と KMM が好きなエンジニアのスージです。
今年開催された Android Developers Summit では素晴らしいセッションが盛り沢山でしたね。
同じタイミングでJetpack Compose v1.3.0
も stable になりました。今回もたくさんのAPIの追加や更新がありますし、その中で個人的に気になった API を紹介しようと思います。
- Compose BoM
- LookaheadLayout
- Variable フォントサポート
- Staggered Grid サポート
- Canvas.drawText()
- Hyphens API
- LineBreak API で改行サポート
- Pull-to-refresh
- Modifier.Node の大リファクタリング
- Kotlin 1.7.20 サポート
- 終わりに
Compose BoM
1.3.0 から新しい Compose BoM(Bill of Materials)パッケージが追加されました。BoMは内部で全てのComposeライブラリのバージョン管理し、各Compose ライブラリのstableバージョンへのリンクを指定します。
build.gradle
で BoM だけのバージョンを指定し、追加したい Composeライブラリのバージョンを指定せず使えます。
https://developer.android.com/jetpack/compose/setup#using-the-bom
//build.gradke.kts dependencies { implementation(platform("androidx.compose:compose-bom:2022.10.00")) implementation("androidx.compose.foundation:foundation") implementation("androidx.compose.animation:animation") .. }
LookaheadLayout
まだv1.2.0
が rc バージョンだった時@doris4lt氏の shared elements transition を実現できそうな LookaheadLayout
API が開発中だという呟きがありましたね。
Yup, LookaheadLayout is a new concept that we've been building. I can barely contain my excitement about the possibilities it opens up, shared element, auto-resizing, and more! 🤩🥰 Should be ready to release soon.
— Doris Liu (@doris4lt) 2022年5月30日
Spoiler: LookaheadLayout + movableContentOf = 🤯
Sneak peek: 👇 https://t.co/vQpocq5mF9 pic.twitter.com/KXp9CUOgas
これはやっと v1.3.0
で追加され、個人的に今回の一番気に入ったAPIです!
LookaheadLayout
は子供たちが変更された時に、start から finishフレームの間の intermediate フレームで子供たちの Measure と Placementの計算をフレームのレンダリングの前にできてます。
つまり、アニメーションの途中のフレームの Measure と Placement を取得可能になってます。 この途中のフレームの計算を Lookahead step と呼ばれています。
Lookahead step で計算した Placement の情報を Modifier.onPlaced()
で取得でき、そのフレームで子供たちの Placement を調整することができます。
/** * [onPlaced] gets invoked after the parent [LayoutModifier] has been placed * and before child [LayoutModifier] is placed. This allows child [LayoutModifier] to adjust * its own placement based on its parent. * .. */ fun Modifier.onPlaced( onPlaced: ( lookaheadScopeCoordinates: LookaheadLayoutCoordinates, layoutCoordinates: LookaheadLayoutCoordinates ) -> Unit ): Modifier
Modifier.intermediateLayout()
では Lookahead step で子供たちの Measure を取得でき、onPlaced
で調整した Placement を使って intermediate フレームの Layout を作ります。
/** * Creates an intermediate layout based on target size of the child layout calculated * in the lookahead. This allows the intermediate layout to morph the child layout * after lookahead through [measure], in which the size of the child layout calculated from the * lookahead is provided. * .. */ fun Modifier.intermediateLayout( measure: MeasureScope.( measurable: Measurable, constraints: Constraints, lookaheadSize: IntSize ) -> MeasureResult, ): Modifier
このまま次の Lookahead step の計算された Measure と Placement 使って子供たちのサイズを変更したりポジションを変えたりします。これを繰り返すと時間の経過で徐々に変化していきアニメーションに見えますね。
v1.2.0
で発表した movableContentOf
を使って、ある @Composable lambda を Compositionツリーの中にRecomposition せず移動できますね。
goodpatch-tech.hatenablog.com
LookaheadLayout
で作れるアニメーションの間・後にmovableContentOf
を使用して @Composable
UI の状態を失わず、再利用できるようにしたらShare Element Transitionを実現できますね。既にこれを活用して@github_skydoves氏は Orbitalという Jetpack Compose の Shared Element Transition ライブラリを作ってくれました!
github.com
もっと理解を深めるため Jetpack Compose Internals を書いてくれた@JorgeCastilloPr氏の2つの記事もぜひ読んでみてください。 effectiveandroid.substack.com effectiveandroid.substack.com
Variable フォントサポート
Compose ではじめて Variable フォントのサポートが追加されました!
val font = remember(weight, spacing) { Font( resId = .., variationSettings = FontVariation.Settings( FontVariation.weight(weight), FontVariation.width(spacing) ) ).toFontFamily() } Text( text = "Variable fonts are here!", fontFamily = font, .. )
Variable フォントサポートしてるフォント一覧と Variable Fonts について詳しく知りたいならこの記事を読んでみてください。
Staggered Grid サポート
LazyVerticalStaggeredGrid
と LazyHorizontalStaggeredGrid
を追加され、やっとStaggered Grid サポートが追加されました。
var (value, onValueChange) = remember { mutableStateOf(0f) } Column { Slider(value = value, onValueChange = onValueChange, steps = 10) LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Adaptive(((value * 50) + 10).dp) ) { items(50) { Box( modifier = Modifier .. .fillMaxWidth() .height((50 until 200).random().dp) ) } } }
LazyVerticalStaggeredGrid
に columns
パラメータで列の数(LazyHorizontalStaggeredGrid
の場合は行の数)を定義します。columns
に渡すStaggeredGridCells
の2つプリセットも用意されていて、どっちでも 全ての列のwidth が同じになります。
- StaggeredGridCells.Adaptive:列の数が決まってなくて、画面の width によって列の数をダイナミックにしたい場合これを使います。列のミニマム width を定義すると、それでできるだけ多くの列に収まるように調整します。
例えば、ミニマム width を
30.dp
に定義し画面の width は124.dp
だったら31.dp
widthの4つの列になります。
columns = StaggeredGridCells.Adaptive(minWidth = 30.dp)
- StaggeredGridCells.Fixed:列の数が決まっていたら、これを使います。例えば、3つの列にしたい場合
columns = StaggeredGridCells.Fixed(count = 3)
Canvas.drawText()
テキストを Canvas
でレンダリングできる drawText()
が追加されました。drawText
は TextMeasurer
またはTextMeasurer.measure(..)
が返す TextLayoutResult
をパラメータで渡します。
TextMeasurer.measure()
でテキスト間に空白を作るような placholders
パラメータがあります。返す TextLayoutResult
の placeholderRects
を使って非テキスト(例えば、inline 画像、絵文字など)を配置できます。
val textMeasurer = rememberTextMeasurer() var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) } Canvas( modifier = Modifier .fillMaxSize() .layout { measurable, constraints -> val placeable = measurable.measure(constraints) textLayoutResult = textMeasurer.measure( text = AnnotatedString(text), constraints = constraints, placeholders = listOf( AnnotatedString.Range( item = Placeholder( width = /* 40.dp */, height = /* 40.dp */, placeholderVerticalAlign = Center ), // range of indices where placeholder will be placed start = .. end = .. ), .. ) ) layout(placeable.width, placeable.height) {..} } ) { textLayoutResult?.let { drawText(textLayoutResult = it, color = Color.Black) it.placeholderRects.forEach { rect -> drawRect( topLeft = rect.topLeft, size = Size(rect.width, rect.height), .. ) } } }
Hyphens
API
ハイフンで言葉を改行できるHyphens
パラメータが TextStyle
で追加されました。
発表された時早速試してみようと思ったら反映せず、バグだと思い Kotlinlang Slack で相談してみました。Compose Text を担当してるエンジニア相談に乗ってくれて、端末の言語が「日本語」だったので反映しないということ教えてくれました。
つまり、このAPIを使う時文字が英語だけではなくLocale
は英語でない場合も反映されないということが注意点だとわかりました。
val text = "Hyphenation \u00AD is here!" Column { val (width, setWidth) = remember { mutableStateOf(300f) } Slider( value = width, onValueChange = setWidth, valueRange = 300f..1000f ) val widthInDp = with(LocalDensity.current) { width.toDp() } Text( modifier = Modifier.width(width = widthInDp), text = text, style = TextStyle(hyphens = Hyphens.Auto), ) }
LineBreak
API で改行サポート
改行を指定できる新しい LineBreak
APIが追加されました。
アプリのよくあるユースケースに対応する LineBreak プリセットも用意されてます。
- LineBreak.Simple: これは一番早く処理できるタイプです。
TextField
みたいな頻繁ににテキストが変わる場合に使います。 - LineBreak.Paragraph:長文・説明など、可読性が最優先で見た目も大事な場合使います。
- LineBreak.Heading:タイトルみたいな短文で改行の影響が少ない場合使います。
このAPIを試してみた結果をこの記事でまとめたのでぜひ読んでみてください。
Pull-to-refresh
まだ @ExperimentalMaterialApi
になりますが Pull-to-refresh は Accompanist から卒業し、Material Design ライブラリーに追加されました。でも、Accompanist の SwipeRefreshLayout と異なって全体のレイアウトではなくインジケーターだけが追加されました。
var refreshing by remember { mutableStateOf(false) } val state = rememberPullRefreshState( refreshing = refreshing, onRefresh = { .. } ) Box(modifier = Modifier.pullRefresh(state = state)) { LazyColumn { if (!refreshing) { items(..) } } PullRefreshIndicator( refreshing = refreshing, state = state, modifier = Modifier.align(Alignment.TopCenter) ) }
Modifier.Node
の大リファクタリング
v1.3.0
から Modifier.Node
が追加され、Compose UI のパフォーマンス向上に向けて Modifier
の大リファクタリングが始まりました。
今年開催された Android Dev Summit で@intelligibabble氏が これについて説明セッションをやりましたのでぜひ見てみてください!
理解した上でざっくり説明してみます。これまで .clickable()
などの Modifier
は、Modifier.then()
とModifier.composable{}
を用いて、複数の statelessとstateful Modifier.Element
のチェーンで構成されていました。
でも Modifier.composed{}
自体はパフォーマンス問題があります。
Modifier.composed{}
はバリューを返す関数なので、Recomposition の時はスキップできない。@Composable
関数じゃないので、Recomposition の前に返すModifier.Element
チェーンとRecompositionで返すModifier.Element
チェーンを Compose コンパイラがequals()
で比較できない。基本的に Compose は Layout が変わったかどうかを Modifierの比較で判断します。ここで Modifierが変わらなくてもコンパイラが比較できず新しい Modifier として扱いし Modifierを Layoutにまた適用してしまいます。
この記事を書くときまだ未実装なんですが Leland 氏は想定してる形は、.clickable()
みたいな Modifier.Element
で構成された Modifier
はModifier.composed{}
を使って Modifier.Element
のチェーンを構成する代わりにModifier.Node
オブジェクトを発行します。そのModifier.Node
オブジェクトをModifier.Node
のチェーンに追加しLayoutNode
に適用します。そうするとメリットは:
Modifier.Node
は mutable なのでModifier.Node
を比較でき、 Recompositionの時 Modifier が更新されたかどうかを判断できます。Modifier.Element
チェーンを unwrap することがなくなりますので、処理が必要な Compositionツリーの数が減ります。Modifier.Element
チェーンの代わりにModifier.Node
オブジェクトのツリーになりますのでツリーが短くなり、tree traversalも早くなります。
Modifier.focusModifier()
の Modifier.Node
のマイグレーションが始まったみたいなので次のバージョンでパフォーマンス向上を期待してます!
Kotlin 1.7.20 サポート
compose-compiler
バージョン v1.3.2 から Kotlin 1.7.20
サポートが入りました!個人的に Kotlin 1.7.20 で以下が気になりました。
Kotlin Native Memory がデフォルトで有効化されてます。Kotlin coroutines と Swift concurrency 間の相性がよくなってます!(The new Kotlin/Native memory manager enabled by default)
新しい
..<
range オペレーター追加されました。元々の..
の range オペレーターと「上限が含まれてない」ことだけが異なってます。(Preview of the..<
operator for creating open-ended ranges)
when (value) { in 0.0..<0.25 -> { /* 0.25を含まれてない */ } in 0.25..<0.5 -> { /* 0.5を含まれてない */ } }
- 新しい
data object
タイプ追加されました。基本的にもっといいtoString
を実装してるobject
です。
object MyObject data object MyDataObject sealed class ReadResult { data class Number(val value: Int) : ReadResult() data class Text(val value: String) : ReadResult() data object EndOfFile : ReadResult() } fun main() { println(MyObject) // org.example.MyObject@1f32e575 println(MyDataObject) // MyDataObject println(ReadResult.Number(1)) // Number(value=1) println(ReadResult.Text("Foo")) // Text(value=Foo) println(ReadResult.EndOfFile) // EndOfFile }
- Generic
value
クラスが追加され、value
クラスでも安定した type-safety を確保できます。
@JvmInline inline class Pet<T>(value: T) fun cuddle(cat: Pet<Cat>) fun takeOutForAWalk(dog: Pet<Dog>)
終わりに
外にも、ここに書いてない 1.3.0 のAPI追加と更新盛り沢山です。v1.4.0
にも Modifier.Node
のパフォーマンス向上含めてどんなAPIが追加や更新されるのか楽しみですね!