この記事は Android ML Platform Team による Android Developers Blog の記事 " Latest updates on Android’s custom ML stack " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
オンデバイス ML には、サーバーベースの ML に比べて、オフラインでも利用できる、待ち時間が少ない、プライバシーが向上する、推論コストが低いといったメリットがあるため、Android でのオンデバイス ML の利用はこれまでに以上に急速に拡大しています。
Android デベロッパーがオンデバイス ML ベースの機能を開発する場合、2 つのオプションから選択するのが一般的です。1 つは、ML Kit のようなトレーニングと最適化が済んでいる ML モデルが含まれた本番環境対応の SDK を使う方法。それよりも細かい制御が必要なら、2 つめの独自のカスタム ML モデルや機能をデプロイする方法です。
この記事では、2022 年 10 月 18 日にアップデートされた Android のカスタム ML スタック(Android にカスタム ML 機能をデプロイするための重要な API やサービス群)について紹介します。
スタンドアロンの TensorFlow Lite に代わる Google Play 開発者サービスの TensorFlow Lite について最初にお知らせしたのは、Google I/O '21 での早期アクセス プレビューでした。それ以降、数万個のアプリを通して、毎月数十億人のユーザーにサービスを提供するまでに至っています。9 月には、Google Play 開発者サービスの TensorFlow Lite 安定版がリリースされ、この機能が Android の公式 ML 推論エンジンになりました。
Google Play 開発者サービスの TensorFlow Lite を使えば、バイナリサイズの削減や自動アップデートによるパフォーマンス向上だけでなく、Android のカスタム ML スタックが将来提供する API やサービスを簡単に組み込めるというメリットも享受できます。このような API やサービスは、公式推論エンジンをベースに開発されるからです。
現在アプリに TensorFlow Lite をバンドルしている方は、ドキュメントを参照して移行してください。
数年前にリリースされた GPU デリゲートと NNAPI デリゲートを使うと、GPU、DSP、NPU といった専用ハードウェアの処理能力を活用できます。現在、GPU デリゲートと NNAPI デリゲートの両方が Google Play 開発者サービスで配信されています。
また、高度なユースケースでは、カスタムのデリゲートを直接利用したいと考えるデベロッパーがいることも認識しています。そこでハードウェア パートナーと連携して、Google Play 開発者サービスを通してカスタム デリゲートにアクセスしやすくします。
Android のハードウェアは多様なので、ユーザーごとに最適なデリゲートを見つけるのは複雑な作業になる可能性があります。この課題を克服するため、新しい API を開発し、実行時に TensorFlow Lite モデルに最適なハードウェア アクセラレーションを安全に設定できるようにします。
来年の早い時期に正式リリースを行うことを目指し、現在はアクセラレーション サービスの早期アクセス (英語) の申し込みを受け付けています。
私たちは、Android で高パフォーマンスなカスタム オンデバイス ML を実現するために、重要な機能を提供する作業を続けています。
今回のアップデート内容をまとめると、現在の Android のカスタム ML スタックには以下が含まれています:
アクセラレーション サービスは近日中にリリースを迎えます。これにより、実行時に最適なデリゲートを選べるようになります。
Android のカスタム ML スタックの詳細や最新情報は、developer.android.com/ml をご覧ください。
この記事は Android Developers Team による Android Developers Blog の記事 " Todoist adopted Compose for Wear OS and increased its growth rate by 50 percent " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
Todoist は、タスクと時間管理用の世界トップクラスのアプリで、3,000 万人以上が大小のプロジェクトの整理、計画、共同作業に活用しています。企業としての Todoist は、ユーザーに仕事や生活の充実度を上げてもらうために尽力しています。そのための方策の 1 つが、さまざまなデバイスからアプリにアクセスできるようにすることです。
そこで、Todoist のデベロッパーは、Wear OS 向け Compose (英語) を採用してウェアラブル向けにアプリを完全に再構築しました。この新しい UI ツールキットは、Android が他のデバイス向けに提供しているのと同じ仕組みをウェアラブルのデベロッパーに提供することで、簡単で効率的な管理しやすいアプリ開発を実現します。
Todoist のデベロッパーは、すでに Android モバイルで Jetpack Compose を使っていたため、短時間で Wear OS 向け Compose を使いこなすことができました。「新しい Wear 向けデザイン言語と Wear OS 向け Compose が発表されたとき、とても興奮しました」と Todoist の Android 担当責任者の Rastislav Vaško 氏は話します。「それが、このプラットフォームの今後に注力する新たなモチベーションとチャンスになりました」
モバイル向けの Jetpack Compose と同じように、Wear OS 向け Compose ツールキットから直接カスタマイズ可能なコンポーネントを組み込めるので、これまで使っていたビューベースのレイアウトよりもはるかに短時間でコードを書いて設計要件を実現できます。公開されているドキュメントや Wear OS 向け Compose の Codelab の実践的なガイドもあったので、以前のツールキットの知識をウェアラブル プラットフォームでも活用できました。
「Wear OS 向け Compose には、レイアウトを作るために必要なものがほぼそろっていました。スワイプで閉じる、TimeText、ScalingLazyList は、すぐに使えてうまく動作するコンポーネントでした。それでいながら、注目される個性的なアプリを作ることができました」と Vaško 氏は言います。ツールキットでまだ提供されていない機能を実現するため、Todoist のチームは Google の Horologist (英語) を使いました。これはオープンソースのライブラリ群で、まだ実現されていない一般的な機能を Wear OS デベロッパーに提供しています。さらに、Compose Layout library を使って、ネイティブ デザインガイドと同等の Fade Away Modifier を組み込みました。
Wear OS 向け Compose には高度な Kotlin 構文と最新の宣言型アプローチが使われているので、Wear OS の UI 開発が簡単になり、複雑な画面でも読みやすく、保守しやすくなります。これは新しい Todoist アプリの制作にとって大きなメリットであり、デベロッパーは時間を節約しながら、これまでよりも多くのことができるようになりました。
再構築の中心になったのは、すべての画面の再設計と、最新の Wear OS 向けマテリアル デザインに準拠する操作でした。Todoist のデベロッパーは、Wear OS 向け Compose を使い、WearableDrawerLayout (英語) からフラットなアプリ構造に移行しました。この変更は Wear OS 向けマテリアル デザインのガイドに従ったもので、これによってアプリのレイアウトを最新化できました。
また、Todoist のデベロッパーは各画面を Wear OS デバイス専用にデザインし、ユーザー エクスペリエンスを煩雑にする不要な要素を削除しました。
「ウェアラブルでは、常に何を省けるかを考え、一瞬で行える的を絞った効率的な操作のみを残すようにしています」と Vaško 氏は述べています。Wear OS 向け Compose は開発とデザインの両面にわたって非常に役立ち、Todoist のチームは一貫したユーザー エクスペリエンスを保ちながら、保守しやすい実装を実現できました。
Todoist のデベロッパーは、Jetpack Compose を使って短時間で効率的に Wear OS 向けの更新版アプリを作ることができました。最新のツール、直感的な API、たくさんのリソース、ドキュメント、サンプルのおかげで、デザインと開発のプロセスがスムーズになり、必要なコードも少なくなって、機能的で新鮮なユーザー エクスペリエンスを短時間で提供できました。
アプリを再構築した後、Google Play での Todoist のインストール数増加率は 50% 上昇し、社内のチームやソーシャル メディアからも肯定的なフィードバックが寄せられました。
Todoist のチームは、Wear OS 向け Compose を使ってアプリで他に何ができるかを見つけたいと思っています。今回の更新はウェアラブルの将来への投資だと考えており、さらなるチャンスと Wear OS 3 デバイスで提供できる機能に期待を寄せています。
Todoist は、Wear OS 向け Compose を使って Wear OS アプリの完全な再構築と再設計を行い、ユーザーとデベロッパーの両方の体験が向上しました。
Wear OS 向け Jetpack Compose は、以下で学ぶことができます。
2022 年に日本で人気を集めた Google Play のコンテンツを紹介する Google Play ベスト オブ 2022 は、アプリおよびゲームの部門賞と各部門の大賞を本日発表しました。
2022 年は、緊急事態宣言が発令されず、久しぶりに行動制限が無い長期休暇になるなど、社会情勢が新型コロナウイルス感染症の拡大以前の生活に徐々に戻ってきた一年でした。アプリでは、コロナ禍でも人気を博したオンラインで楽しく過ごせるエンターテイメントアプリに加えて、心身の健康を育むアプリや日々の生活に役立つ家計簿アプリなど、多種多様なアプリに関心が集まりました。また、今年からウェアラブル部門とタブレット部門、そして社会貢献部門を新設し、マルチデバイスでのアプリ体験やアプリを通じた社会問題の解決なども注目を集めました。
ゲームにおいては、今年も素晴らしいモバイルゲームが数多く生まれましたが、新しさのなかでも、どこか親しみのあるタイトルが注目を集めました。日常を取り戻しつつも、不安定な日々が続いた 2022 年。慣れ親しんだキャラクターやアニメ的な世界観が、私たちの心に安心感を与えてくれたのかもしれません。中でも、ゲーム初心者にも分かりやすい UI やゲーム好きも楽しめる幅広い機能性まで網羅したタイトルが特に人気を集めました。
また、アプリと同様に、マルチデバイスでのゲーム体験を表彰するタブレット部門と Chromebook 部門に加えて、機能のアップデートや様々なイベントによって、常に新鮮なゲーム体験を提供し、長年愛される人気タイトルを表彰するオンゴーイング部門、今年から提供開始した Google Play Pass で特に人気を博したアプリを表彰する Play Pass 部門、そしてゲームの世界に入り込んだような気分を味わえるタイトルを表彰するストーリー部門を新設しました。
そんな 2022 年という年に、日本の Google Play ベスト オブ 2022 を受賞した作品をご紹介します。受賞された皆様、おめでとうございます!
ベストアプリ 2022
U-NEXT/ユーネクスト:映画、ドラマ、アニメなどが見放題
ユーザー投票部門
大賞
ダイエットアプリ あすけん カロリー計算・食事記録・体重管理
トップ 3
エンターテイメント部門
HykeComic-ハイクコミック:フルカラー漫画(マンガ)
部門賞
生活お役立ち部門
B/43(ビーヨンサン) - 家計簿プリカ
自己改善部門
隠れた名作部門
Awarefy - 認知行動療法アプリ
社会貢献部門
ピリカ - ごみ拾いアプリで社会貢献
タブレット部門 アプリカテゴリ
ウェアラブル部門
LINE(ライン) - 通話・メールアプリ
ベストゲーム 2022
ヘブンバーンズレッド
トップ 5
エキサイティング部門
遊戯王 マスターデュエル
キュート&カジュアル部門
ドラゴンクエストけしケシ!
インディー部門
SOULVARS
ストーリー部門
鋼の錬金術師 MOBILE
オンゴーイング部門
パズル&ドラゴンズ(Puzzle & Dragons)
Play Pass 部門
FINAL FANTASY VII
タブレット部門 ゲームカテゴリ
Chromebook 部門 ゲームカテゴリ
Roblox
この記事は、Google Developer Expert (GDE) @new_runnable (takahirom) さんに寄稿いただいたゲスト記事です。この「エキスパートに学ぶシリーズ」では、まだ Jetpack Compose を使ったことがないデベロッパー向けに既存アプリで段階的に導入を進める方法や導入するメリットについて GDE の方々よりご紹介いただきます。
みなさんの Android のアプリがまだ Jetpack Compose に移行されていない場合、RecyclerView を使っている部分もあるのではないでしょうか?
RecyclerView は効率的にリストなどのデータを表示する View で、今までの Android アプリの開発の中では頻繁に使われてきました。Jetpack Compose は View ベースの UI と統合することができ、View ベースのアプリに対しても段階的に導入が可能になっています。Jetpack Compose は RecyclerView を使っているアプリにも段階的に導入可能になっています。
RecyclerView から Jetpack Compose に書き直すことでかなりコードを単純化できます。この記事ではRecyclerView による実装をどうやって Jetpack Compose に置き換えていくかを紹介していきます。
置き換えるにはまずは「エキスパートに学ぶシリーズ : 1. Jetpack Compose を既存アプリに導入する」を読んで Jetpack Compose を導入していく必要があります。RecyclerView が使われていない画面であれば「エキスパートに学ぶシリーズ : 2. Jetpack Compose の段階的移行」を読んで、View をうまく移行していくのが良いと思います。
RecyclerView に関しては一気に Jetpack Compose に置き換えできればそれが一番簡単ですが、RecyclerView にたくさんの View が入ってしまっていて、一気に移行できない場合もあります。
そこで、今回は 2 つのステップに分けて移行する方法を紹介します。
簡単な RecyclerView でのコードをまず紹介し、そこからの移行の方法を紹介していきます。
こちらのサンプルは Android Studio の新しいバージョンを教えてくれるようなものをイメージしています。
RecyclerView で実装した場合は以下のような実装になるのではないかと思います。これを Jetpack Compose に置き換えていきます。
少しだけ各要素を紹介しておきます。
data class Article(val id: Int, val title: String)class ArticlesViewModel : ViewModel() { private val _articlesStateFlow = MutableStateFlow( listOf( Article(1, "🦊 Android Studio Arctic Fox is out!"), Article(2, "🐝 Android Studio Bumblebee is out!"), Article(3, "\uD83D\uDC3F️ Android Studio Chipmunk is out!"), Article(4, "\uD83D\uDC2C Android Studio Dolphin is out!"), ) ) val articlesStateFlow: StateFlow<List<Article>> = _articlesStateFlow.asStateFlow()}class MainActivity : ComponentActivity() { private val articlesViewModel by viewModels<ArticlesViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) val articlesAdapter = ArticlesAdapter() binding.recyclerView.adapter = articlesAdapter lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { articlesViewModel.articlesStateFlow.collect { articles -> articlesAdapter.submitList(articles) } } } }}class ArticlesAdapter : ListAdapter<Article, ArticlesAdapter.ArticleViewHolder>( DIFF_CALLBACK) { class ArticleViewHolder( private val binding: ItemArticleBinding ) : RecyclerView.ViewHolder(binding.root) { fun bindTo(article: Article) { binding.title.text = article.title } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder { return ArticleViewHolder(ItemArticleBinding.inflate(LayoutInflater.from(parent.context))) } override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) { holder.bindTo(getItem(position)) } companion object { val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Article>() { override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean { return oldItem == newItem } } }}activity_main.xml<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /></FrameLayout>item_article.xml<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="8dp" android:textAppearance="?attr/textAppearanceHeadline3" tools:text="test" /></LinearLayout>
data class Article(val id: Int, val title: String)
class ArticlesViewModel : ViewModel() {
private val _articlesStateFlow = MutableStateFlow(
listOf(
Article(1, "🦊 Android Studio Arctic Fox is out!"),
Article(2, "🐝 Android Studio Bumblebee is out!"),
Article(3, "\uD83D\uDC3F️ Android Studio Chipmunk is out!"),
Article(4, "\uD83D\uDC2C Android Studio Dolphin is out!"),
)
val articlesStateFlow: StateFlow<List<Article>> = _articlesStateFlow.asStateFlow()
}
class MainActivity : ComponentActivity() {
private val articlesViewModel by viewModels<ArticlesViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val articlesAdapter = ArticlesAdapter()
binding.recyclerView.adapter = articlesAdapter
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
articlesViewModel.articlesStateFlow.collect { articles ->
articlesAdapter.submitList(articles)
class ArticlesAdapter : ListAdapter<Article, ArticlesAdapter.ArticleViewHolder>(
DIFF_CALLBACK
) {
class ArticleViewHolder(
private val binding: ItemArticleBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bindTo(article: Article) {
binding.title.text = article.title
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(ItemArticleBinding.inflate(LayoutInflater.from(parent.context)))
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
holder.bindTo(getItem(position))
companion object {
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</FrameLayout>
item_article.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textAppearance="?attr/textAppearanceHeadline3"
tools:text="test" />
</LinearLayout>
この RecyclerView で表示している View の実装を Compose に移行していきましょう。ここでは RecyclerView 自体は置き換えず、中で表示している View を Compose に置き換えます。
まず RecyclerView のバージョンを 1.3.0-rc01、Compose UI のバージョンを 1.2.1 以降を使うようにします。これは Compose と RecyclerView が協調して動作するために必要です。 ※1
build.gradleimplementation 'androidx.recyclervew:recyclerview:1.3.0-rc01'
build.gradle
implementation 'androidx.recyclervew:recyclerview:1.3.0-rc01'
最初にさっそく Jetpack Compose で表示するコンポーズ可能な関数を用意しましょう。引数で Article クラスのインスタンスを受け取り、Text() を利用して内容を表示してあげます。ここでは単純に記事の内容を表示してあげるコンポーズ可能な関数になっています。
ここでは @Preview を使ってこのコンポーズ可能な関数の Preview も表示してあげています。
@Composablefun ArticleRow(article: Article, modifier: Modifier = Modifier) { Column(modifier) { Text( text = article.title, style = MaterialTheme.typography.h3, modifier = Modifier.padding(8.dp) ) }}@Preview@Composablefun ArticleRowPreview() { Mdc3Theme { Surface { ArticleRow( article = Article(1, "Hello ArticleRow"), modifier = Modifier.fillMaxWidth() ) } }}
@Composable
fun ArticleRow(article: Article, modifier: Modifier = Modifier) {
Column(modifier) {
Text(
text = article.title,
style = MaterialTheme.typography.h3,
modifier = Modifier.padding(8.dp)
@Preview
fun ArticleRowPreview() {
Mdc3Theme {
Surface {
ArticleRow(
article = Article(1, "Hello ArticleRow"),
modifier = Modifier.fillMaxWidth()
Android Studio 上の Preview 表示
RecyclerView 内で表示する View として ArticleRow() を表示するため、ArticleRowComposeView を作成します。
ArticleRowComposeView は AbstractComposeView を継承することで RecyclerView 内で View として Jetpack Compose を表示させます。
また、ArticleRowComposeView は Compose の State をフィールドとして持っています。これにより、RecyclerView の Adapter から、この Compose の State に記事データを入れ、コンポーズ可能な関数内でその State を読むことで記事データを Compose で表示させています。
class ArticleRowComposeView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0// AbstractComposeView を継承する) : AbstractComposeView(context, attrs, defStyle) { // Compose の State をフィールドとして持つ var article by mutableStateOf<Article?>(null) @Composable override fun Content() { Mdc3Theme { // Compose の State をコンポーズ可能な関数内で読む val article = article if (article != null) { ArticleRow( article = article, modifier = Modifier.fillMaxWidth() ) } } }}
class ArticleRowComposeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
// AbstractComposeView を継承する
) : AbstractComposeView(context, attrs, defStyle) {
// Compose の State をフィールドとして持つ
var article by mutableStateOf<Article?>(null)
override fun Content() {
// Compose の State をコンポーズ可能な関数内で読む
val article = article
if (article != null) {
article = article,
RecyclerView とつなぎ込んで表示できるようにします。この作った ArticleRowComposeView を RecyclerView の Adapter の実装を変更して表示させます。
具体的には onCreateViewHolder() で ArticleRowComposeView を作成し、ArticleRowComposeView を持つ ViewHolder を作成します。
また Adapter.onBindViewHolder() から bindTo() の実装が呼ばれることにより ArticleRowComposeView に記事データを渡すことで Compose からそのデータを読み取ることができるようになります。
RecyclerView は名前の通り View を再利用 (Recycle) して表示するので、例えばスクロールで onCreateViewHolder() で一度作った ViewHolder とその中にある View を再利用し、 onBindViewHolder() でデータを再度入れる動きになります。
class ArticlesAdapter : ListAdapter<Article, ArticlesAdapter.ArticleViewHolder>( DIFF_CALLBACK) { class ArticleViewHolder( private val articleRowComposeView: ArticleRowComposeView ) : RecyclerView.ViewHolder(articleRowComposeView) { fun bindTo(article: Article) { articleRowComposeView.article = article } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder { val articleRowComposeView = ArticleRowComposeView(parent.context) return ArticleViewHolder(articleRowComposeView) } override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) { holder.bindTo(getItem(position)) }
private val articleRowComposeView: ArticleRowComposeView
) : RecyclerView.ViewHolder(articleRowComposeView) {
articleRowComposeView.article = article
val articleRowComposeView = ArticleRowComposeView(parent.context)
return ArticleViewHolder(articleRowComposeView)
Jetpack Compose Interop: Using Compose in a RecyclerView (英語) の記事でも触れられているようにスクロール時に毎回 Compose の Composition が破棄 (dispose) されないようにすることでパフォーマンスを改善することができます。
recyclerview:1.3.0-alpha02 より後のバージョンを使っていると RecyclerView と Compose が協調して動作するようになるため、スクロール中に頻繁に破棄されなくなります。
以下のように DisposableEffect を使ってログを出すことで、その挙動を確認することができます。もしスクロールしたときに頻繁に ItemRow xxx DISPOSED のログが出るようであれば、うまく RecyclerView のアップデートができていないか、間違ったタイミングで ComposeView を作ってしまっているなどの可能性があるので記事をみて詳細を確認してみてください。
@Composablefun ArticleRow(article: Article, modifier: Modifier = Modifier) { DisposableEffect(Unit) { Log.d("ArticleRow", "ItemRow ${article.id} composed") onDispose { Log.d("ArticleRow", "ItemRow ${article.id} DISPOSED") } }
DisposableEffect(Unit) {
Log.d("ArticleRow", "ItemRow ${article.id} composed")
onDispose { Log.d("ArticleRow", "ItemRow ${article.id} DISPOSED") }
ここまでで、RecyclerView の中身が Compose に置き換わりました。後は外側の RecyclerView を Jetpack Compose 置き換えることになります。
シンプルにここでは setContent{} を呼び出すことで Activity で Compose を表示するようにしています。この RecyclerView だけ移行したい場合などは、前回の記事が参考になると思います。
この 2 のステップによって、コード量がかなり少なくなっています。理由は XML のレイアウトを消せたことと、RecyclerView の Adapter のコードが LazyColumn に移行されたためです。
...// この 2 つが自動インポートできないことがあるようです。import androidx.compose.foundation.lazy.itemsimport androidx.compose.runtime.getValue...class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ArticlesScreen() } }}@Composableprivate fun ArticlesScreen(articlesViewModel: ArticlesViewModel = viewModel()) { Mdc3Theme { val articles by articlesViewModel.articlesStateFlow.collectAsState() LazyColumn { items( items = articles, key = { article -> article.id } ) { article -> ArticleRow( article = article, modifier = Modifier.fillMaxWidth() ) } } }}
...
// この 2 つが自動インポートできないことがあるようです。
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.getValue
setContent {
ArticlesScreen()
private fun ArticlesScreen(articlesViewModel: ArticlesViewModel = viewModel()) {
val articles by articlesViewModel.articlesStateFlow.collectAsState()
LazyColumn {
items(
items = articles,
key = { article -> article.id }
) { article ->
今回の実装について解説します。
ArticlesViewModel を viewModel() 関数を使って Jetpack Compose の中から取得しています。この viewModel() 関数を使うには以下の依存関係を追加する必要があります。
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
ここでは LazyColumn を利用してリスト表示をしています。LazyColumn は RecyclerView に似ていて必要なときだけ再コンポーズ (Recompose) などの最適化をしてくれます。また key の引数を渡しておくとさらに賢く最適化をしてくれます。詳しくは Use lazy layout keys (英語) で説明されています。
2 つのステップに分けて Jetpack Compose を移行していきました。移行した結果、コードはこの小さなサンプルでも 24 行ほど削減できたようです。また宣言的 UI により直感的にコードが書けるようになるなどのメリットも享受できます。Jetpack Compose に移行するメリットについて気になっている方は Compose を導入する理由にあるので確認してみてください。
git diff main compose --stat 4 files changed, 56 insertions(+), 80 deletions(-)
git diff main compose --stat
4 files changed, 56 insertions(+), 80 deletions(-)
このように Jetpack Compose では Java から Kotlin にしたようにアプリを段階的に移行できるようになっているので、少しずつ進めて開発効率を上げていきましょう。
※1 正確には RecyclerView 1.3.0-alpha02, Compose UI 1.2.0-beta02 以降で動きますが、記事を書いた時点で利用できるできるだけ安定したバージョンを書いています。将来的には RecyclerView 1.3.0 などもリリースされていくと思うので、なるべく新しいバージョンをご利用ください。