久しぶりに自分専用のAndroidアプリを作りたくなったため、最近のAndroid開発事情を調べてみたところ、
- Jetpack Compose
- ViewModel
- Room
など、色々と学びたくなるような技術を知りました。
ただ、学びながら自分専用のアプリを作り始めても挫折するだろうと思い、まずは学ぶためのアプリを作ることにしました。
何か良い題材がないか本を探したところ、以前お世話になったシリーズの最新刊「作ればわかる!Androidプログラミング Kotlin対応」(金宏 和實 著、翔泳社) が良さそうでした。
作ればわかる!Androidプログラミング Kotlin対応 10の実践サンプルで学ぶAndroidアプリ開発入門(金宏 和實)|翔泳社の本
そこで、同書籍のアイデアを Kotlin + Jetpack Compose で実装してみることにしました。
この記事では、各アプリを実装したときに気づいたことなどをメモとして残しておきます。
なお、メモの内容に誤りがありましたらご指摘ください。
目次
- 環境
- やらないこと
- 4章 ハイ&ローゲーム
- 5章 名刺切らしてまして
- 6章 ご飯なんにする?
- 10章 若くても血圧は記録せよ
- KSP版のRoomをインストールする
- Roomを使うために Entity・DAO・Database を用意し、Applicationを差し替える
- Dateの代わりにLocalDateTimeを使い、型コンバータで変換する
- DateTimeFormatterを使ってLocalDateTimeを文字列表記にする
- Entityで id を autoGenerate するならば、Insert時に渡す id の値は 0 にする
- Roomでの読み書きは LaunchedEffect + CoroutineScope + withContext で行う
- Navigation Composeで省略可能な引数を渡すため、クエリパラメータ構文を使う
- Snackbarは、 LaunchedEffect とともに使う
- Compose 1.2.0から、Scaffoldの content に padding パラメータの利用が必須化した
- ソースコード
環境
- Android Studio 2021.2.1 Patch 1
- Jetpack Compose 1.1系および1.2系
- 最初はAndroid Studioでプロジェクトを作成したときにできる1.1系を使用
- Roomを使うところで1.2系を使用
- Room 2.4.3
- Navigation Compose 2.5.1
やらないこと
今回の一番の目的は「Jetpack Compose の書き方に慣れる」としたため、今回やらないこととして以下を決めました。
- Jetpack Composeを使った時の良いディレクトリ構成
- ディレクトリ構成を真剣に考えるよりも動くものを作る
- 現時点ではどんな構成が一番良いのかよくわからないため
- ディレクトリ構成を真剣に考えるよりも動くものを作る
- MVVMを使うこと
- 今回は画面が少ないこと、優先して学びたいことがあったため
- Kotlinっぽい書き方をすること
- 今回はKotlinを学ぶのが目的ではないため、Android Studioでエラーやサジェストが出なければOKとした
- 書籍のアプリと同じデザイン・同じ実装とすること
- どこまでJetpack Compose で作れるのか分かっていないため、おおよそできればOKとした
- そのため、ConstraintLayout では作成していない
- 書籍のアプリをすべて実装すること
- 自分専用のアプリを作るにあたり、必要そうなアプリのみ実装することにした
- そのため、センサー系や地図系は実装していない
- 実装したくなったらそのときに実装する
以降、各章で調べたことを置いておきます。
4章 ハイ&ローゲーム
トランプの画像について
書籍通り、無料素材倶楽部さんよりトランプの画像をダウンロードしました。ありがとうございました。
なお、書籍の記載にある通り、自分の環境でも文字化けしています。
Scaffoldで topBar
や bottomBar
、 content
などをいい感じに定義する
参考にしたのは以下です。
androidx.compose.material | Android Developers
また、レイアウトについては以下の公式ドキュメントも参考にしました。
- Compose レイアウトの基本 | Jetpack Compose | Android Developers
- Compose でのレイアウト | Jetpack Compose | Android Developers
- Compose 内のマテリアル テーマ設定 | Jetpack Compose | Android Developers
marginは Modifier
を使って指定する
ふだんDBに近い生活をしているせいか、 Row
と Column
の置き方が逆な感覚がありました。
参考にしたドキュメントは以下です。
- android - How to add Margin in Jetpack Compose? - Stack Overflow
- Jetpack Composeのレイアウト. Jetpack Composeでレイアウトを組むための基本的なまとめです | by Kenji Abe | Medium
要素の中央寄せは、AlignmentとArrangementを使う
以下を参考にしました。
【Jetpack Compose】 要素を中央に配置する|yasukotelin|note
ローカルにある画像の表示は、 Image
などで painterResource
を使う
トランプ画像の表示が必要なため、以下を参考に painterResource
を使いました。
- Compose のリソース | Jetpack Compose | Android Developers
- 【Android】Jetpack Composeでよく使うプロパティをまとめてみた - Qiita
状態は remember
や rememberSaveable
と、 mutableStateOf
などで保持する
以下を参考にしました。
状態と Jetpack Compose | Android Developers
5章 名刺切らしてまして
画面を固定するには、 LocalContext
と requestedOrientation
を使う
書籍では AndroidManifest.xml
で画面の向きを固定していました。
せっかくなので、Jetpack Composeでの画面の向きの固定方法を調べたところ、 LocalContext
と requestedOrientation
で作れば実装できました。
android - How to force orientation for some screens in Jetpack Compose? - Stack Overflow
TextFieldでのキーボード制御は KeyboardOptions
を使う
「数字だけのキーボード」など、キーボード制御する方法を調べたところ、 KeyboardOptions
を使えば実装できました。
android - How to set the inputType for a TextField in Jetpack Compose - Stack Overflow
Jetpack Compose での画面遷移は Navigation Compose
を使う
ライブラリの最新のバージョンはこちらで確認できます。
Navigation | Android デベロッパー | Android Developers
なお、Navigation Compose の使い方については以下が参考になりました。
- Compose を使用したナビゲーション | Jetpack Compose | Android Developers
- Jetpack Compose で Navigation を利用した View の切り替え - Diary
- Navigation ComposeのNavOptions - Kenji Abe - Medium
共有プリファレンスの PreferenceManager は、現時点では deprecated な模様
書籍では共有プリファレンスへ保存する際 PreferenceManager
を使っていました。ただ、現時点では deprecated な模様です。
PreferenceManagerが@Deprecatedで困った話 - Qiita
そのため、以下を参考に、代替ライブラリを使いました。公式ドキュメントはこのあたりです。
TopAppBar の action
でオプションメニューを実現する
書籍ではオプションメニューを作っていましたが、今回は TopAppBar の action
を使うことにしました。
androidx.compose.material | Android Developers)
6章 ご飯なんにする?
オプションメニューのネストは、Composableを2つ作る
書籍ではオプションメニューを使っていました。
Jetpack Composeでのオプションメニュー実装を調べたところ、以下の方法のようにメインとネストのメニューを作れば実装できました。
android - What is the better or easier way to create "nested" menus in Jetpack Compose? - Stack Overflow
data classの値の更新は copy
を使う
選択したメニューは Kotlin の data class として保持することを考えました。
ただ、選択したメニューを変更したときに、data class のプロパティをどのように更新すればよいか調べたところ、以下の回答がありました。
android - Jetpack Compose State: Modify class property - Stack Overflow
また、 MutableState オブジェクトの宣言は3パターンあると知り、setter を取得できる構文 val (value, setValue) = remember { mutableStateOf(default) }
もあると知りました。
状態と Jetpack Compose | Android Developers
長押しイベントは combinedClickable
の onLongClick
で設定できる
書籍では、長押しするとコンテキストメニューを表示するという仕様がありました。
Jetpack Compose で長押しイベントを設定する方法を調べたところ、以下の回答がありました。
kotlin - Button Long Press Listener in Android jetpack compose - Stack Overflow
ただし、公式ドキュメントを見ると、現時点では combinedClickable
は Experimental な機能のようです。
Compose 修飾子のリスト | Jetpack Compose | Android Developers
そのため、Composableで使うときは @OptIn(ExperimentalFoundationApi::class)
な指定も合わせて行う必要があるようです。
コンテキストメニューは DropdownMenu
で実装する
上記の onLongClick
時に、 DropdownMenu を表示すれば良さそうです。
intent は LocalContext.current
の取得後に context.startActivity
する
書籍では intent でメールやSMSを呼び出していました。
Composeで intent を使う方法を調べたところ、
- LocalContext.current で context を取得
- context を使って context.startActivity する
という流れで実装できました。
- 参考
なお、Composableでない関数で context を使いたい場合は、Composableから context を渡してあげれば良さそうでした。
android - @composable invocations can only happen from the context of an @composable function - Stack Overflow
その他
- 各種ごはんの画像は、いらすとやさんよりダウンロードしました。ありがとうございました。
10章 若くても血圧は記録せよ
書籍では血圧を記録するためのアプリを作ります。
ただ、自宅には血圧計がないため、今回は体重の記録アプリとしました。なお、プロジェクト名は Blood Pressure
のままです。
また、現在のAndroidでデータベースを使うときは Room
を使うことが多いようなので、書籍とは異なり Room
を使って実装することにしました。
KSP版のRoomをインストールする
Roomを使うためには build.gradle
に依存関係を追加する必要がありました。
セットアップ | Room を使用してローカル データベースにデータを保存する | Android デベロッパー | Android Developers
上記のドキュメントを見るとRoomには
- kapt
- ksp
の2つの版がありそうでした。
どちらの方を使うのが良いかを調べたところ、以下の記事類を見るに、KSP版のほうが良さそうでした。
- Android Developers Japan Blog: Kotlin Symbol Processing(KSP)アルファ版のお知らせ
- kaptからkspに変わるとどれくらい早くなるのか〜Room編〜 - Qiita
QiitaではRoomのサポート状況はExperimentalと書かれていましたが、改めて現時点の対応状況を調べると、以下の通り Official Supported されていました。
Supported libraries | Kotlin Symbol Processing API | Kotlin
そのため、今回はKSP版を使うことにしました。
KSP版を使うには、 KSP自体もプロジェクトやアプリの build.gradle に追加する必要があります。
#68 Jetpack ComposeでToDoアプリを作る - Room | Mokelab Blog
KSPの最新バージョンを調べたところ、以下に記載がありました。
Maven Repository: com.google.devtools.ksp » symbol-processing-api
KSP 1.7系では、
- 1.7.10-1.0.6
- 1.7.0-1.0.6
の2つのバージョンがありました。
どちらのバージョンを使えばよいか迷ったため、Compose Compiler Versionを見ることにしました。
Pre-release Kotlin Compatibility | Compose to Kotlin Compatibility Map | Android Developers
1.3.0は出たばっかりなのか、英語版のみ rc
が取れた表記になっていました。今回は出たばっかりのものでハマると大変だと思い、 Compose Compiler Version は 1.2.0
とすることにしました。
そのため、 Compatible Kotlin Version
は 1.7.0
になることから、KSPも 1.7.0-1.0.6
を使えば良さそうでした。
なお、こちらのページでは、日本語版も安定版リリースが 1.3.0 となっています。
Compose Compiler | Android デベロッパー | Android Developers
これでKSPのバージョン指定ができたため、続いてRoomを指定します。Roomは最新バージョン 2.4.3
が使えそうです。
Room | Android デベロッパー | Android Developers
Roomを使うために Entity・DAO・Database を用意し、Applicationを差し替える
公式ドキュメントに従い、
- Entity
- DAO
- Databaseクラス
の3つを用意します。
Room を使用してローカル データベースにデータを保存する | Android デベロッパー | Android Developers
なお、Databaseクラスのインスタンスの生成について、公式ドキュメントでは
val db = Room.databaseBuilder( applicationContext, AppDatabase::class.java, "database-name" ).build()
と書かれていますが、この処理をどこでやるかが気になりました。
Codelabを読むと
アプリに含めるデータベースとリポジトリのインスタンスは、それぞれ 1 つのみにする必要があります。そのための簡単な方法は、これらを Application クラスのメンバーとして作成することです。こうすれば、必要な際にはそのたびに作成する代わりに、アプリから取得するだけで済みます。
とあったため、
ようにしました。
Android Jetpack(Room + Compose)でTodoアプリ作ろう【後編】 - Qiita
Dateの代わりにLocalDateTimeを使い、型コンバータで変換する
書籍では登録時間として Date()
の値をDBに保存しています。
ただ、Dateについて調べると、例えば以下のように java.util.Date
は deprecated という記事が散見されました。
ほとんどが非推奨メソッドとなったjava.util.Date、代替手段と非推奨メソッド利用していた場合の問題とは | セキュリティ対策のラック
そこで、今回は Date ではなく LocalDateTime を使うことにしました。
LocalDateTime | Android Developers
なお、 LocalDateTimeは API 26 で使えるようになったため、 app/build.gradle
の defaultConfig
を minSdk 26
へと変更します。
ただ、RoomではLocalDateTimeをそのまま保存することができないことから、型コンバータを使用してDB上はLong型として保存するようにします。
Room を使用して複雑なデータを参照する | Android デベロッパー | Android Developers
// (略) class LocalDateTimeConverter { @TypeConverter fun fromTimeStamp(value: Long?): LocalDateTime? { val instant: Instant? = value?.let { Instant.ofEpochSecond(it) } return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()) } @TypeConverter fun toTimeStamp(value: LocalDateTime?): Long? { return value?.atZone(ZoneId.systemDefault())?.toEpochSecond() } }
この型コンバータは、Databaseのところでアノテーション @TypeConverters
として以下のように指定すれば良いようです。
@Database(entities = [WeightRecord::class], version = 1, exportSchema = false) @TypeConverters(LocalDateTimeConverter::class) abstract class MyDatabase: RoomDatabase() { abstract fun weightRecordDao(): WeightRecordDao }
DateTimeFormatterを使ってLocalDateTimeを文字列表記にする
書籍だと登録日時を画面に表示しているので、LocalDateTimeを文字列表記にする必要がありました。
方法としては、DateTimeFormatterを使えば良いようです。
Kotlinで日付をフォーマットする方法 | 寝室コンピューティング
Entityで id を autoGenerate するならば、Insert時に渡す id の値は 0 にする
Insert時には各項目を埋める必要がありますが、idは自動生成するようにしたため、何の値を渡せばよいか迷いました。
公式ドキュメントによると
If the field type is Long or Int (or its TypeConverter converts it to a Long or Int), Insert methods treat 0 as not-set while inserting the item.
とあったため、 0
を渡せば良さそうです。
Roomでの読み書きは LaunchedEffect + CoroutineScope + withContext で行う
このあたりは理解しきれていないので、今はできた形だけ書いておきます。
以下を参考にして、Roomでの読み書きが LaunchedEffect + CoroutineScope + withContext で行えるようになりました。
- 【Android】はじめてのRoom - Qiita
- Android Jetpack(Room + Compose)でTodoアプリ作ろう【後編】 - Qiita
- 【Android開発】Roomを使ってデータを保存する方法 | そまちょブログ
- 非同期 DAO クエリを作成する | Android デベロッパー | Android Developers
- Android での Kotlin コルーチン | Android デベロッパー | Android Developers
var weight by rememberSaveable { mutableStateOf("") } if (recordId.isNotEmpty()) { LaunchedEffect(Unit) { CoroutineScope(Dispatchers.Main).launch { withContext(Dispatchers.Default) { val dao = MyApplication.database.weightRecordDao() val record = dao.findById(recordId.toInt()) weight = record.weight.toString() } } } }
本来は深掘りしないといけないんですが、今回はこの程度で...
なお、 LaunchedEffect
については以下を参考にしました。
LaunchedEffect|サンプルで理解するJetpack Composeの副作用の仕組み
Navigation Composeで省略可能な引数を渡すため、クエリパラメータ構文を使う
書籍では、新規登録と編集は同じ画面を使っていました。
今回は
recordId
を渡さない場合、新規登録として扱うrecordId
を渡す場合、編集として扱う
とします。
そのため、Navigation Composeへ recordId
を渡す/渡さないで制御できれば良さそうでした。
調べてみると、以下に省略可能な引数の渡し方が書いてありました。
省略可能な引数の追加 | Compose を使用したナビゲーション | Jetpack Compose | Android Developers
そのため、編集側の定義を
NavHost(navController = navController, startDestination = "main") { composable( "edit?recordId={recordId}", arguments = listOf(navArgument("recordId") { defaultValue = "" }) ) { backStackEntry -> backStackEntry.arguments?.getString("recordId")?.let { it -> EditScreen( navController = navController, setShowSnackBar = setShowSnackBar, setMessage = setMessage, recordId = it ) } } }
とします。
そして、新規登録ボタンでは
@Composable fun AddButton(navController: NavController) { FloatingActionButton(onClick = { navController.navigate("edit") }) { Icon(Icons.Filled.Add, contentDescription = "追加") } }
と、何もクエリパラメータ構文でクエリパラメータなしで定義します。
一方、編集時は
Column( modifier = Modifier.clickable { navController.navigate("edit?recordId=${weightRecord.id}") // これ } ) { weightRecord.recordedAt?.let { Text(text= it.format(dtf)) } Text(text="No. ${weightRecord.id}") Text(text="Weight: ${weightRecord.weight}") }
のようにクエリパラメータ付きで定義することで、実現できました。
Snackbarは、 LaunchedEffect とともに使う
「登録しました」というメッセージを出すために、今回は Snackbar を使います。
Snackbarの使い方を調べたところ、以下に LaunchedEffect
とともに使う例がありました。
- android - How to show snackbar with a button onclick in Jetpack Compose - Stack Overflow
- Configuring Snackbar in Jetpack Compose when using Scaffold with Bottom Navigation
そのため、一番上の Composable で
val (message, setMessage) = remember { mutableStateOf("") }
と、新規登録/編集画面から渡されるメッセージを保持し、メイン画面のComposableで
val scaffoldState = rememberScaffoldState() if (showSnackBar) { LaunchedEffect(scaffoldState.snackbarHostState) { val result = scaffoldState.snackbarHostState.showSnackbar( message = message, actionLabel = "閉じる" ) when (result) { SnackbarResult.Dismissed -> { setShowSnackBar(false) } SnackbarResult.ActionPerformed -> { setShowSnackBar(false) } } } }
のように message
の中身をSnackbarで表示させることで、やりたいことができました。
Compose 1.2.0から、Scaffoldの content に padding パラメータの利用が必須化した
体重記録アプリで初めて 1.2.0 系を使うようにしたところ、
Jetpack Compose: Content padding parameter it is not used
のようなエラーメッセージが出るようになりました。
調べてみたところ、1.2.0系からScaffoldのcontentには padding を渡す必要があるようです。
android - Content padding parameter it is not used - Stack Overflow
そのため、上記の回答通り、 padding を渡すようにしたところ、エラーメッセージは表示されなくなりました。
ソースコード
Githubに上げました。
https://github.com/thinkAmi-sandbox/tsukuwaka_android_with_kotlin