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 などもリリースされていくと思うので、なるべく新しいバージョンをご利用ください。
12 月 16 日に、DevFest Tokyo と Google の共催で「DevFest & Android Dev Summit Japan 2022」を開催します。
本イベントでは、10 月 25 日 (日本時間) に米国にて開始した Android Dev Summit (ADS) 2022 から「最先端の Android 開発 (MAD) 」「フォームファクター」「プラットフォーム」の 3 つの重要テーマに沿って、日本のデベロッパー向けに最新情報コンテンツをお届けします。
ADS の発表内容について、より理解を深めていただきやすいように Google 社員による振り返りや Q&A セッションをご用意しました。また国内のコンテンツや事例も多くご用意しており、 Google Developer Expert (GDE) によるビギナー向け Jetpack Compose の導入に関するセッションや Jetpack Compose を導入された株式会社メルカリ様や株式会社おいしい健康様、利用が拡大している大画面デバイス向けの UI 構築を実施された株式会社 U-NEXT 様にご登壇いただき、導入の背景や製品開発の経緯、導入後の率直な感想についてお話しいただきます。
一部のセッションをオンラインでも配信するのと同時に、ご希望に合わせて Google 東京オフィスの会場にご招待できるよう準備しています。会場では、タブレット、ウェアラブル、Chromebook、TV などのハードウェア端末を触って、アプリを体験できるデモブースや Google の技術担当社員と交流する機会を設けております。
また今回は DevFest イベントと共済になっており、 Android 以外にもウェブ、 Flutter や Firebase などのマルチプラットフォーム技術、機械学習などデベロッパー向けの幅広いコンテンツをご用意しておりますので、ご自身の興味・関心に合ったセッションにご参加ください。
オフライン (東京会場) 登録はこちら
*オフライン (東京会場) のご登録はイベントへの参加を確定するものではございません。参加が確定された方には、12 月初めまでに参加確定のご案内をお送りいたします。
オンライン視聴登録はこちら
皆さまのご参加をお待ちしております。
Posted by Mari Kawanishi - Developer Marketing Manager, Google Play
この記事は、 Google Developer Expert (GDE) @yanzm さんに寄稿いただいたゲスト記事です。この「エキスパートに学ぶシリーズ」では、まだ Jetpack Compose を使ったことがないデベロッパー向けに既存アプリで段階的に導入を進める方法や導入するメリットについて GDE の方々よりご紹介いただきます。前回は既存アプリに Jetpack Compose を導入する手順について紹介しました。今回は XML レイアウトの中に Jetpack Compose による UI を表示するための View を追加し、段階的に Jetpack Compose に置き換えていく方法を紹介します。
まず、カンファレンスのセッション情報を表示するためのレイアウトを例に、XML で定義されたレイアウトを Jetpack Compose へ置き換えて行きます。
レイアウトは次の様に ConstraintLayout を使って構成されています。その中には、セッションタイトル、スピーカーの画像、スピーカーの名前、スピーカーの肩書きを表示する 3 つの TextView と 1 つの ImageView が含まれます。
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/session_title" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginEnd="16dp" android:textAppearance="@style/TextAppearance.AppCompat.Headline" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="Jetpack Compose で Material Design 3" /> <ImageView android:id="@+id/speaker_image" android:layout_width="72dp" android:layout_height="72dp" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginBottom="16dp" android:background="#cccccc" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/session_title" /> <TextView android:id="@+id/speaker_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:textAppearance="@style/TextAppearance.AppCompat.Body1" app:layout_constraintBottom_toTopOf="@id/speaker_title" app:layout_constraintStart_toEndOf="@id/speaker_image" app:layout_constraintTop_toTopOf="@id/speaker_image" app:layout_constraintVertical_chainStyle="packed" tools:text="Yuki Anzai" /> <TextView android:id="@+id/speaker_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:textAppearance="@style/TextAppearance.AppCompat.Caption" app:layout_constraintBottom_toBottomOf="@id/speaker_image" app:layout_constraintStart_toStartOf="@id/speaker_name" app:layout_constraintTop_toBottomOf="@id/speaker_name" tools:text="Android GDE" /></androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/session_title"
android:layout_width="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Jetpack Compose で Material Design 3" />
<ImageView
android:id="@+id/speaker_image"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginBottom="16dp"
android:background="#cccccc"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/session_title" />
android:id="@+id/speaker_name"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
app:layout_constraintBottom_toTopOf="@id/speaker_title"
app:layout_constraintStart_toEndOf="@id/speaker_image"
app:layout_constraintTop_toTopOf="@id/speaker_image"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Yuki Anzai" />
android:id="@+id/speaker_title"
android:layout_marginTop="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
app:layout_constraintBottom_toBottomOf="@id/speaker_image"
app:layout_constraintStart_toStartOf="@id/speaker_name"
app:layout_constraintTop_toBottomOf="@id/speaker_name"
tools:text="Android GDE" />
</androidx.constraintlayout.widget.ConstraintLayout>
このレイアウトを持つアプリは MDC-Android (英語) の マテリアル デザイン 3 テーマをベースとするアプリのテーマを View システムで使っています。
そのため Jetpack Compose 用の マテリアル デザイン 3 コンポーネントライブラリに加えて、既存 View システムで使用しているテーマを Jetpack Compose で再利用するための MDC-Android Compose Theme Adapter (英語) のマテリアル デザイン 3 用ライブラリを追加します。
dependencies { … implementation "androidx.compose.material3:material3:1.0.0" implementation "com.google.android.material:compose-theme-adapter-3:1.0.21"}
dependencies {
…
implementation "androidx.compose.material3:material3:1.0.0"
implementation "com.google.android.material:compose-theme-adapter-3:1.0.21"
XML レイアウトの中に Jetpack Compose による UI を表示するには、 ComposeView という View を利用します。
XML レイアウトの中に ComposeView を追加して android:id を指定します。
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout …> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/session_title" … /> …</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout …>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
app:layout_constraintTop_toTopOf="parent" />
… />
ComposeView には setContent メソッドが用意されています。このメソッドのラムダブロック内に Jetpack Compose を利用したコードを記述して UI を実装していきます 。
class SessionDetailActivity : AppCompatActivity() { private val binding by lazy { ActivitySessionDetailBinding.inflate(layoutInflater) } private val viewModel: SessionDetailViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) … binding.composeView.setContent { // この中に Jetpack Compose のコードを書いていく Mdc3Theme { Text("Hello, Compose!") } } }}
class SessionDetailActivity : AppCompatActivity() {
private val binding by lazy {
ActivitySessionDetailBinding.inflate(layoutInflater)
private val viewModel: SessionDetailViewModel by viewModels()
binding.composeView.setContent {
// この中に Jetpack Compose のコードを書いていく
Text("Hello, Compose!")
XML レイアウトの置き換え先になるコンポーズ可能関数を SessionDetail という名前で用意します。以下、 SessionDetail Composable の様な記述は、 SessionDetail という名前のコンポーズ可能関数を意味します。 SessionDetail Composable に表示させるセッションの情報は、SessionData 型のオブジェクトを引数でとるようにします。
Jetpack Compose では Preview アノテーションを使って Composable のプレビューを表示することができるので、 SessionDetail Composable のプレビューも用意しておきます。
@Composableprivate fun SessionDetail(data: SessionData) {}@Preview@Composableprivate fun SessionDetailPreview() { Mdc3Theme { Surface { SessionDetail( data = SessionData( … ) ) } }}
private fun SessionDetail(data: SessionData) {
private fun SessionDetailPreview() {
SessionDetail(
data = SessionData(
SessionData は SessionDetailViewModel が保持しており、Flow<SessionData> 型で公開されています。
class SessionDetailViewModel : ViewModel() { val sessionDataFlow: Flow<SessionData> = …}
class SessionDetailViewModel : ViewModel() {
val sessionDataFlow: Flow<SessionData> = …
Jetpack Compose には Flow を State に変換する collectAsState メソッドが用意されています。このメソッドを使って Flow<SessionData> を State<SessionData?> に変換します。State の値に value プロパティでアクセスし、 SessionDetail Composable に渡します。
SessionDetailViewModel ではなく SessionDetail を渡すようにすることで、 SessionDetail Composable を stateless(状態を持たない)な Composable にできます。stateless な Composable はテストがしやすくプレビューも簡単にできます。
class SessionDetailActivity : AppCompatActivity() { … private val viewModel: SessionDetailViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) … binding.composeView.setContent { // この中に Jetpack Compose のコードを書いていく Mdc3Theme { val state: State<SessionData?> = viewModel.sessionDataFlow .collectAsState(null) val data: SessionData? = state.value if (data != null) { SessionDetail(data = data) } } } }}
val state: State<SessionData?> = viewModel.sessionDataFlow
.collectAsState(null)
val data: SessionData? = state.value
if (data != null) {
SessionDetail(data = data)
セッションタイトルを表示する Text Composable を SessionDetail Composable に追加します。セッションタイトルを表示していた TextView は削除するのではなく android:visibility を invisible にします。
スピーカーの画像を表示する ImageView がこの TextView を参照しているため、削除すると ImageView のレイアウトでエラーがおこり、参照先を変更しなければなりません。また android:visibility が gone の場合だと TextView のエリア分だけ ImageView が上にずれてしまいます。
@Composableprivate fun SessionDetail(data: SessionData) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp), ) { Text( text = data.sessionTitle, style = MaterialTheme.typography.headlineSmall, ) }}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = data.sessionTitle,
style = MaterialTheme.typography.headlineSmall,
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout …> <androidx.compose.ui.platform.ComposeView … /> <TextView android:id="@+id/session_title" … android:visibility="invisible" … /> …</androidx.constraintlayout.widget.ConstraintLayout>
android:visibility="invisible"
インターネット上にある画像を Compose で表示する例として、ここでは Coil ライブラリ (英語) を使います。
dependencies { … implementation "io.coil-kt:coil-compose:2.2.2"}
implementation "io.coil-kt:coil-compose:2.2.2"
Coil ライブラリ の AsyncImage Composable を使ってスピーカーの画像を表示します。
ConstraintLayout 内の ImageView は android:visibility を invisible にしておきます。
@Composableprivate fun SessionDetail(data: SessionData) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp), ) { Text( text = data.sessionTitle, style = MaterialTheme.typography.headlineSmall, ) Row( modifier = Modifier.padding(top = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { AsyncImage( model = data.speakerImageUrl, contentDescription = "speaker image", modifier = Modifier.size(72.dp) ) } }}
Row(
modifier = Modifier.padding(top = 16.dp),
verticalAlignment = Alignment.CenterVertically,
AsyncImage(
model = data.speakerImageUrl,
contentDescription = "speaker image",
modifier = Modifier.size(72.dp)
スピーカーの名前と肩書きも同じように Text Composable で表示します。
@Composableprivate fun SessionDetail(data: SessionData) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp), ) { Text( text = data.sessionTitle, style = MaterialTheme.typography.headlineSmall, ) Row( modifier = Modifier.padding(top = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { AsyncImage( model = data.speakerImageUrl, contentDescription = "speaker image", modifier = Modifier.size(72.dp) ) Column( modifier = Modifier.padding(start = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( text = data.speakerName, style = MaterialTheme.typography.bodyMedium, ) Text( text = data.speakerTitle, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } }}
modifier = Modifier.padding(start = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
text = data.speakerName,
style = MaterialTheme.typography.bodyMedium,
text = data.speakerTitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
ConstraintLayout 内の全ての View を SessionDetail Composable に置き換えたので、 XML レイアウトおよび View Binding をやめて Activity の setContent を使うようにします。
class SessionDetailActivity : AppCompatActivity() { private val viewModel: SessionDetailViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Mdc3Theme { val state: State<SessionData?> = viewModel.sessionDataFlow .collectAsState(null) val data: SessionData? = state.value if (data != null) { SessionDetail(data = data) } } } }}
XML レイアウトを段階的に Jetpack Compose に置き換えていく方法を紹介しました。ComposeView を使うことで View 階層の中に Jetpack Compose による UI を簡単に組み込むことができます。
表示内容の変わらない View ならデータの更新について考える必要がないため、 Jetpack Compose への初めての置き換えに最適です。ぜひ挑戦してみてください。