- 投稿日:2020-07-27T20:42:05+09:00
kaptでjavaの予約語のプロパティを使わないほうがいい
TL;DR
kotlin -> javaのcode generationを行うときは変数名にjavaの予約語は使わないほうがいいよ
kaptとは
- javaのAnnotation processorsをkotlinでサポートするpluginのこと
- android開発ではおなじみのdagger2やDataBindingで使用することが多い
- アノテーションを付けたクラスを付けてkaptを走らせるとアノテーションを読み取ってコードを自動生成してくれる
見つかった経緯
- androidの公式ORMのRoomを使用していてぶつかりました
- ある公開APIの返却のレスポンスjsonでキー名がcatchというものをその定義のままRoomを使用してローカルDBに保存しようとしてたらビルドがこけて調べました
Room
- Roomはgoogleが作っているSQLiteを抽象化してくれるライブラリです
- 公式HP
- Roomは下記のように@Entityがついているclassをテーブル定義としてkaptでDAO等を自動生成します
@Entity data class StringEntity( @PrimaryKey(autoGenerate = false) val id: Int, val catch: String )確認方法
- サンプルプロジェクトを作って試しました
- entityを用意しました
- entityの変数名をcatchにしてビルド
- 下記のようにビルド失敗になりました
エラー: Entities and POJOs must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type). public final class StringEntity { ^ Tried the following constructors but they failed to match: StringEntity(int,java.lang.String) -> [param:id -> matched field:id, param:catch -> matched field:unmatched]\room-catch\app\build\tmp\kapt3\stubs\debug\com\ryunen344\room\column\db\entity\StringEntity.java:9: エラー: Cannot find setter for field. private final int id = 0;原因
- kaptでkotlinのコードから情報を読み取ってjavaのコードにする際にjavaの予約語とぶつかるため失敗するようです
- 既に同様(catchではなくabstract)の問題でissueあげて、won't fix張られてるものがありました
- void, catch, abstract, javaのprimitive型名にするとkaptが失敗することまでは確認しました
結論
kaptだけでなくkotlinを書くときはjavaを意識して書いたほうが良い(当たり前っちゃ当たり前)
- 投稿日:2020-07-27T19:14:02+09:00
Android 11でShortcutBadgerを使えるようにする
ShortcutBadgerが使えない!
Android 11対応を行っていたところ、バッジの更新が出来なくなっていることに気が付きました。
アプリは ShortcutBadger を使用してバッジの更新をしていました。原因と対策
原因は Android 11 から追加された Package Visibility によって、他のパッケージへのアクセスが制限されたことによるものでした。
対応は、ShortcutBadgerのintentを許可するよう、マニフェストファイルに追加するだけです。
AndroidManifest.xml<manifest ...> <application> ... </application> <queries> <intent> <action android:name="android.intent.action.BADGE_COUNT_UPDATE"/> </intent> </queries> </manifest>
- 投稿日:2020-07-27T18:43:31+09:00
Unit Test 探求記(その3)
はじめに
前回は、build.gradle から build.gradle.kts への書き換えを行いました。
今回は、LiveData のテストを行ってみようと思います。
テスト対象
今回は、Transformations#map(LiveData, Function) に対して複数の LiveData を受けられるようにしたメソッドを作成して、それをテスト対象とします。
◆ TransformationsUtils
▶ src
TransformationsUtilspackage com.objectfanatics.commons.androidx.lifecycle import androidx.annotation.MainThread import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData object TransformationsUtils { /** * @see androidx.lifecycle.Transformations.map */ @MainThread fun <T> map(vararg sources: LiveData<out Any?>, function: () -> T): LiveData<T> = MediatorLiveData<T>().apply { sources.forEach { source -> addSource(source) { value = function() } } } }テスト構想
テストを作りこむ前に、テストの大まかな流れをコーディングしてみます。
◆ TransformationsUtilsTest(失敗)
▶ src
TransformationsUtilsTest.ktpackage com.objectfanatics.commons.androidx import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.objectfanatics.commons.androidx.lifecycle.TransformationsUtils import org.junit.Test class TransformationsUtilsTest { @Test fun map() { // 3 つの MutableLiveData を用意 val input1: MutableLiveData<String?> = MutableLiveData() val input2: MutableLiveData<String?> = MutableLiveData() val input3: MutableLiveData<String?> = MutableLiveData() // 上記 3 つの MutableLiveData を受けて文字列を返す LiveData を用意 val output: LiveData<String> = TransformationsUtils.map(input1, input2, input3) { "${input1.value}, ${input2.value}, ${input3.value}" } // 実行結果が標準出力に表示されるように準備 output.observeForever { value -> println("value = $value") } // テストの実行 input1.postValue("red") input2.postValue("blue") input3.postValue("green") } }これを実行してみると、以下のようなエラーになりました。
java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.◆ build.gradle.kts
android.os.Looper#getMainLooper()
の値は Android の実行環境がLooper
にセットするものなので、unit test 環境では利用できません。そこで、以下のように対応します。▶ src
build.gradle.kts差分val archVersion = "2.1.0" dependencies { // @see https://developer.android.com/jetpack/androidx/releases/lifecycle // optional - Test helpers for LiveData testImplementation("androidx.arch.core:core-testing:$archVersion") }◆ TransformationsUtilsTest(成功)
▶ src
TransformationsUtilsTest.kt差分import androidx.arch.core.executor.testing.InstantTaskExecutorRule import org.junit.Rule import org.junit.rules.TestRule class TransformationsUtilsTest { @Rule @JvmField val rule: TestRule = InstantTaskExecutorRule() }※
@JvmField
を忘れるとorg.junit.internal.runners.rules.ValidationError: The @Rule 'rule' must be public.
と怒られます。◆ 実行結果
テストの実行により、以下の文字列が表示されました。
value = red, null, null value = red, blue, null value = red, blue, greenテスト実装
red, blue, green の内容だとサンプルとして味気ないので、テストの内容を以下のように変更しました。1
- 場面設定
- 名前と苗字を入力するための入力フォームがある。
- 送信ボタンは、名前と苗字の両方が空でない場合のみ押すことができる。
- 仕様
- 名前入力用の MutableLiveData がある。
- 苗字入力用の MutableLiveData がある。
- 送信ボタンの押下可否を表す LiveData を TransformationsUtils.map() によって作成する。
- シナリオ
- 最初は、名前も苗字も null.
- 名前を入力する。苗字が null なので送信ボタンは押せない。
- 苗字も入力する。名前も苗字も空でないので、送信ボタンは押せる。
◆ kotlin:build.gradle.kts
mockk への依存関係を追加します。
▶ src
build.gradle.kts差分dependencies { // for mockk // @see https://mockk.io/#installation testImplementation("io.mockk:mockk:1.10.0") }◆ TransformationsUtilsTest
結果が表示されるのではなく、結果を検証するようにコードを変更します。
▶ src
TransformationsUtilsTest.kt抜粋class TransformationsUtilsTest { @Rule @JvmField val rule: TestRule = InstantTaskExecutorRule() /** * フォーム(名前、苗字)の両方が空でない時にのみ送信ボタンが押せるという内容のテストケース。 */ @Test fun map() { // テストの実行経過を記録する Recorder の用意 val recorder = mockk<Recorder<Boolean>>(relaxed = true) // 名前、苗字の MutableLiveData を用意 val firstName: MutableLiveData<String?> = MutableLiveData() val lastName: MutableLiveData<String?> = MutableLiveData() val forms = listOf(firstName, lastName) // 送信ボタンが押せるかどうかの MutableLiveData を用意。 // ※名前、苗字の両方が空でない場合のみ true になる val isSubmitButtonEnabled: LiveData<Boolean> = TransformationsUtils.map(firstName, lastName) { !forms.any { it.value.isNullOrEmpty() } } // 実行経過が Recorder に記録されるように準備 isSubmitButtonEnabled.observeForever { value -> recorder.record(value) } // テストの実行 firstName.postValue("シャミ子") lastName.postValue("吉田") // 実行内容の検証 verify { // firstName が "シャミ子" になったが lastName が null のため false recorder.record(false) // firstName が "シャミ子"、lastName が "吉田" になったため true recorder.record(true) } // 実行内容の検証がすべて完了したことの確認 confirmVerified(recorder) } interface Recorder<T> { fun record(value: T) } }テスト結果
おわりに
今回は、mockk を用いて LiveData の unit test を行いました。
本来はこのようなシナリオにするのは良くないのですが、わかりやすさのためにあえてやりました。後悔はしてない。 ↩
- 投稿日:2020-07-27T13:51:33+09:00
中華の闇 携帯電話
4月1日から使い始めた中華製のスマホUMIDIGI X ついに壊れました。(7月26日現在)
115日間で壊れたことになります。
壊れた場所は、type cのソケット毎日、PCに接続して充電したり、Visual studioのadb に接続していた。
原因は、ケーブル側のソケットの形状が悪いために、本体側のソケットが破損される。
アマゾンで購入すると1年間の保証付きである。
当然修理依頼のチャットを出した。らちが明かないので、電話した。
なんだ修理してくんない!,金返すから送れになった。
中国人女性は、電話の向こうで賢そうな応対してくれた。(ハッカーに対して高圧的なしゃべり)
日本語勉強しても、日本的な女性的なものの勉強がたりない。
要するに、通常の使い方していたかいなかを論破しようとした。
この電話を使い始めた理由は、ジャーナリストから中華スマホで個人情報抜かれているエビデンス作ることが目的である。中華スマホは、個人情報を盗むか?
コロナアプリで 日本政府に個人情報を盗まれました。
googleに個人情報を盗まれました。
UmidigiにUpdateする際に位置情報と電話番号盗まれました。
Rakuten unlimitに位置情報 盗まれました。
家内に顔認証を破られました。
個人的感想は、そんなチップ入ってないし、OSもGoogleにおんぶにだっこみたいな携帯だった。
AmazonのUmudigi担当に勧められるままにS5Proを注文した。
Mediatek Helio G90T 2GHz 8 Core Arm めちゃくちゃはやい。
富岳と同じ系統のCPU RISC(単純命令セット)パケットモニターするのにUserLandを使っているんだけど相性抜群によい。
携帯でLinux/Ubuntuとかpython,C,Fortranまで使える。
結局、1万円高いデバイス買わされた。まあいっか~! Rasberry pi4が買えなくなった!こうなったらUSBにGPIOつけたら、無停電付きのRaspiにしてやるぞ。
- 投稿日:2020-07-27T08:49:22+09:00
Unit Test 探求記(その2)
はじめに
前回は、Android Studio を用いて自動生成されたプロジェクトの unit test を確認してみました。
- Android Studio から Android プロジェクトを自動生成
- unit test と instrumented unit test を実行
- 依存関係の確認
今回は、build.gradle を build.gradle.kts に書き換えてみようと思います。
root
◆ build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = "1.3.72" repositories { google() jcenter() } dependencies { classpath "com.android.tools.build:gradle:4.0.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir }◆ build.gradle.kts
// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { val kotlinVersion by extra { "1.3.72" } repositories { google() jcenter() } dependencies { classpath("com.android.tools.build:gradle:4.0.1") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } } task("clean", Delete::class) { delete = setOf(rootProject.buildDir) }app
◆ build.gradle
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 defaultConfig { applicationId "com.objectfanatics.myapplication" minSdkVersion 23 targetSdkVersion 29 versionCode 1 versionName "1.0" // JUnit 4 のテストクラス群を利用するため、AndroidJUnitRunner をデフォルトの test instrumentation runner としてセットしている。 // instrumented unit test とは実機(やエミュレータ)上で実行される unit test のこと。 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.0' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' // 通常の unit test から利用される JUnit 4 のクラス群への依存。※ org.junit.Test 等 testImplementation 'junit:junit:4.12' // instrumented unit test から利用される JUnit 4 のクラス群への依存。※ AndroidJUnit4, ActivityScenarioRule 等 androidTestImplementation 'androidx.test.ext:junit:1.1.1' // Espresso testing framework への依存。 // Espresso testing framework は UI テストのためのフレームワーク。 // @see https://developer.android.com/training/testing/ui-testing/espresso-testing androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' }◆ build.gradle.kts
val kotlinVersion: String? by extra plugins { id("com.android.application") kotlin("android") id("kotlin-android-extensions") } android { compileSdkVersion(29) defaultConfig { applicationId = "com.objectfanatics.myapplication" minSdkVersion(23) targetSdkVersion(29) versionCode = 1 versionName = "1.0" // JUnit 4 のテストクラス群を利用するため、AndroidJUnitRunner をデフォルトの test instrumentation runner としてセットしている。 // instrumented unit test とは実機(やエミュレータ)上で実行される unit test のこと。 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { getByName("release") { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } } dependencies { implementation(fileTree("dir" to "libs", "include" to listOf("*.jar"))) implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion") implementation("androidx.core:core-ktx:1.3.0") implementation("androidx.appcompat:appcompat:1.1.0") implementation("androidx.constraintlayout:constraintlayout:1.1.3") // 通常の unit test から利用される JUnit 4 のクラス群への依存。※ org.junit.Test 等 testImplementation("junit:junit:4.12") // instrumented unit test から利用される JUnit 4 のクラス群への依存。※ AndroidJUnit4, ActivityScenarioRule 等 androidTestImplementation("androidx.test.ext:junit:1.1.1") // Espresso testing framework への依存。 // Espresso testing framework は UI テストのためのフレームワーク。 // @see https://developer.android.com/training/testing/ui-testing/espresso-testing androidTestImplementation("androidx.test.espresso:espresso-core:3.2.0") }おわりに
今回は、build.gradle から build.gradle.kts への書き換えを行ってみました。
- 投稿日:2020-07-27T07:51:08+09:00
Unit Test 探求記(その1)
はじめに
Unit test に関して、あまりトピックを絞らずに、まったりと実験しつつ、その過程を綴ってみようと思います。
新規プロジェクトの作成
まずは、Android Studio にて、新規のプロジェクトを作ってみました。1
Unit Test の実行
ここで言う unit test とは、src/test 以下のテストのことを指しています。
わざと間違えて、テストが実行されていることを確認してみます。2
class ExampleUnitTest { @Test fun addition_isCorrect() { // assertEquals(4, 2 + 2) // わざと間違えてみる。 assertEquals(5, 2 + 2) } }$ ./gradlew clean test --info com.objectfanatics.myapplication.ExampleUnitTest > addition_isCorrect FAILED java.lang.AssertionError: expected:<5> but was:<4> at org.junit.Assert.fail(Assert.java:88) at org.junit.Assert.failNotEquals(Assert.java:834) at org.junit.Assert.assertEquals(Assert.java:645) at org.junit.Assert.assertEquals(Assert.java:631) at com.objectfanatics.myapplication.ExampleUnitTest.addition_isCorrect(ExampleUnitTest.kt:18)ちゃんとテストが失敗していることを確認できました。
Instrumented Unit Test の実行
ここで言う instrumented unit test とは、src/androidTest 以下のテストのことを指しています。
わざと間違えて、テストが実行されていることを確認してみます。3
@RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext // assertEquals("com.objectfanatics.myapplication", appContext.packageName) fail("I made a mistake on purpose.") } }実機を接続して、実行:
$ ./gradlew clean connectedAndroidTest > Task :app:connectedDebugAndroidTest Starting 1 tests on ASUS_X00PD - 8.0.0 com.objectfanatics.myapplication.ExampleInstrumentedTest > useAppContext[ASUS_X00PD - 8.0.0] FAILED java.lang.AssertionError: I made a mistake on purpose. at org.junit.Assert.fail(Assert.java:88) > Task :app:connectedDebugAndroidTest FAILEDちゃんとテストが失敗していることを確認できました。
依存関係を確認してみる
以下に、テスト関連の設定を抜粋し、コメントしてみます。4
android { // JUnit 4 のテストクラス群を利用するため、AndroidJUnitRunner をデフォルトの test instrumentation runner としてセットしている。 // instrumented unit test とは実機(やエミュレータ)上で実行される unit test のこと。 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } } dependencies { // 通常の Unit Test から利用される JUnit 4 のクラス群への依存。※ org.junit.Test 等 testImplementation 'junit:junit:4.12' // instrumented unit test から利用される JUnit 4 のクラス群への依存。※ AndroidJUnit4, ActivityScenarioRule 等 androidTestImplementation 'androidx.test.ext:junit:1.1.1' // Espresso testing framework への依存。 // Espresso testing framework は UI テストのためのフレームワーク。 // @see https://developer.android.com/training/testing/ui-testing/espresso-testing androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' }
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
を外しても IDE 上で警告は出ませんが、実行時に以下のようになり、instrumented unit test が失敗します:$ ./gradlew clean connectedAndroidTest > Task :app:connectedDebugAndroidTest Starting 0 tests on ASUS_X00PD - 8.0.0 Tests on ASUS_X00PD - 8.0.0 failed: Instrumentation run failed due to 'Process crashed.' com.android.build.gradle.internal.testing.ConnectedDevice > No tests found.[ASUS_X00PD - 8.0.0] FAILED No tests found. This usually means that your test classes are not in the form that your test runner expects (e.g. don't inherit from TestCase or lack @Test annotations). > Task :app:connectedDebugAndroidTest FAILED FAILURE: Build failed with an exception.エラーの表示から依存関係の指定漏れであることを推測するのは難しそうですね、、、。
おわりに
今回は、Android Studio がデフォルトで生成した Android プロジェクトの unit test 関連コードを確認してみました。
https://github.com/beyondseeker/chrono0013/commit/87e62c2e76ce9f018291f2cd8cdcf2f7bb7d9873 ↩
https://github.com/beyondseeker/chrono0013/blob/7c3bb714cdbd749c4275ab2101c26ec2fce795be/app/src/test/java/com/objectfanatics/myapplication/ExampleUnitTest.kt ↩
https://github.com/beyondseeker/chrono0013/blob/c02fd3911744ac8ff6965b9df6c5e6eb86f838aa/app/src/androidTest/java/com/objectfanatics/myapplication/ExampleInstrumentedTest.kt ↩
https://github.com/beyondseeker/chrono0013/blob/4c19532e449c025eee9ec76365a69e45c31b5cfc/app/build.gradle ↩
- 投稿日:2020-07-27T01:24:57+09:00
AWS CodeBuildでFlutterのAdd-to-app向けのMavenリポジトリを作成する。
先日のGDG Tokyo Flutter Meetupの既存アプリにFlutterで新機能を追加しようというセッションに感銘を受けて、FlutterのAdd-to-appを始めました。そのセッションでは
モジュールからAARをビルドして既存アプリに追加
社内Mavenリポジトリを運用していてそこに相乗りするなどのケースという説明があり、MavenリポジトリにAARを置くとネイティブアプリが簡単にビルドできるようになるのでやってみました。
該当セッションのスライドはこちらです。(Google Drive)
既存アプリにFlutterで新機能を追加しよう今回のゴール
公式のAdd-to-appの解説ではbuild.gradleファイルの
repositories
設定に相対パスを設定していますが、それを独自に作成したMevenリポジトリのURLにします。app/build.gradlerepositories { maven { // 今回作るMavenリポジトリのURL url 'https://tfandkusu-maven.s3-ap-northeast-1.amazonaws.com/QuickEchoSoundMemo' } maven { url 'https://storage.googleapis.com/download.flutter.io' } } dependencies { def flutter_module_version = '0.0.1' debugImplementation "jp.bellware.echo.sound_memo:flutter_debug:$flutter_module_version" profileImplementation "jp.bellware.echo.sound_memo:flutter_profile:$flutter_module_version" releaseImplementation "jp.bellware.echo.sound_memo:flutter_release:$flutter_module_version" }前提
- AndroidネイティブなアプリにFlutterで作った画面を追加します。
- FlutterモジュールのソースコードはGithubにホストします。
- AARのビルドにAWS CodeBuildを使用します。
- MavenリポジトリにAWS S3を使います。今回は静的なウェブサイトとして誰からでも読み取れる設定にします。
手順
Mavenリポジトリを構成するためのAWS S3バケットを作成する
AWS S3のコンソール画面を開きます。
バケット名とリージョンを設定します。バケット名は世界全体でユニークである必要があります。
今回は誰でも読み取れるMavenリポジトリを作るので、パブリックアクセスを許可します。
あとは特に設定を変更せずに
バケットを作成
ボタンを押します。作成したバケットに対して
バケットポリシー
を設定します。設置したファイルはすべて誰からでも読めるようにします。バケットポリシー{ "Version": "2012-10-17", "Statement": [ { "Sid": "PublicRead", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::tfandkusu-maven/*" } ] }公式のこちらの情報を参考しました。
AWS CodeBuildのビルドプロジェクトを作成する
AWS CodeBuildのコンソール画面を開きます。
ビルドプロジェクトを作成する
ボタンを押します。プロジェクト名、説明を入力して
ビルドバッジを有効にする
にチェックをつけます。ビルドバッジは補足で解説します。Flutterモジュールのソースコードがどこにあるか指定します。今回はソースプロバイダにGitHubを選択して、GitHubに接続してGitHubのリポジトリを指定しました。
ビルド発動の条件を設定します。今回はmasterブランチへのPUSHで発動する設定にしました。
ビルドに使うDockerイメージなどの環境を設定します。
CodeBuild に用意されている Docker イメージにAndroid用はありますがFlutter用は無いので、カスタムイメージ
、その他のレジストリ
でDockerHubにあるcirrusci/flutterイメージを指定しました。flutter
で検索したら一番ダウンロード数が多いイメージでした。ビルドコマンドの設定についてはデフォルトのソース直下にあるbuildspec.ymlファイルを参照する設定にしました。
ビルドした結果の格納先を設定します。
タイプ
をAmazon S3
にして前の手順で作成したバケット名を入力します。名前
もMavenリポジトリのURLに含まれることになるので、わかりやすい名前をつけます。今回は静的なウェブサイト公開になるのでアーティファクト暗号化の削除
にチェックを入れます。buildspec.ymlを作成する
AWS CodeBuildはbuildspec.ymlファイルの内容に従ってビルドを実行します。今回はこのような内容にしました。
buildspec.ymlversion: 0.2 phases: install: # flutterモジュールのpubspec.yamlからバージョンを取得するために # yqコマンドをインストールする commands: - apt update - apt install -y gnupg2 - apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CC86BB64 - add-apt-repository -y ppa:rmescandon/yq - apt install -y yq build: # AARを作成する commands: - flutter build aar --build-number `yq r pubspec.yaml version` artifacts: # アーティファクトとしてS3にコピーするディレクトリを指定する files: - '**/*' base-directory: build/host/outputs/repo単純にAARを作成するだけならば、
flutter build aar
を実行するだけで良いですが、そうするとバージョンが1.0
固定になってしまいます。flutterモジュールのpubspec.yamlファイルのversionフィールドは無視されます。Androidネイティブアプリのgradleによるビルドでは、Mavenリポジトリからダウンロードされたflutterモジュールは一度ダウンロードされると中身が変わっても再ダウンロードされません。よって変更の度にバージョンを上げる必要があります。
またAARのバージョンはコマンド引数で指定する必要があります。flutter build aar --build-number 0.0.1
今回はコマンド引数に渡すバージョンをpubspec.yamlファイルのversionフィールドから流用するようにしました。
pubspec.yamlname: sound_memo description: Sound memo module for Quick Echo app. # 略 version: 0.0.1 # これを流用 # 略そのためにはyamlファイルをパースする必要があり、今回はyqコマンドを使うことにしました。このようにyqコマンドを使うとversionフィールドの値だけを標準出力できます。
yq r pubspec.yaml version今回使用したDockerイメージcirrusci/flutterはUbuntuをベースに作られていたので、yqコマンドのreadme.mdを参考にaptコマンドなどでyqコマンドをインストールしています。
ビルドの度にyqコマンドをインストールしているところが気になる場合は、初めからyqコマンドが入っているDockerイメージを作成してDockerHubにpushすると良いと思います。AWS CodeBuildでビルドを実行する
ここまで準備できたらflutterモジュールのソースコードをmasterブランチにPUSHします。ビルドの様子をコンソールから見ることが出来ます。
完了するとS3にビルド結果が格納されます。アーティファクトで設定した名前(今回はQuickEchoSoundMemo)はS3でのパスに使われます。
オブジェクトを公開する設定にしているので認証なしでWebアクセスが可能です。
https://tfandkusu-maven.s3-ap-northeast-1.amazonaws.com/QuickEchoSoundMemo/jp/bellware/echo/sound_memo/flutter_debug/maven-metadata.xml
ホスト名はバケット名とリージョンから作られパスはS3でのパスそのままになります。AndroidネイティブプロジェクトからFlutterモジュールを使う
公式のAdd-to-appの解説ではapp/build.gradleファイルの
repositories
設定に相対パスを設定していますが、それをS3にアップロードしたファイルのWebアクセスURLにします。app/build.gradlerepositories { maven { // S3にアップロードしたファイルのWebアクセスURLにする。 url 'https://tfandkusu-maven.s3-ap-northeast-1.amazonaws.com/QuickEchoSoundMemo' } maven { url 'https://storage.googleapis.com/download.flutter.io' } } dependencies { def flutter_module_version = '0.0.1' debugImplementation "jp.bellware.echo.sound_memo:flutter_debug:$flutter_module_version" profileImplementation "jp.bellware.echo.sound_memo:flutter_profile:$flutter_module_version" releaseImplementation "jp.bellware.echo.sound_memo:flutter_release:$flutter_module_version" }あとはAdd-to-appの公式解説に従いFlutterActivityを呼び出したり、FlutterFragmentを設置したりして使います。
補足
ビルドバッジ
AWS CodeBuildではビルドの結果を画像として配信する機能があります。
バッジURLのコピー
ボタンを押してクリップボードのURLをGithubのREADME.mdにimgタグで貼り付けます。AWS CodeBuildを使っていることや、その成功失敗が分かりやすくなります。
Slack通知
AWS CodeBuildの結果をSlackに投稿することが出来ます。設定方法はこちらの記事をご参照ください。
CodeBuild の結果をSlack に通知する (CodeBuild + Amazon SNS + AWS Chatbot)
- 投稿日:2020-07-27T01:10:58+09:00
Sunflowerリポジトリで学ぶJetPack〜WorkManager編
新型コロナの影響で自宅待機になってしまい、その間勉強するものとして
Sunflower
リポジトリを
勧めてもらいました。JetPackのライブラリのうち、今回は
WorkManager
編です尚、引用しているソースは明記しているところ以外は、基本的には全て
Sunflower
のリポジトリのものです。環境
- 確認時は
Android Studio
のバージョンは3.6.2
を使用しましたJetPack
はAndroidX
ライブラリを利用するのでCompile SDK
を28
以上にする必要がありますそもそも
WorkManager
ってなに?WorkManager API を使用すると、アプリが終了したりデバイスが再起動したりしても 実行することが要求される延期可能な非同期タスクのスケジュールを簡単に設定できます。です!
WorkManager
の主な機能機能
- API 14 までの下位互換性
- API 23 以上が搭載されたデバイスでは
JobScheduler
を使用- API 14~22 が搭載されたデバイスでは
BroadcastReceiver
とAlarmManager
を組み合わせて使用- ネットワークの可用性や充電ステータスなどの処理の制約を追加する
- 非同期の 1 回限りのタスクや定期的なタスクのスケジュールを設定する
- スケジュール設定されたタスクの監視と管理を行う
- タスクを連携させる
- アプリやデバイスが再起動してもタスクを確実に実行する
- Doze モードなどの省電力機能に準拠する (公式より)
この中だと、APIレベルが低いものでも同じように使えるというところと、
Dozeモード
に対応しているのがいいですね
(Dozeモード中
にバックグラウンド処理が色々動かない、というのはAndroidあるあるだと思いますので。。。)注意点
公式によると
WorkManager は、アプリが終了したりデバイスが再起動したりしても確実に実行する必要がある遅延可能なタスク(つまり、直ちに実行する必要がないタスク)を対象としています。次に例を示します。
- ログやアナリティクスをバックエンド サービスに送信する
- アプリデータをサーバーと定期的に同期する
つまり、確実に定周期で何かをする、などということには対応していない、ということですね。
利用する際に必要なこと
必要なことは下記の4点です
- 依存関係の記載
- バックグラウンドタスクの作成
- タスクを実行する方法とタイミングを設定とタスクをシステムに引き渡す処理
依存関係の記載
build.gradle
に記載する内容
build.gradle
には下記の依存関係の記載を行います。
(公式ドキュメントより)build.gradledependencies { def work_version = "2.3.1" // (Java only) implementation "androidx.work:work-runtime:$work_version" // Kotlin + coroutines implementation "androidx.work:work-runtime-ktx:$work_version" // optional - RxJava2 support implementation "androidx.work:work-rxjava2:$work_version" // optional - GCMNetworkManager support implementation "androidx.work:work-gcm:$work_version" // optional - Test helpers androidTestImplementation "androidx.work:work-testing:$work_version" }
- 必須設定
Java
Kotlin + Coroutines
Java
で使う場合は、Java
の設定が、Kotlin
で使う場合はKotlin
の設定が必須となります。- オプション設定
RxJava2
GCMNetworkManager
Test helpers
- これらはオプションなので、利用する場合は記載します。
Sunflower
リポジトリの場合
Sunflower
リポジトリの場合はどのようになっているかを見てみましょうbuild.gradlebuildscript { // Define versions in a single place ext { : // App dependencies : workVersion = '2.1.0' } : }app/build.gradledependencies { : implementation "androidx.work:work-runtime-ktx:$rootProject.workVersion" : // Testing dependencies : androidTestImplementation "androidx.work:work-testing:$rootProject.workVersion" : }
Sunflower
リポジトリの場合は、下記設定となっていました。
Kotlin
で利用しています- オプションは
Test helpers
を利用していますバックグラウンド タスクを作成する
それでは
Sunflower
のバックグラウンドタスクを見てみましょう。SeedDatabaseWorker.kt(part1)class SeedDatabaseWorker( context: Context, workerParams: WorkerParameters ) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result = coroutineScope { . . .
- タスクは通常は
Worker
クラスを使用して定義しますWorkmanager
から提供されるバックグラウンドスレッドでdowork()
メソッドが同期的
に実行されます- ただし、
Sunflower
リポジトリではCoroutineWorker
を利用しています。
- こちらは
Kotlin
を使う際にオススメです- その場合
doWork()
メソッドはsuspend
関数になり、サブスレッドで実行されます。
- 実行するスレッドは、
Worker
クラスとは異なり、Configuration
で指定されたExecutor
ではありません。Dispatchers.Default
で実行されます。SeedDatabaseWorker.kt(part2)try { applicationContext.assets.open(PLANT_DATA_FILENAME).use { inputStream -> JsonReader(inputStream.reader()).use { jsonReader -> val plantType = object : TypeToken<List<Plant>>() {}.type val plantList: List<Plant> = Gson().fromJson(jsonReader, plantType) val database = AppDatabase.getInstance(applicationContext) database.plantDao().insertAll(plantList) Result.success() } } } catch (ex: Exception) { Log.e(TAG, "Error seeding database", ex) Result.failure() } } companion object { private val TAG = SeedDatabaseWorker::class.java.simpleName } }
- 以降には、バックグラウンドタスクで実施する処理が記載されています。
- ここでは
assets
内のJsonファイルを読み込んだあと、パースしてデータベースに保存しています。doWork()
メソッドはResult
の各メソッドを返します。
Result.success()
: タスクが正常に終了したかどうかResult.failure()
: タスクが失敗したかどうかResult.retry()
: 後でタスクを再試行する必要があるかどうか
- ここでは、データベースに保存ができれば正常終了、例外が発生した場合は失敗となっています。
タスク実行方法とタイミングを設定およびタスクをシステムに引き渡す処理
概要
Worker(CoroutineWorker)
が作業単位を定義しますが、それに対し、WorkRequest
は作業を実行する方法とタイミングを定義します。- タスクには1回実行するものと、定期的に実行されるものがあります
- 1回のもの →
OneTimeWorkRequest
- 定期的なもの →
PeriodicWorkRequest
WorkRequest
を定義した後、WorkManager
でenqueue()
メソッドを使用してスケジュールを設定できます。
Sunflower
リポジトリAppDatabase.kt// Create and pre-populate the database. See this article for more details: // https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785 private fun buildDatabase(context: Context): AppDatabase { return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) .addCallback(object : RoomDatabase.Callback() { override fun onCreate(db: SupportSQLiteDatabase) { super.onCreate(db) val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build() WorkManager.getInstance(context).enqueue(request) } }) .build() }
Sunflower
リポジトリでは下記の処理になっています。
- データベース作成時に一度だけ実施する処理
OneTimeWorkRequestBuilder
でSeedDatabaseWorker
を実施している。まとめ
- バックグラウンドタスクの作成は
Worker
クラスを作成する。Kotlin
の場合はCoroutineWorker
がおすすめWorker
クラスのdowork()
メソッド内に実行する処理を書く。- タスクの実行の際、1回しか実施しないことは、
OneTimeWorkRequest
、定期的に実施するものはPeriodicWorkRequest
で実行する。以上です!
参考サイト
- Sunflowerリポジトリ
- WorkManager: 公式ドキュメント
- 投稿日:2020-07-27T01:09:30+09:00
【Room】Sunflowerリポジトリで学ぶJetPack〜Room編
新型コロナの影響で自宅待機になってしまい、その間勉強するものとして
Sunflower
リポジトリを
勧めてもらいました。JetPackのライブラリのうち、今回は
Room
編です尚、引用しているソースは明記しているところ以外は、基本的には全て
Sunflower
のリポジトリのものです。環境
- 確認時は
Android Studio
のバージョンは3.6.2
を使用しましたJetPack
はAndroidX
ライブラリを利用するのでCompile SDK
を28
以上にする必要がありますそもそも
Room
ってなに?公式の説明
Room 永続ライブラリは SQLite 全体に抽象化レイヤを提供することで、 データベースへのより安定したアクセスを可能にし、 SQLite を最大限に活用できるようにします。です!
Room
を使う理由
- アプリ内ではオブジェクトを利用して、
SQLite
のデータベースにアクセスできる。- コンパイル時にチェックができる
(
Sunflower
のリポジトリのトップページのRoom
の説明より)利用する際に必要なこと
必要なことは下記の4点です
- 依存関係の記載
- データベース作成
- エンティティ作成
- DAO作成
依存関係の記載
build.gradle
に記載する内容
build.gradle
には下記の依存関係の記載を行います。
(公式ドキュメントより)build.gradledependencies { def room_version = "2.2.3" implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor // optional - Kotlin Extensions and Coroutines support for Room implementation "androidx.room:room-ktx:$room_version" // optional - RxJava support for Room implementation "androidx.room:room-rxjava2:$room_version" // optional - Guava support for Room, including Optional and ListenableFuture implementation "androidx.room:room-guava:$room_version" // Test helpers testImplementation "androidx.room:room-testing:$room_version" }
- 注意点
room-compiler
の部分については、Kotlin
で使用する場合はannotationProcessor
ではなく、kapt
にします- オプション設定
Kotlin
RxJava
Guava
Test helpers
- これらはオプションなので、利用する場合は記載します。
Sunflower
リポジトリの場合
Sunflower
リポジトリの場合はどのようになっているかを見てみましょうbuild.gradlebuildscript { // Define versions in a single place ext { : // App dependencies : roomVersion = '2.1.0' : } : }app/build.gradledependencies { kapt "androidx.room:room-compiler:$rootProject.roomVersion" : implementation "androidx.room:room-runtime:$rootProject.roomVersion" implementation "androidx.room:room-ktx:$rootProject.roomVersion" : }
Sunflower
リポジトリの場合は、Kotlin
のオプションのみ利用しているようです。データベースのクラス作成
AppDatabase.kt(part1)/** * The Room database for this app */ @Database(entities = [GardenPlanting::class, Plant::class], version = 1, exportSchema = false) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun gardenPlantingDao(): GardenPlantingDao abstract fun plantDao(): PlantDaoデータベースの設定を行うクラスをみてみましょう。
データベース利用の際の設定的なものは、ここの最初の部分に書かれています。
- クラスの宣言に
@Database
アノテーションを記載します(必須)
- データベースに関連付けられているエンティティのリストをアノテーション内に含む必要があり、
entities
にそれを記載します。(必須)
- ここでは、
GardenPlanting
クラスとPlant
クラスが記載されています。version
はデータベースのバージョンを定義できます。マイグレーション処理を書く際はこちらのバージョンを参照できます(任意)exportSheme
はfalse
に設定しています。(任意)
- こちらは
default
でtrue
に設定されます。true
に設定された場合はroom.schemaLocation
がbuild.gradle
に定義されている場合は、データベースのスキーマ情報をJSONファイルにエクスポートします。それを利用するとマイグレーションのテストが実施できます。- 詳しくはこちら
@TypeConverter
アノテーションはentity
に保存できない型がある場合(例えば、GardenPlanting
クラスのplantDate
はCalender型)
)、変換処理を実装したクラスをつくり、そのクラス名を記載します。(任意)
- ここでは、
Converter
クラスにCalender
↔︎long
の相互変換処理が記載されており、そのConverter
クラスを記載しています。
RoomDatabase
を拡張する抽象クラスとしてクラスを宣言します(必須)
abstract class AppDatabase : RoomDatabase()
として宣言しています。引数が 0 で、
@Dao
アノテーション付きのクラスを返す抽象メソッドをクラス内に記載します(必須)
gardenPlantingDao()
メソッドとplantDao()
メソッドを記載してあります。AppDatabase.kt(part2)companion object { // For Singleton instantiation @Volatile private var instance: AppDatabase? = null fun getInstance(context: Context): AppDatabase { return instance ?: synchronized(this) { instance ?: buildDatabase(context).also { instance = it } } }こちらは、シングルトンで利用するための内容が記載されています。
すでにインスタンスが作られている場合は、作られているものを使い、ない場合は作るという処理です
スレッドセーフにするためにsynchronized()
で排他制御を行っています。AppDatabase.kt(part3)// Create and pre-populate the database. See this article for more details: // https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785 private fun buildDatabase(context: Context): AppDatabase { return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) .addCallback(object : RoomDatabase.Callback() { override fun onCreate(db: SupportSQLiteDatabase) { super.onCreate(db) val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build() WorkManager.getInstance(context).enqueue(request) } }) .build() } } }こちらでは、データベースを作成する処理です。
*Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME).build
でデータベースのインスタンスを作成します。
* その間にaddCallback()
メソッドが入っています。
* こちらはデータベースが作成された時に実施されるコールバックメソッドです。
* 例えば、データベースの初期データなどを読み込む必要がある場合などで利用できます。
* ここではWorkManager
を利用し、バックグラウンド処理でjsonファイルから初期値の情報を読み込んで、データベースに保存しています。エンティティの定義
それでは、
Sunflower
リポジトリのGardenPlanting
クラスのエンティティ定義からみてみましょうGardenPlanting.kt(part1)@Entity( tableName = "garden_plantings", foreignKeys = [ ForeignKey(entity = Plant::class, parentColumns = ["id"], childColumns = ["plant_id"]) ], indices = [Index("plant_id")] )
@Entity
のアノテーションをつけてクラスを作成します。(必須)
tableName
に指定した名称が、SQLite
データベース内のテーブル名になります。もし、なにも記載しなかった場合はクラス名がテーブル名になります。(任意)foreignKeys
は外部キー制約の定義を行います。ここでは、Plant
クラスのid
にある値のみ、plant_id
に設定可能となります。(任意)indices
で、このテーブルに設定するインデックスを指定します。この場合はplant_id
をインデックスに設定しています。(任意)GardenPlanting.kt(part2)data class GardenPlanting( @ColumnInfo(name = "plant_id") val plantId: String, /** * Indicates when the [Plant] was planted. Used for showing notification when it's time * to harvest the plant. */ @ColumnInfo(name = "plant_date") val plantDate: Calendar = Calendar.getInstance(), /** * Indicates when the [Plant] was last watered. Used for showing notification when it's * time to water the plant. */ @ColumnInfo(name = "last_watering_date") val lastWateringDate: Calendar = Calendar.getInstance() ) { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var gardenPlantingId: Long = 0 }
@ColumnInfo
で各カラムの定義を行います。変数宣言などは通常の場合と同一です。(必須)@ColumnInfo
アノテーションの中のname
でカラム名を定義します。(任意)
- テーブル名と同様に、指定しなかった場合は変数名がカラム名となります。
- ここでは
plantId
,plantDate
,lastWateringDate
の3つの引数を指定するコンストラクターを定義しています。- プライマリーキーには
@PrimaryKey
のアノテーションをつけて変数を定義します。(任意)
- カラム名
id
、変数名gardenPlantId
がプライマリーキーに指定されています。- ここでは
autoGenerate = true
を指定し、自動的に割り当てるようになっています。
DAO
の定義次に
GardenPlantingDao
をみてみましょう。
DAO
とはData Access Object
の略でデータベースの操作のインターフェースを提供するオブジェクトです。
使う側はデータベースの詳細を知ることなくデータの保存や取得が利用可能です。GardenPlantingDao.kt(Part1)/** * The Data Access Object for the [GardenPlanting] class. */ @Dao interface GardenPlantingDao {
@Dao
アノテーションをつけたインターフェースを定義します。(必須)GardenPlantingDao.kt(Part2)@Query("SELECT * FROM garden_plantings") fun getGardenPlantings(): LiveData<List<GardenPlanting>> @Query("SELECT EXISTS(SELECT 1 FROM garden_plantings WHERE plant_id = :plantId LIMIT 1)") fun isPlanted(plantId: String): LiveData<Boolean> /** * This query will tell Room to query both the [Plant] and [GardenPlanting] tables and handle * the object mapping. */ @Transaction @Query("SELECT * FROM plants WHERE id IN (SELECT DISTINCT(plant_id) FROM garden_plantings)") fun getPlantedGardens(): LiveData<List<PlantAndGardenPlantings>>
@Query
アノテーションは、データベースに対して読み書き処理を実行できます。
- コンパイル時に検証されるので、クエリに問題があると、コンパイルエラーになります
- 戻り値についても検証を行い、誤りがある場合は警告またはエラーを表示します
- クエリにパラメータを渡す場合は、
:[メソッドの引数名]
としてクエリ内に記載します
- ここでは2つめのクエリの中の
:plantId
がそのように使われています@Transaction
アノテーションで記載したメソッドは、全て同一トランザクションで実行します- 各メソッドの戻り値に
LiveData
を指定することができます。LiveData
とRoom
の組み合わせで、例えば、データベースのテーブルが更新された際にView
の更新などができます。詳しくはLiveData編をご覧くださいGardenPlantingDao.kt(Part3)@Insert suspend fun insertGardenPlanting(gardenPlanting: GardenPlanting): Long @Delete suspend fun deleteGardenPlanting(gardenPlanting: GardenPlanting) }
@Insert
アノテーションをつけたメソッドは、データベースに挿入する実装がRoom
によって作成されます。
- 複数のパラメータを指定することができます。
- 全てのパラメータを同一のトランザクションで実行します
- 戻り値を指定すると、挿入されるアイテムの新しい
rowId
になるlong
の値を返すことができます。
rowId
の詳細についてはこちら@Delete
アノテーションをつけたメソッドは、こちらも、@Insert
と同様に、データベースないの指定したエンティティのセットを削除する処理を作成します。ここでは使用されていませんが、
@Update
も同様ですメソッド名の前に
suspend
がついているものは、コルーチン機能を使用した際に非同期かすることができ、メインスレッド上で実行される可能性がなくなります。(サブスレッドで実行して下さい、の意)参考サイト
- Sunflowerリポジトリ
- ViewModel: 公式ドキュメント
- LiveData: 公式ドキュメント
- DataBinding: 公式ドキュメント
- Room: 公式ドキュメント
- MySQL 外部キ−制約:
@IT
の記事- SQLite インデックス: SQLiteのインデックスについて記載されているサイト
- SQLite rowId: SQLiteのrowIdについて
- Room Dao: Room Dao公式ドキュメント
- Room マイグレーション: Roomマイグレーション
- 投稿日:2020-07-27T01:09:30+09:00
Sunflowerリポジトリで学ぶJetPack〜Room編
新型コロナの影響で自宅待機になってしまい、その間勉強するものとして
Sunflower
リポジトリを
勧めてもらいました。JetPackのライブラリのうち、今回は
Room
編です尚、引用しているソースは明記しているところ以外は、基本的には全て
Sunflower
のリポジトリのものです。環境
- 確認時は
Android Studio
のバージョンは3.6.2
を使用しましたJetPack
はAndroidX
ライブラリを利用するのでCompile SDK
を28
以上にする必要がありますそもそも
Room
ってなに?公式の説明
Room 永続ライブラリは SQLite 全体に抽象化レイヤを提供することで、 データベースへのより安定したアクセスを可能にし、 SQLite を最大限に活用できるようにします。です!
Room
を使う理由
- アプリ内ではオブジェクトを利用して、
SQLite
のデータベースにアクセスできる。- コンパイル時にチェックができる
(
Sunflower
のリポジトリのトップページのRoom
の説明より)利用する際に必要なこと
必要なことは下記の4点です
- 依存関係の記載
- データベース作成
- エンティティ作成
- DAO作成
依存関係の記載
build.gradle
に記載する内容
build.gradle
には下記の依存関係の記載を行います。
(公式ドキュメントより)build.gradledependencies { def room_version = "2.2.3" implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor // optional - Kotlin Extensions and Coroutines support for Room implementation "androidx.room:room-ktx:$room_version" // optional - RxJava support for Room implementation "androidx.room:room-rxjava2:$room_version" // optional - Guava support for Room, including Optional and ListenableFuture implementation "androidx.room:room-guava:$room_version" // Test helpers testImplementation "androidx.room:room-testing:$room_version" }
- 注意点
room-compiler
の部分については、Kotlin
で使用する場合はannotationProcessor
ではなく、kapt
にします- オプション設定
Kotlin
RxJava
Guava
Test helpers
- これらはオプションなので、利用する場合は記載します。
Sunflower
リポジトリの場合
Sunflower
リポジトリの場合はどのようになっているかを見てみましょうbuild.gradlebuildscript { // Define versions in a single place ext { : // App dependencies : roomVersion = '2.1.0' : } : }app/build.gradledependencies { kapt "androidx.room:room-compiler:$rootProject.roomVersion" : implementation "androidx.room:room-runtime:$rootProject.roomVersion" implementation "androidx.room:room-ktx:$rootProject.roomVersion" : }
Sunflower
リポジトリの場合は、Kotlin
のオプションのみ利用しているようです。データベースのクラス作成
AppDatabase.kt(part1)/** * The Room database for this app */ @Database(entities = [GardenPlanting::class, Plant::class], version = 1, exportSchema = false) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun gardenPlantingDao(): GardenPlantingDao abstract fun plantDao(): PlantDaoデータベースの設定を行うクラスをみてみましょう。
データベース利用の際の設定的なものは、ここの最初の部分に書かれています。
- クラスの宣言に
@Database
アノテーションを記載します(必須)
- データベースに関連付けられているエンティティのリストをアノテーション内に含む必要があり、
entities
にそれを記載します。(必須)
- ここでは、
GardenPlanting
クラスとPlant
クラスが記載されています。version
はデータベースのバージョンを定義できます。マイグレーション処理を書く際はこちらのバージョンを参照できます(任意)exportSheme
はfalse
に設定しています。(任意)
- こちらは
default
でtrue
に設定されます。true
に設定された場合はroom.schemaLocation
がbuild.gradle
に定義されている場合は、データベースのスキーマ情報をJSONファイルにエクスポートします。それを利用するとマイグレーションのテストが実施できます。- 詳しくはこちら
@TypeConverter
アノテーションはentity
に保存できない型がある場合(例えば、GardenPlanting
クラスのplantDate
はCalender型)
)、変換処理を実装したクラスをつくり、そのクラス名を記載します。(任意)
- ここでは、
Converter
クラスにCalender
↔︎long
の相互変換処理が記載されており、そのConverter
クラスを記載しています。
RoomDatabase
を拡張する抽象クラスとしてクラスを宣言します(必須)
abstract class AppDatabase : RoomDatabase()
として宣言しています。引数が 0 で、
@Dao
アノテーション付きのクラスを返す抽象メソッドをクラス内に記載します(必須)
gardenPlantingDao()
メソッドとplantDao()
メソッドを記載してあります。AppDatabase.kt(part2)companion object { // For Singleton instantiation @Volatile private var instance: AppDatabase? = null fun getInstance(context: Context): AppDatabase { return instance ?: synchronized(this) { instance ?: buildDatabase(context).also { instance = it } } }こちらは、シングルトンで利用するための内容が記載されています。
すでにインスタンスが作られている場合は、作られているものを使い、ない場合は作るという処理です
スレッドセーフにするためにsynchronized()
で排他制御を行っています。AppDatabase.kt(part3)// Create and pre-populate the database. See this article for more details: // https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785 private fun buildDatabase(context: Context): AppDatabase { return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) .addCallback(object : RoomDatabase.Callback() { override fun onCreate(db: SupportSQLiteDatabase) { super.onCreate(db) val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build() WorkManager.getInstance(context).enqueue(request) } }) .build() } } }こちらでは、データベースを作成する処理です。
*Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME).build
でデータベースのインスタンスを作成します。
* その間にaddCallback()
メソッドが入っています。
* こちらはデータベースが作成された時に実施されるコールバックメソッドです。
* 例えば、データベースの初期データなどを読み込む必要がある場合などで利用できます。
* ここではWorkManager
を利用し、バックグラウンド処理でjsonファイルから初期値の情報を読み込んで、データベースに保存しています。エンティティの定義
それでは、
Sunflower
リポジトリのGardenPlanting
クラスのエンティティ定義からみてみましょうGardenPlanting.kt(part1)@Entity( tableName = "garden_plantings", foreignKeys = [ ForeignKey(entity = Plant::class, parentColumns = ["id"], childColumns = ["plant_id"]) ], indices = [Index("plant_id")] )
@Entity
のアノテーションをつけてクラスを作成します。(必須)
tableName
に指定した名称が、SQLite
データベース内のテーブル名になります。もし、なにも記載しなかった場合はクラス名がテーブル名になります。(任意)foreignKeys
は外部キー制約の定義を行います。ここでは、Plant
クラスのid
にある値のみ、plant_id
に設定可能となります。(任意)indices
で、このテーブルに設定するインデックスを指定します。この場合はplant_id
をインデックスに設定しています。(任意)GardenPlanting.kt(part2)data class GardenPlanting( @ColumnInfo(name = "plant_id") val plantId: String, /** * Indicates when the [Plant] was planted. Used for showing notification when it's time * to harvest the plant. */ @ColumnInfo(name = "plant_date") val plantDate: Calendar = Calendar.getInstance(), /** * Indicates when the [Plant] was last watered. Used for showing notification when it's * time to water the plant. */ @ColumnInfo(name = "last_watering_date") val lastWateringDate: Calendar = Calendar.getInstance() ) { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var gardenPlantingId: Long = 0 }
@ColumnInfo
で各カラムの定義を行います。変数宣言などは通常の場合と同一です。(必須)@ColumnInfo
アノテーションの中のname
でカラム名を定義します。(任意)
- テーブル名と同様に、指定しなかった場合は変数名がカラム名となります。
- ここでは
plantId
,plantDate
,lastWateringDate
の3つの引数を指定するコンストラクターを定義しています。- プライマリーキーには
@PrimaryKey
のアノテーションをつけて変数を定義します。(任意)
- カラム名
id
、変数名gardenPlantId
がプライマリーキーに指定されています。- ここでは
autoGenerate = true
を指定し、自動的に割り当てるようになっています。
DAO
の定義次に
GardenPlantingDao
をみてみましょう。
DAO
とはData Access Object
の略でデータベースの操作のインターフェースを提供するオブジェクトです。
使う側はデータベースの詳細を知ることなくデータの保存や取得が利用可能です。GardenPlantingDao.kt(Part1)/** * The Data Access Object for the [GardenPlanting] class. */ @Dao interface GardenPlantingDao {
@Dao
アノテーションをつけたインターフェースを定義します。(必須)GardenPlantingDao.kt(Part2)@Query("SELECT * FROM garden_plantings") fun getGardenPlantings(): LiveData<List<GardenPlanting>> @Query("SELECT EXISTS(SELECT 1 FROM garden_plantings WHERE plant_id = :plantId LIMIT 1)") fun isPlanted(plantId: String): LiveData<Boolean> /** * This query will tell Room to query both the [Plant] and [GardenPlanting] tables and handle * the object mapping. */ @Transaction @Query("SELECT * FROM plants WHERE id IN (SELECT DISTINCT(plant_id) FROM garden_plantings)") fun getPlantedGardens(): LiveData<List<PlantAndGardenPlantings>>
@Query
アノテーションは、データベースに対して読み書き処理を実行できます。
- コンパイル時に検証されるので、クエリに問題があると、コンパイルエラーになります
- 戻り値についても検証を行い、誤りがある場合は警告またはエラーを表示します
- クエリにパラメータを渡す場合は、
:[メソッドの引数名]
としてクエリ内に記載します
- ここでは2つめのクエリの中の
:plantId
がそのように使われています@Transaction
アノテーションで記載したメソッドは、全て同一トランザクションで実行します- 各メソッドの戻り値に
LiveData
を指定することができます。LiveData
とRoom
の組み合わせで、例えば、データベースのテーブルが更新された際にView
の更新などができます。詳しくはLiveData編をご覧くださいGardenPlantingDao.kt(Part3)@Insert suspend fun insertGardenPlanting(gardenPlanting: GardenPlanting): Long @Delete suspend fun deleteGardenPlanting(gardenPlanting: GardenPlanting) }
@Insert
アノテーションをつけたメソッドは、データベースに挿入する実装がRoom
によって作成されます。
- 複数のパラメータを指定することができます。
- 全てのパラメータを同一のトランザクションで実行します
- 戻り値を指定すると、挿入されるアイテムの新しい
rowId
になるlong
の値を返すことができます。
rowId
の詳細についてはこちら@Delete
アノテーションをつけたメソッドは、こちらも、@Insert
と同様に、データベースないの指定したエンティティのセットを削除する処理を作成します。ここでは使用されていませんが、
@Update
も同様ですメソッド名の前に
suspend
がついているものは、コルーチン機能を使用した際に非同期かすることができ、メインスレッド上で実行される可能性がなくなります。(サブスレッドで実行して下さい、の意)参考サイト
- Sunflowerリポジトリ
- ViewModel: 公式ドキュメント
- LiveData: 公式ドキュメント
- DataBinding: 公式ドキュメント
- Room: 公式ドキュメント
- MySQL 外部キ−制約:
@IT
の記事- SQLite インデックス: SQLiteのインデックスについて記載されているサイト
- SQLite rowId: SQLiteのrowIdについて
- Room Dao: Room Dao公式ドキュメント
- Room マイグレーション: Roomマイグレーション
- 投稿日:2020-07-27T01:08:54+09:00
【ViewModel】Sunflowerリポジトリで学ぶJetPack〜ViewModel編
新型コロナの影響で自宅待機になってしまい、その間勉強するものとして
Sunflower
リポジトリを
勧めてもらいました。JetPackのライブラリのうち、今回は
ViewModel
編です
DataBinding編、LiveData編などでも、
チラチラ出てきてはいましたが、見てみぬふりをしていました笑尚、引用しているソースは明記しているところ以外は、基本的には全て
Sunflower
のリポジトリのものです。環境
- 確認時は
Android Studio
のバージョンは3.6.2
を使用しましたJetPack
はAndroidX
ライブラリを利用するのでCompile SDK
を28
以上にする必要がありますそもそも
ViewModel
ってなに?公式の説明によると
ViewModel
は、ライフサイクル
を意識した方法で UI 関連のデータを保存および管理するためのクラスですViewModel
クラスを使用すると、画面の回転などの設定の変更後にデータを引き継ぐことができます。です!
ViewModel
ってなに?(詳細)公式にはこんな感じのことが書いてありました。
- UI コントローラで利用するデータをバンドルから復元して利用しないで済む
- アクティビティが
onSaveInstanceState()
メソッドを使用してonCreate()
のバンドルからデータを復元しなくてもよくなります。- そもそも
この方法(onSaveInstanceState()利用)
が適しているのは少量のデータの場合だけです。
- ユーザーやビットマップのリストのようにデータの量が多くなる可能性がある場合には適していません。
画面をぐるぐるする度に、バンドルからデータをとって。。。とするのはめんどくさかったので、
それをしないで済むのは良さそうですね!
- メモリリークの心配がない
- UI コントローラでは実行に時間がかかる非同期呼び出しを頻繁に行う必要があります。それらを管理し、破棄された後にシステムが呼び出しのクリーンアップを行ってメモリリークが発生しないようにする必要がありますが、
ViewModel
を利用した場合それをする必要がありません。画面のライフサイクルに合わせて、データの破棄や再取得を考えて設計・実装するのは大変ですね。。。
それをしないで済むのはこちらも良さそうです!
- リソースの無駄がない
- この管理ではメンテナンスを何度も実施する必要があります
- 設定の変更によってオブジェクトが再作成された場合にはすでに行った呼び出しを再度行わなければならないこともあるため、リソースが無駄になります。
メモリリークしない作りにするために、冗長な処理になってしまいがちだったかもしれません。
それをしないで済むのはこちらもありがたいです!
- UIコントローラクラスの肥大化防止とテスト効率の向上
- UI コントローラに対してデータベースやネットワークからのデータ読み込みも行うよう要求すると、クラスが肥大化することになります
- UI コントローラに過度の役割を割り当てると、アプリの作業を他のクラスに任せずに 1 つのクラスですべて処理しようとすることになり、テストも困難になります。
- ビューデータの所有権を UI コントローラのロジックから切り離すことで、複雑さが軽減され、効率性が高まります。
処理が増えてくると、
Activity
やFragment
が巨大化してしまい、本当のところ実際にUIをコントロールしているところが
見えにくくなってしまう、という状況になってしまいがちだと思います。
また、処理とUIコントロールの部分が分割できるよう、設計を工夫してなんとかしていたと思いますが、
公式でサポートするViewModel
で出来るのは頼もしいですね!!!
ViewModel
クラスその名の通り、上記のようなUIコントローラー向けにUIデータを準備するための
ViewModel
ヘルパークラスが用意されています!!!
それを使えば上記の問題が解決できるはずです!使用箇所
Sunflower
リポジトリの中ではどのようにViewModel
が使われているか、実際に見てみましょう
ViewModel
の定義GardenPlantingListViewModel.ktclass GardenPlantingListViewModel internal constructor( gardenPlantingRepository: GardenPlantingRepository ) : ViewModel() { val plantAndGardenPlantings: LiveData<List<PlantAndGardenPlantings>> = gardenPlantingRepository.getPlantedGardens() }
plantAndGardenPlantings
を取得する処理は、アクティビティやフラグメントではなく、GardenPlantingListViewModel
内に割り当てられてられています。ViewModel内で定義しているデータは
LiveData
となっています
- こうすると更新を監視できて、非アクティブになった時に破棄できるなどメリットがあるため、一緒に使う場合が多いと思います。
- ※詳しくはLiveData編も併せて確認してください。
ちなみに、ここでは
plantAndGardenPlantings
の値自体の取得処理は、GardenPlantingRepository
を利用し抽象化されており、さらに外部でインスタンス化されたものをコンストラクターで渡されています。(依存性注入)
- こうすることで、取得先がローカルDBからサーバーに変わったとしても、このクラスとの依存関係がないため、変更は不要であり、変更に強い設計になっています。
- 尚、依存性注入については
Dagger2
を利用して実施する場合が多いですが、Sunflowerリポジトリ
では、Dagger2
の依存性注入については、対応しない!と明言しています笑
ViewModel
利用箇所
UI コントローラー
(ここではFragment
)ではこのようにして、ViewModel
を利用していました。GardenFragment.ktclass GardenFragment : Fragment() { : : private val viewModel: GardenPlantingListViewModel by viewModels { InjectorUtils.provideGardenPlantingListViewModelFactory(requireContext()) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentGardenBinding.inflate(inflater, container, false) : : subscribeUi(adapter, binding) return binding.root } private fun subscribeUi(adapter: GardenPlantingAdapter, binding: FragmentGardenBinding) { viewModel.plantAndGardenPlantings.observe(viewLifecycleOwner) { result -> binding.hasPlantings = !result.isNullOrEmpty() adapter.submitList(result) }
viewModel
を定義している部分ですが、by viewModels {ラムダ式}
となっています。
- これは
Android KTX
の機能のうち、Properties Delegete
というもので、この場合は下記のパターンです。- →ラムダ式の中に自作の
Factory Method
を書くことができ、そこで作られたインスタンスをViewModel
に与えることができますclass MyFragment : Fragment() { val viewmodel: MYViewModel by viewmodels { myFactory } }ここでは、
InjectorUtils#provideGardenPlantingListViewModelFactory()
メソッドを実施し、このViewModel
のインスタンスを取得しています
viewModel.plantAndGardenPlantings
はLiveData
のため、observe()
して変更を監視します。- この処理は
Fragment
が作成された時にコールされる処理、onCreateView()
メソッド内で実施しています。- 値がセットされる度に
onChange()
メソッドがコールされ、ラムダ式内の処理が実施されます。そこで、UIの更新を実施しています。
ViewModel
とFactory
メソッドについて
ViewModel
のFactory
メソッドにはandroidx.lifecycle.ViewModelProvider
を使用します。引数無しの場合
val viewmodel: MYViewModel by viewmodels{ ViewModelProvider.NewInstanceFactory().create(MYViewModel::class.java) }これでOKです。(新たなクラスの定義は不要です。)
引数ありの場合(今回のパターン)
ただし、今回は引数がある場合なので、
引数があるViewModelFactory
クラスを作成しています。こちらは
ViewModelFactory
クラスのFactory
メソッド(Factoryクラス
のコンストラクターメソッド)を呼び出している処理です。
Sunflower
リポジトリでは、kotlin:InjectorUtils
クラスに各ViewModel
のFactory
メソッドをまとめて記載してあります。InjectorUtils.ktobject InjectorUtils { : : fun provideGardenPlantingListViewModelFactory( context: Context ): GardenPlantingListViewModelFactory { val repository = getGardenPlantingRepository(context) return GardenPlantingListViewModelFactory(repository) } : : }続いて、
GardenPlantingListViewModel
のFactory
クラスであるkotlin:GardenPlantingListViewModelFactory
を見てみましょうGardenPlantingListViewModelFactory.ktclass GardenPlantingListViewModelFactory( private val repository: GardenPlantingRepository ) : ViewModelProvider.NewInstanceFactory() { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return GardenPlantingListViewModel(repository) as T } }
GardenPlantingRepository
を引数として渡す形で定義されています。
Sunflower
リポジトリの他のViewModel
クラスも同じ形式となっていたので、別途作成する場合もこの形式を踏襲し、以下の部分の変更のみすればよさそうです
- クラス名(◯◯◯Factory)
Constructor
の引数- オーバーライドする
create
メソッドが返す引数(新たに作成したViewModel
のクラス名)
ViewModel
とテストについて
ViewModel
のライフサイクルは長めに設計されています。そのため、ViewModel
のテストをしやすい設計となっています。
- 画面が無くなった(
Activity#onDestroy()
)「後」に、なにかやる、などもViewmodelScope.onCleared()
とかでできます。- この図はActivityについて書かれてますが、
Fragment
などでも基本的には同じのようですまとめ
- ViewModel は、ライフサイクルを意識した方法で UI 関連のデータを保存および管理するためのクラスで、画面の回転などの設定の変更後にデータを引き継ぐことができる。
- UIコントローラー(
Activity
,Fragment
など)とライフサイクルを共にするため、メモリリークの心配やリソースの無駄がない- UIコントローラーから
ViewModel
を切り出すことにより、UIコントローラーの肥大化を防止し、テストをしやすくすることができるAndroid KTX
のProperties Delegete
でFactory
メソッドでインスンタンスを作ることができる。ViewModel
クラスのコンストラクタが、引数なし
の場合は自前のFactory
クラスを作らないでもよいが、引数あり
の場合はFactory
クラスを作る必要がある。以上です!
参考サイト
- Sunflowerリポジトリ
- ViewModel: 公式ドキュメント
- LiveData: 公式ドキュメント
- DataBinding: 公式ドキュメント
- Android-KTX: @mangano-ito さんの記事
- Android-KTX Fragment.app:公式ドキュメント
- 投稿日:2020-07-27T01:08:54+09:00
Sunflowerリポジトリで学ぶJetPack〜ViewModel編
新型コロナの影響で自宅待機になってしまい、その間勉強するものとして
Sunflower
リポジトリを
勧めてもらいました。JetPackのライブラリのうち、今回は
ViewModel
編です
DataBinding編、LiveData編などでも、
チラチラ出てきてはいましたが、見てみぬふりをしていました笑尚、引用しているソースは明記しているところ以外は、基本的には全て
Sunflower
のリポジトリのものです。環境
- 確認時は
Android Studio
のバージョンは3.6.2
を使用しましたJetPack
はAndroidX
ライブラリを利用するのでCompile SDK
を28
以上にする必要がありますそもそも
ViewModel
ってなに?公式の説明によると
ViewModel
は、ライフサイクル
を意識した方法で UI 関連のデータを保存および管理するためのクラスですViewModel
クラスを使用すると、画面の回転などの設定の変更後にデータを引き継ぐことができます。です!
ViewModel
ってなに?(詳細)公式にはこんな感じのことが書いてありました。
- UI コントローラで利用するデータをバンドルから復元して利用しないで済む
- アクティビティが
onSaveInstanceState()
メソッドを使用してonCreate()
のバンドルからデータを復元しなくてもよくなります。- そもそも
この方法(onSaveInstanceState()利用)
が適しているのは少量のデータの場合だけです。
- ユーザーやビットマップのリストのようにデータの量が多くなる可能性がある場合には適していません。
画面をぐるぐるする度に、バンドルからデータをとって。。。とするのはめんどくさかったので、
それをしないで済むのは良さそうですね!
- メモリリークの心配がない
- UI コントローラでは実行に時間がかかる非同期呼び出しを頻繁に行う必要があります。それらを管理し、破棄された後にシステムが呼び出しのクリーンアップを行ってメモリリークが発生しないようにする必要がありますが、
ViewModel
を利用した場合それをする必要がありません。画面のライフサイクルに合わせて、データの破棄や再取得を考えて設計・実装するのは大変ですね。。。
それをしないで済むのはこちらも良さそうです!
- リソースの無駄がない
- この管理ではメンテナンスを何度も実施する必要があります
- 設定の変更によってオブジェクトが再作成された場合にはすでに行った呼び出しを再度行わなければならないこともあるため、リソースが無駄になります。
メモリリークしない作りにするために、冗長な処理になってしまいがちだったかもしれません。
それをしないで済むのはこちらもありがたいです!
- UIコントローラクラスの肥大化防止とテスト効率の向上
- UI コントローラに対してデータベースやネットワークからのデータ読み込みも行うよう要求すると、クラスが肥大化することになります
- UI コントローラに過度の役割を割り当てると、アプリの作業を他のクラスに任せずに 1 つのクラスですべて処理しようとすることになり、テストも困難になります。
- ビューデータの所有権を UI コントローラのロジックから切り離すことで、複雑さが軽減され、効率性が高まります。
処理が増えてくると、
Activity
やFragment
が巨大化してしまい、本当のところ実際にUIをコントロールしているところが
見えにくくなってしまう、という状況になってしまいがちだと思います。
また、処理とUIコントロールの部分が分割できるよう、設計を工夫してなんとかしていたと思いますが、
公式でサポートするViewModel
で出来るのは頼もしいですね!!!
ViewModel
クラスその名の通り、上記のようなUIコントローラー向けにUIデータを準備するための
ViewModel
ヘルパークラスが用意されています!!!
それを使えば上記の問題が解決できるはずです!使用箇所
Sunflower
リポジトリの中ではどのようにViewModel
が使われているか、実際に見てみましょう
ViewModel
の定義GardenPlantingListViewModel.ktclass GardenPlantingListViewModel internal constructor( gardenPlantingRepository: GardenPlantingRepository ) : ViewModel() { val plantAndGardenPlantings: LiveData<List<PlantAndGardenPlantings>> = gardenPlantingRepository.getPlantedGardens() }
plantAndGardenPlantings
を取得する処理は、アクティビティやフラグメントではなく、GardenPlantingListViewModel
内に割り当てられてられています。ViewModel内で定義しているデータは
LiveData
となっています
- こうすると更新を監視できて、非アクティブになった時に破棄できるなどメリットがあるため、一緒に使う場合が多いと思います。
- ※詳しくはLiveData編も併せて確認してください。
ちなみに、ここでは
plantAndGardenPlantings
の値自体の取得処理は、GardenPlantingRepository
を利用し抽象化されており、さらに外部でインスタンス化されたものをコンストラクターで渡されています。(依存性注入)
- こうすることで、取得先がローカルDBからサーバーに変わったとしても、このクラスとの依存関係がないため、変更は不要であり、変更に強い設計になっています。
- 尚、依存性注入については
Dagger2
を利用して実施する場合が多いですが、Sunflowerリポジトリ
では、Dagger2
の依存性注入については、対応しない!と明言しています笑
ViewModel
利用箇所
UI コントローラー
(ここではFragment
)ではこのようにして、ViewModel
を利用していました。GardenFragment.ktclass GardenFragment : Fragment() { : : private val viewModel: GardenPlantingListViewModel by viewModels { InjectorUtils.provideGardenPlantingListViewModelFactory(requireContext()) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentGardenBinding.inflate(inflater, container, false) : : subscribeUi(adapter, binding) return binding.root } private fun subscribeUi(adapter: GardenPlantingAdapter, binding: FragmentGardenBinding) { viewModel.plantAndGardenPlantings.observe(viewLifecycleOwner) { result -> binding.hasPlantings = !result.isNullOrEmpty() adapter.submitList(result) }
viewModel
を定義している部分ですが、by viewModels {ラムダ式}
となっています。
- これは
Android KTX
の機能のうち、Properties Delegete
というもので、この場合は下記のパターンです。- →ラムダ式の中に自作の
Factory Method
を書くことができ、そこで作られたインスタンスをViewModel
に与えることができますclass MyFragment : Fragment() { val viewmodel: MYViewModel by viewmodels { myFactory } }ここでは、
InjectorUtils#provideGardenPlantingListViewModelFactory()
メソッドを実施し、このViewModel
のインスタンスを取得しています
viewModel.plantAndGardenPlantings
はLiveData
のため、observe()
して変更を監視します。- この処理は
Fragment
が作成された時にコールされる処理、onCreateView()
メソッド内で実施しています。- 値がセットされる度に
onChange()
メソッドがコールされ、ラムダ式内の処理が実施されます。そこで、UIの更新を実施しています。
ViewModel
とFactory
メソッドについて
ViewModel
のFactory
メソッドにはandroidx.lifecycle.ViewModelProvider
を使用します。引数無しの場合
val viewmodel: MYViewModel by viewmodels{ ViewModelProvider.NewInstanceFactory().create(MYViewModel::class.java) }これでOKです。(新たなクラスの定義は不要です。)
引数ありの場合(今回のパターン)
ただし、今回は引数がある場合なので、
引数があるViewModelFactory
クラスを作成しています。こちらは
ViewModelFactory
クラスのFactory
メソッド(Factoryクラス
のコンストラクターメソッド)を呼び出している処理です。
Sunflower
リポジトリでは、kotlin:InjectorUtils
クラスに各ViewModel
のFactory
メソッドをまとめて記載してあります。InjectorUtils.ktobject InjectorUtils { : : fun provideGardenPlantingListViewModelFactory( context: Context ): GardenPlantingListViewModelFactory { val repository = getGardenPlantingRepository(context) return GardenPlantingListViewModelFactory(repository) } : : }続いて、
GardenPlantingListViewModel
のFactory
クラスであるkotlin:GardenPlantingListViewModelFactory
を見てみましょうGardenPlantingListViewModelFactory.ktclass GardenPlantingListViewModelFactory( private val repository: GardenPlantingRepository ) : ViewModelProvider.NewInstanceFactory() { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return GardenPlantingListViewModel(repository) as T } }
GardenPlantingRepository
を引数として渡す形で定義されています。
Sunflower
リポジトリの他のViewModel
クラスも同じ形式となっていたので、別途作成する場合もこの形式を踏襲し、以下の部分の変更のみすればよさそうです
- クラス名(◯◯◯Factory)
Constructor
の引数- オーバーライドする
create
メソッドが返す引数(新たに作成したViewModel
のクラス名)
ViewModel
とテストについて
ViewModel
のライフサイクルは長めに設計されています。そのため、ViewModel
のテストをしやすい設計となっています。
- 画面が無くなった(
Activity#onDestroy()
)「後」に、なにかやる、などもViewmodelScope.onCleared()
とかでできます。- この図はActivityについて書かれてますが、
Fragment
などでも基本的には同じのようですまとめ
- ViewModel は、ライフサイクルを意識した方法で UI 関連のデータを保存および管理するためのクラスで、画面の回転などの設定の変更後にデータを引き継ぐことができる。
- UIコントローラー(
Activity
,Fragment
など)とライフサイクルを共にするため、メモリリークの心配やリソースの無駄がない- UIコントローラーから
ViewModel
を切り出すことにより、UIコントローラーの肥大化を防止し、テストをしやすくすることができるAndroid KTX
のProperties Delegete
でFactory
メソッドでインスンタンスを作ることができる。ViewModel
クラスのコンストラクタが、引数なし
の場合は自前のFactory
クラスを作らないでもよいが、引数あり
の場合はFactory
クラスを作る必要がある。以上です!
参考サイト
- Sunflowerリポジトリ
- ViewModel: 公式ドキュメント
- LiveData: 公式ドキュメント
- DataBinding: 公式ドキュメント
- Android-KTX: @mangano-ito さんの記事
- Android-KTX Fragment.app:公式ドキュメント
- 投稿日:2020-07-27T01:03:05+09:00
【LiveData】Sunflowerリポジトリで学ぶJetPack〜LiveData編
新型コロナの影響で自宅待機になってしまい、その間勉強するものとして
Sunflower
リポジトリを勧めてもらいました。JetPackのライブラリのうち、今回は
LiveData
編です
DataBinding編からの続きです。尚、引用しているソースは明記しているところ以外は、基本的には全て
Sunflower
のリポジトリのものです。環境
- 確認時は
Android Studio
のバージョンは3.6.2
を使用しましたJetPack
はAndroidX
ライブラリを利用するのでCompile SDK
を28
以上にする必要がありますそもそもLiveDataってなに?
公式の説明によると
LiveData
は監視可能なデータホルダー クラス- 通常の監視と異なるのはライフサイクルに応じた監視が可能
- ライフサイクルがアクティブなオブザーバーのみを更新する
- ライフサイクルの状態が
STARTED
またはRESUMED
の場合 = アクティブLiveData
は更新に関する情報をアクティブなオブザーバーにのみ通知非アクティブ
なオブザーバーには、変更に関する通知は行われない
オブザーバー
はLifecycleOwner
インターフェースを実装するオブジェクトとペアで登録できる
- ペアリング → 対応する
Lifecycle
オブジェクトの状態がDESTROYED
→オブザーバー
を削除可能アクティビティ
とフラグメント
で利用すると、LiveData
オブジェクトを安全に監視可能
- ライフサイクルが破棄されるとすぐに登録が解除される
だそうで、ざっくりいうと
LifecycleOwner
が破棄されれば登録が解除される通知機能を持つクラス
という感じでしょうか??(^^;メリット
公式にはこんなに書いてありました!
- UI をデータの状態と一致させることができる
- メモリリークが発生しない
- 停止されたアクティビティに起因するクラッシュが発生しない
- 手動によるライフサイクル処理が行われない
- データが常に最新
- 適切な設定の変更
- リソースの共有
たくさんあります!!
購読処理の実施(LiveDataとの連携)
DataBinding編
で見ていた処理を確認してみましょう使用箇所
GardenFragment.ktoverride fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentGardenBinding.inflate(inflater, container, false) : : subscribeUi(adapter, binding) return binding.root } private fun subscribeUi(adapter: GardenPlantingAdapter, binding: FragmentGardenBinding) { viewModel.plantAndGardenPlantings.observe(viewLifecycleOwner) { result -> binding.hasPlantings = !result.isNullOrEmpty() adapter.submitList(result) }
- 名前の通り
subscribeUi()
メソッドで購読処理を実施しており、plantAndGardenPlantings
をobserve
しています。 こうすることで、plantAndGardenPlantings
の値が変更された時にresultが更新されて、そこから算出されたhasPlantings
でRecyclerView
を更新します。購読を実施するタイミングについて
この購読処理は
GardenFragment#onCreateView
で実施されています。
その理由は下記によるものです。
- 殆どの場合画面が
作られた時
にのみ実施されるのが適しているためです。
- 例えば、
Activity
のonResume()
などの場合は、繰り返し呼ばれる場合があるので不適切- アクティビティやフラグメントが
アクティブ
になり次第、そのデータを表示できるため(その前に準備しておく必要がある!)LiveDataクラスを見てみる
次に
plantAndGardenPlantings
がどうなっているかをみてみましょう!LiveDataクラスの定義
GardenPlantingListViewModel.ktclass GardenPlantingListViewModel internal constructor( gardenPlantingRepository: GardenPlantingRepository ) : ViewModel() { val plantAndGardenPlantings: LiveData<List<PlantAndGardenPlantings>> = gardenPlantingRepository.getPlantedGardens() }なにやらカッコが多くて見にくいですが
plantAndGardenPlantings
の型はLiveData<List<PlantAndGardenPlantings>>
となっています
これはLiveDataクラス
で、PlantAndGardenPlantingsクラスのリストの型
をしているよ! という定義です。GardenPlantingRepository.ktclass GardenPlantingRepository private constructor( private val gardenPlantingDao: GardenPlantingDao ) { : : fun getPlantedGardens() = gardenPlantingDao.getPlantedGardens() : : }GardenPlantingDao.kt@Dao interface GardenPlantingDao { : : @Transaction @Query("SELECT * FROM plants WHERE id IN (SELECT DISTINCT(plant_id) FROM garden_plantings)") fun getPlantedGardens(): LiveData<List<PlantAndGardenPlantings>> : : }
GardenPlantingRepository#getPlantedGardens()
→GardenPlantingDao#getPlantedGardens()
と順に呼び出していき、
最終的にはDBからデータを取得していますね。
Observe
の定義とonChanged()
LiveData.kt@MainThread inline fun <T> LiveData<T>.observe( owner: LifecycleOwner, crossinline onChanged: (T) -> Unit ): Observer<T> { val wrappedObserver = Observer<T> { t -> onChanged.invoke(t) } observe(owner, wrappedObserver) return wrappedObserver }そして、LiveDataの定義をみてみます。。。
とても複雑ですが、ポイントは
observe()
はメインスレッドで実施する- 第二引数のラムダ式
onChange()
も、メインスレッドで実施する改めて最初の
observe
メソッドを実施している部分をみてみると・・・GardenFragment.ktprivate fun subscribeUi(adapter: GardenPlantingAdapter, binding: FragmentGardenBinding) { viewModel.plantAndGardenPlantings.observe(viewLifecycleOwner) { result -> binding.hasPlantings = !result.isNullOrEmpty() adapter.submitList(result) }
observe()
メソッドのラムダ式で書いてある第二引数のonChange()
はメインスレッド
で実行する。- resultに値がセットされた場合、通知がくる
- result には
list<PlantAndGardenPlantings>
が返ってくる注意点
- 実際に動作確認を実施したところ、
値が変更した時
のみに動作するのかと思っていたのですが、
そうではなくLiveDataに値がセットされる度
に、onChanged()
が実行されていました。
(onChanged()
じゃない気がしますが。。。)まとめ
LiveData
とは、LifecycleOwner
が破棄されれば登録が解除される通知機能を持つクラスです *LiveData<データ型>
で定義します- LiveDataの購読処理はonCreate() (Fragmentの場合はonCreateView)で実施するのが殆どの場合適している。
- 購読された
LiveData
のデータが更新されると、その度に通知が来る。
- ただし、更新とは言っても
変更する度
ではなく、値がセットされる度
に通知がくる。以上です!
参考サイト
- Sunflowerリポジトリ
- LiveData: 公式ドキュメント
- DataBinding: 公式ドキュメント
- 投稿日:2020-07-27T01:03:05+09:00
Sunflowerリポジトリで学ぶJetPack〜LiveData編
新型コロナの影響で自宅待機になってしまい、その間勉強するものとして
Sunflower
リポジトリを勧めてもらいました。JetPackのライブラリのうち、今回は
LiveData
編です
DataBinding編からの続きです。尚、引用しているソースは明記しているところ以外は、基本的には全て
Sunflower
のリポジトリのものです。環境
- 確認時は
Android Studio
のバージョンは3.6.2
を使用しましたJetPack
はAndroidX
ライブラリを利用するのでCompile SDK
を28
以上にする必要がありますそもそもLiveDataってなに?
公式の説明によると
LiveData
は監視可能なデータホルダー クラス- 通常の監視と異なるのはライフサイクルに応じた監視が可能
- ライフサイクルがアクティブなオブザーバーのみを更新する
- ライフサイクルの状態が
STARTED
またはRESUMED
の場合 = アクティブLiveData
は更新に関する情報をアクティブなオブザーバーにのみ通知非アクティブ
なオブザーバーには、変更に関する通知は行われない
オブザーバー
はLifecycleOwner
インターフェースを実装するオブジェクトとペアで登録できる
- ペアリング → 対応する
Lifecycle
オブジェクトの状態がDESTROYED
→オブザーバー
を削除可能アクティビティ
とフラグメント
で利用すると、LiveData
オブジェクトを安全に監視可能
- ライフサイクルが破棄されるとすぐに登録が解除される
だそうで、ざっくりいうと
LifecycleOwner
が破棄されれば登録が解除される通知機能を持つクラス
という感じでしょうか??(^^;メリット
公式にはこんなに書いてありました!
- UI をデータの状態と一致させることができる
- メモリリークが発生しない
- 停止されたアクティビティに起因するクラッシュが発生しない
- 手動によるライフサイクル処理が行われない
- データが常に最新
- 適切な設定の変更
- リソースの共有
たくさんあります!!
購読処理の実施(LiveDataとの連携)
DataBinding編
で見ていた処理を確認してみましょう使用箇所
GardenFragment.ktoverride fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentGardenBinding.inflate(inflater, container, false) : : subscribeUi(adapter, binding) return binding.root } private fun subscribeUi(adapter: GardenPlantingAdapter, binding: FragmentGardenBinding) { viewModel.plantAndGardenPlantings.observe(viewLifecycleOwner) { result -> binding.hasPlantings = !result.isNullOrEmpty() adapter.submitList(result) }
- 名前の通り
subscribeUi()
メソッドで購読処理を実施しており、plantAndGardenPlantings
をobserve
しています。 こうすることで、plantAndGardenPlantings
の値が変更された時にresultが更新されて、そこから算出されたhasPlantings
でRecyclerView
を更新します。購読を実施するタイミングについて
この購読処理は
GardenFragment#onCreateView
で実施されています。
その理由は下記によるものです。
- 殆どの場合画面が
作られた時
にのみ実施されるのが適しているためです。
- 例えば、
Activity
のonResume()
などの場合は、繰り返し呼ばれる場合があるので不適切- アクティビティやフラグメントが
アクティブ
になり次第、そのデータを表示できるため(その前に準備しておく必要がある!)LiveDataクラスを見てみる
次に
plantAndGardenPlantings
がどうなっているかをみてみましょう!LiveDataクラスの定義
GardenPlantingListViewModel.ktclass GardenPlantingListViewModel internal constructor( gardenPlantingRepository: GardenPlantingRepository ) : ViewModel() { val plantAndGardenPlantings: LiveData<List<PlantAndGardenPlantings>> = gardenPlantingRepository.getPlantedGardens() }なにやらカッコが多くて見にくいですが
plantAndGardenPlantings
の型はLiveData<List<PlantAndGardenPlantings>>
となっています
これはLiveDataクラス
で、PlantAndGardenPlantingsクラスのリストの型
をしているよ! という定義です。GardenPlantingRepository.ktclass GardenPlantingRepository private constructor( private val gardenPlantingDao: GardenPlantingDao ) { : : fun getPlantedGardens() = gardenPlantingDao.getPlantedGardens() : : }GardenPlantingDao.kt@Dao interface GardenPlantingDao { : : @Transaction @Query("SELECT * FROM plants WHERE id IN (SELECT DISTINCT(plant_id) FROM garden_plantings)") fun getPlantedGardens(): LiveData<List<PlantAndGardenPlantings>> : : }
GardenPlantingRepository#getPlantedGardens()
→GardenPlantingDao#getPlantedGardens()
と順に呼び出していき、
最終的にはDBからデータを取得していますね。
Observe
の定義とonChanged()
LiveData.kt@MainThread inline fun <T> LiveData<T>.observe( owner: LifecycleOwner, crossinline onChanged: (T) -> Unit ): Observer<T> { val wrappedObserver = Observer<T> { t -> onChanged.invoke(t) } observe(owner, wrappedObserver) return wrappedObserver }そして、LiveDataの定義をみてみます。。。
とても複雑ですが、ポイントは
observe()
はメインスレッドで実施する- 第二引数のラムダ式
onChange()
も、メインスレッドで実施する改めて最初の
observe
メソッドを実施している部分をみてみると・・・GardenFragment.ktprivate fun subscribeUi(adapter: GardenPlantingAdapter, binding: FragmentGardenBinding) { viewModel.plantAndGardenPlantings.observe(viewLifecycleOwner) { result -> binding.hasPlantings = !result.isNullOrEmpty() adapter.submitList(result) }
observe()
メソッドのラムダ式で書いてある第二引数のonChange()
はメインスレッド
で実行する。- resultに値がセットされた場合、通知がくる
- result には
list<PlantAndGardenPlantings>
が返ってくる注意点
- 実際に動作確認を実施したところ、
値が変更した時
のみに動作するのかと思っていたのですが、
そうではなくLiveDataに値がセットされる度
に、onChanged()
が実行されていました。
(onChanged()
じゃない気がしますが。。。)まとめ
LiveData
とは、LifecycleOwner
が破棄されれば登録が解除される通知機能を持つクラスです *LiveData<データ型>
で定義します- LiveDataの購読処理はonCreate() (Fragmentの場合はonCreateView)で実施するのが殆どの場合適している。
- 購読された
LiveData
のデータが更新されると、その度に通知が来る。
- ただし、更新とは言っても
変更する度
ではなく、値がセットされる度
に通知がくる。以上です!
参考サイト
- Sunflowerリポジトリ
- LiveData: 公式ドキュメント
- DataBinding: 公式ドキュメント
- 投稿日:2020-07-27T01:01:55+09:00
【DataBinding】Sunflowerリポジトリで学ぶJetPack〜DataBinding編
新型コロナの影響で自宅待機になってしまい、その間勉強するものとして
Sunflower
リポジトリを
勧めてもらいました。JetPackのライブラリのうち、今回は
DataBinding
編です尚、引用しているソースは明記しているところ以外は、全て
Sunflower
のリポジトリのものです。環境
- 確認時は
Android Studio
のバージョンは3.6.2
を使用Data binding
はサポートライブラリなので、Android 4.0
(API レベル 14)以降で利用可能Android Plugin for Gradle
の1.5.0
以降でサポートされているが、なるべく最新を使用したほうがいい(らしい)DataBinding を利用するのに必要なこと
- ビルド環境設定
- レイアウトファイルを変更
- 購読処理の実施(LiveDataとの連携)
準備
Build.gradle
ファイルでData Binding
の機能を有効にします
Sunflower
ではapp/build.gradle
に記載してありますねー!(^^)app/build.gradleandroid { compileSdkVersion rootProject.compileSdkVersion dataBinding { enabled = true }レイアウトファイル
ここでは
Data Binding
を利用しているfragment_garden.xml
を見てみましょうfragment_garden.xml<layout 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"> <data> <variable name="hasPlantings" type="boolean" /> </data> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/garden_list" android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" android:layout_marginStart="@dimen/card_side_margin" android:layout_marginEnd="@dimen/card_side_margin" android:layout_marginTop="@dimen/margin_normal" app:isGone="@{!hasPlantings}" app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager" app:spanCount="@integer/grid_columns" tools:listitem="@layout/list_item_garden_planting"/> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" app:isGone="@{hasPlantings}"> : : </LinearLayout> </FrameLayout> </layout>ポイント
- レイアウトファイルが
<Layout>タグ
で開始されている。(<LinearLayout>
とかじゃない)- その中に
<data>タグ
がある。- さらにその中に
<variable>タグ
でバインディング変数を定義している- バインディング変数は
name
で名前を、type
で型を定義している- バインディング変数の利用箇所は
@{変数名}
と記載する ここでは、"@{hasPlantings}"
と記載している *ちなみに"@{viewModel.imageUrl}"
のようなプロパティ参照もできる(list_item_garden_planting.xml
参照)ここでは
hasPlantings
の値によってRecyclerView
となにもない時に表示するLinearLayout
を出し分けてますねデータのバインド
ではレイアウトファイルで記載した変数をどう関連付けるのか、
Kotlin
ファイルを確認してみましょうGardenFragment.ktclass GardenFragment : Fragment() { private lateinit var binding: FragmentGardenBinding : : override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentGardenBinding.inflate(inflater, container, false) val adapter = GardenPlantingAdapter() binding.gardenList.adapter = adapter binding.addPlant.setOnClickListener { navigateToPlantListPage() } subscribeUi(adapter, binding) return binding.root } : : }ポイント
1. データバインディングを実施すると、バインディングクラスがレイアウトファイルごとに作られる。 作られるクラスの名称は下記のルールで決定されます
バインディング クラスはレイアウト ファイルごとに生成されます。 デフォルトのクラス名は、レイアウト ファイルの名前がパスカルケースに変換され、Binding サフィックスが付加されたものになります。→ここの場合は
fragment_garden.xml
のレイアウトファイルからFragmentGardenBinding
クラスが作成されます2. データバインディングの変数を利用するために、
1.
で自動生成されたバインディングクラスでinflate()
します。
3.Fragment
、ListView
、RecyclerView
などいずれかのアダプター内でデータバインディングの変数を利用する場合は、DataBindingUtil#inflate()
メソッドも利用可能です
DataBindingUtil#inflate()
利用箇所
GardenPlantingAdapter
では、DataBindingUtil#inflate()
メソッドを利用してバインディングしていました。GardenPlantingAdapter.ktoverride fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( DataBindingUtil.inflate( LayoutInflater.from(parent.context), R.layout.list_item_garden_planting, parent, false ) ) }購読処理の実施(LiveDataとの連携)
それでは、購読する処理を見てみましょう
GardenFragment.ktoverride fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentGardenBinding.inflate(inflater, container, false) : : subscribeUi(adapter, binding) return binding.root } private fun subscribeUi(adapter: GardenPlantingAdapter, binding: FragmentGardenBinding) { viewModel.plantAndGardenPlantings.observe(viewLifecycleOwner) { result -> binding.hasPlantings = !result.isNullOrEmpty() adapter.submitList(result) }名前の通り
subscribeUi()
メソッドで購読処理を実施しています。
なるほどー。 ここでは、実際にレイアウトにバインディングしたhasPlantings
を直接購読しているのではなく、その情報を算出するためのplantAndGardenPlantings
をobserve
しているんですね!
こうすることで、plantAndGardenPlantings
の値が変更された時にresultが更新されて、そこから算出された
hasPlantings
でRecyclerView
を更新すると。ここから先はLiveData編に記載します!
参考サイト
- Sunflowerリポジトリ
- DataBinding: 公式ドキュメント
- LiveData: :公式ドキュメント
- 投稿日:2020-07-27T01:01:55+09:00
Sunflowerリポジトリで学ぶJetPack〜DataBinding編
新型コロナの影響で自宅待機になってしまい、その間勉強するものとして
Sunflower
リポジトリを
勧めてもらいました。JetPackのライブラリのうち、今回は
DataBinding
編です尚、引用しているソースは明記しているところ以外は、全て
Sunflower
のリポジトリのものです。環境
- 確認時は
Android Studio
のバージョンは3.6.2
を使用Data binding
はサポートライブラリなので、Android 4.0
(API レベル 14)以降で利用可能Android Plugin for Gradle
の1.5.0
以降でサポートされているが、なるべく最新を使用したほうがいい(らしい)DataBinding を利用するのに必要なこと
- ビルド環境設定
- レイアウトファイルを変更
- 購読処理の実施(LiveDataとの連携)
準備
Build.gradle
ファイルでData Binding
の機能を有効にします
Sunflower
ではapp/build.gradle
に記載してありますねー!(^^)app/build.gradleandroid { compileSdkVersion rootProject.compileSdkVersion dataBinding { enabled = true }レイアウトファイル
ここでは
Data Binding
を利用しているfragment_garden.xml
を見てみましょうfragment_garden.xml<layout 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"> <data> <variable name="hasPlantings" type="boolean" /> </data> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/garden_list" android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" android:layout_marginStart="@dimen/card_side_margin" android:layout_marginEnd="@dimen/card_side_margin" android:layout_marginTop="@dimen/margin_normal" app:isGone="@{!hasPlantings}" app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager" app:spanCount="@integer/grid_columns" tools:listitem="@layout/list_item_garden_planting"/> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" app:isGone="@{hasPlantings}"> : : </LinearLayout> </FrameLayout> </layout>ポイント
- レイアウトファイルが
<Layout>タグ
で開始されている。(<LinearLayout>
とかじゃない)- その中に
<data>タグ
がある。- さらにその中に
<variable>タグ
でバインディング変数を定義している- バインディング変数は
name
で名前を、type
で型を定義している- バインディング変数の利用箇所は
@{変数名}
と記載する ここでは、"@{hasPlantings}"
と記載している *ちなみに"@{viewModel.imageUrl}"
のようなプロパティ参照もできる(list_item_garden_planting.xml
参照)ここでは
hasPlantings
の値によってRecyclerView
となにもない時に表示するLinearLayout
を出し分けてますねデータのバインド
ではレイアウトファイルで記載した変数をどう関連付けるのか、
Kotlin
ファイルを確認してみましょうGardenFragment.ktclass GardenFragment : Fragment() { private lateinit var binding: FragmentGardenBinding : : override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentGardenBinding.inflate(inflater, container, false) val adapter = GardenPlantingAdapter() binding.gardenList.adapter = adapter binding.addPlant.setOnClickListener { navigateToPlantListPage() } subscribeUi(adapter, binding) return binding.root } : : }ポイント
1. データバインディングを実施すると、バインディングクラスがレイアウトファイルごとに作られる。 作られるクラスの名称は下記のルールで決定されます
バインディング クラスはレイアウト ファイルごとに生成されます。 デフォルトのクラス名は、レイアウト ファイルの名前がパスカルケースに変換され、Binding サフィックスが付加されたものになります。→ここの場合は
fragment_garden.xml
のレイアウトファイルからFragmentGardenBinding
クラスが作成されます2. データバインディングの変数を利用するために、
1.
で自動生成されたバインディングクラスでinflate()
します。
3.Fragment
、ListView
、RecyclerView
などいずれかのアダプター内でデータバインディングの変数を利用する場合は、DataBindingUtil#inflate()
メソッドも利用可能です
DataBindingUtil#inflate()
利用箇所
GardenPlantingAdapter
では、DataBindingUtil#inflate()
メソッドを利用してバインディングしていました。GardenPlantingAdapter.ktoverride fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( DataBindingUtil.inflate( LayoutInflater.from(parent.context), R.layout.list_item_garden_planting, parent, false ) ) }購読処理の実施(LiveDataとの連携)
それでは、購読する処理を見てみましょう
GardenFragment.ktoverride fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentGardenBinding.inflate(inflater, container, false) : : subscribeUi(adapter, binding) return binding.root } private fun subscribeUi(adapter: GardenPlantingAdapter, binding: FragmentGardenBinding) { viewModel.plantAndGardenPlantings.observe(viewLifecycleOwner) { result -> binding.hasPlantings = !result.isNullOrEmpty() adapter.submitList(result) }名前の通り
subscribeUi()
メソッドで購読処理を実施しています。
なるほどー。 ここでは、実際にレイアウトにバインディングしたhasPlantings
を直接購読しているのではなく、その情報を算出するためのplantAndGardenPlantings
をobserve
しているんですね!
こうすることで、plantAndGardenPlantings
の値が変更された時にresultが更新されて、そこから算出された
hasPlantings
でRecyclerView
を更新すると。ここから先はLiveData編に記載します!
参考サイト
- Sunflowerリポジトリ
- DataBinding: 公式ドキュメント
- LiveData: :公式ドキュメント
- 投稿日:2020-07-27T01:00:50+09:00
【Navigation】Sunflowerリポジトリで学ぶJetPack〜Navigation編
新型コロナの影響で自宅待機になってしまい、その間勉強するものとして
Sunflower
リポジトリを
勧めてもらいました。JetPackのライブラリのうち、今回は
Navigation
編です尚、引用しているソースは明記しているところ以外は、全て
Sunflower
のリポジトリのものです。環境
- Android Studioは
3.3
以上が必要- 確認時は
3.6.2
を使用Navgation を利用するのに必要なこと
大きく分けて3つ必要です
1. 準備(依存関係の記載)
1. レイアウトファイルにナビゲーションを使うことを記載する
3. ナビゲーション グラフのファイルを作る準備
依存関係の記載
- 公式によると、
build.gradle
に以下の4つの依存関係を記載する必要があります。
- Java language implementation
- Kotlin
- Dynamic Feature Module Support
- Testing Navigation
公式の
build.gradle
のサンプルbuild.gradledependencies { def nav_version = "2.3.0-alpha01" // Java language implementation implementation "androidx.navigation:navigation-fragment:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" // Kotlin implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" // Dynamic Feature Module Support implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version" // Testing Navigation androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" }gradleファイル
それでは、Sunflowerの
build.gradle
を見てみましょう。
Navigation
と関係ないところは省略build.gradle(sunflower)
build.gradlebuildscript { // Define versions in a single place ext { : navigationVersion = '2.2.0' : } dependencies { : classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion" } }公式のチュートリアルにはないものが含まれています!
こちらはsafe args
の機能を利用する際に記載の必要があるものです。
Navgation利用時に安全にパラメータを渡す機能です。
こちらはroot
のbuild.gradle
に記載する必要があるとのこと。
https://developer.android.com/guide/navigation/navigation-pass-data#Safe-argsbuild.gradle(:app)
app/build.gradledependencies { : implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion" implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion" :
$rootProject.navigationVersion
はbuild.gradle(sunflower)
で定義しているnavigationVersion = '2.2.0'
ですねここからわかること
- Sunflowerリポジトリでは、下記のものは利用していないので、依存関係の記載がない。
Java
Dynamic Feature Module Support
、Navigationのテスト
kotlin
は使っているので、依存関係の記載がある。layout
次にレイアウトファイルを見てみましょう。
最初に表示される画面から詳細画面に遷移するところで使っているはずなので、
最初に表示する画面を見てみます。
起動時のActivity
はGardenActivity
でそこでsetContentView
しているxmlファイルはactivity_garden.xml
です
レイアウトはこんな感じactivity_garden.xml
activity_garden.xml<fragment android:id="@+id/nav_host" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph="@navigation/nav_garden"/> </FrameLayout>お、やっぱりここに
Navgation
関連の設定をしています。必要なこと
android:name
にはandroidx.navigation.fragment.NavHostFragment
を設定する
- この
fragment
はNavigation
を使うよ!app:navGraph
には 利用するNavigationのXMLファイルを記載します。ここでNavGraph
と関連付けます。
- ナビゲーショングラフはこのファイルを使うよ!
その他
app:defaultNavHost="true"
を設定すると、戻るボタンで戻るようになります。
- 1つの画面に複数の
Fragment
があり、両方でNavgation
を利用している時などに、戻るボタンを効かなくする、 とかの時にはfalse
にする模様ナビゲーショングラフ
レイアウトで指定されていた
nav_garden
のファイルを見てみましょうNavigation Editor
こんな感じで、画面が複数と、その間を結ぶ矢印が表示されています。
XMLファイル
nav_garden.xml<navigation 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" app:startDestination="@id/view_pager_fragment"> <fragment android:id="@+id/view_pager_fragment" android:name="com.google.samples.apps.sunflower.HomeViewPagerFragment" tools:layout="@layout/fragment_view_pager"> <action android:id="@+id/action_view_pager_fragment_to_plant_detail_fragment" app:destination="@id/plant_detail_fragment" app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" /> </fragment> <fragment android:id="@+id/plant_detail_fragment" android:name="com.google.samples.apps.sunflower.PlantDetailFragment" android:label="@string/plant_details_title" tools:layout="@layout/fragment_plant_detail"> <argument android:name="plantId" app:argType="string" /> </fragment> </navigation>ポイント
- 最初に表示する画面が
<navigation>
タグのapp:startDestination
に記載されている。- 画面遷移に関連する画面が
<fragment>タグ
で記載されている。- 画面遷移の処理詳細が、遷移元の画面に
<action>
タグで記載されている。- 遷移先画面に渡されるパラメータが遷移先画面に
<argment>
タグで記載されている。※この辺りは、
Navigation Editor
で画面をクリックしてグイーンとやって・・・みたいな感じで設定できる部分もあるようです!
(iOS
でいうStoryboard
みたいな感じでしょうか?)
https://developer.android.com/guide/navigation/navigation-getting-started?hl=ja#Designate-startactionタグ設定値の記載方法について
特有の設定値については下記となっています。
※アニメーション関連の設定値が分かりづらかったので
こちらを参照しました
https://stackoverflow.com/questions/56285197/what-is-the-difference-between-enteranim-popenteranim-exitanim-popexitanim
https://developer.android.com/guide/navigation/navigation-animate-transitionsつまるところ、アニメーションはPUSH時とPOP時で分けて記述が可能ということのようです。
名称 内容 設定されている値 android:id
遷移アクションの名称(これをソース内に記載する) @id/action_view_pager_fragment_to_plant_detail_fragment
app:destination
遷移先の画面(宛先) @id/plant_detail_fragment
app:enterAnim
宛先に遷移する際のアニメーション @anim/slide_in_right
app:exitAnim
宛先からに出て行く時のアニメーション @anim/slide_out_left
app:popEnterAnim
宛先にPOPで遷移する時のアニメーション @anim/slide_in_left
app:popExitAnim
宛先からPOPで出て行く時のアニメーション @anim/slide_out_right
使用箇所について
- RecyclerViewの項目をタップした時に、
navigatioToPlant
で画面遷移を実施している。navigatioToPlant
のactionViewPagerFragmentToPlantDetailFragment(plantId)
で遷移先を決定するNavDirections
を取得している。NavDirections
の取得にはNavDirections
インターフェースを継承したクラスActionViewPagerFragmentToPlantDetailFragment
のコンストラクタActionViewPagerFragmentToPlantDetailFragment(plantId)
使用している。ActionViewPagerFragmentToPlantDetailFragment
クラスでは、NavDirections
インターフェースのgetActionId()
メソッドをオーバーライドしている。getActionId()
メソッドは action_view_pager_fragment_to_plant_detail_fragmentから取得している。
⬆︎先ほどナビゲーショングラフのXMLファイルで定義していたaction
のIDですね!(やっと出てきた!(^^;;)GardenPlantingAdapter.ktclass ViewHolder( private val binding: ListItemGardenPlantingBinding ) : RecyclerView.ViewHolder(binding.root) { init { binding.setClickListener { view -> binding.viewModel?.plantId?.let { plantId -> navigateToPlant(plantId, view) } } } private fun navigateToPlant(plantId: String, view: View) { val direction = HomeViewPagerFragmentDirections .actionViewPagerFragmentToPlantDetailFragment(plantId) view.findNavController().navigate(direction) }HomeViewPagerFragmentDirections.ktclass HomeViewPagerFragmentDirections private constructor() { private data class ActionViewPagerFragmentToPlantDetailFragment( val plantId: String ) : NavDirections { override fun getActionId(): Int = R.id.action_view_pager_fragment_to_plant_detail_fragment override fun getArguments(): Bundle { val result = Bundle() result.putString("plantId", this.plantId) return result } } companion object { fun actionViewPagerFragmentToPlantDetailFragment(plantId: String): NavDirections = ActionViewPagerFragmentToPlantDetailFragment(plantId) } }引数の設定
遷移先の画面のタグ内の
<argument>
タグ内に、
遷移先画面で受け取る引数を設定可能
名称 内容 設定されている値 android:name
引数の名前 plantId
app:argType
引数の型 string
遷移先での引数の取得
遷移先のコードは下記です。
PlantDetailFragment.ktprivate val args: PlantDetailFragmentArgs by navArgs() private val plantDetailViewModel: PlantDetailViewModel by viewModels { InjectorUtils.providePlantDetailViewModelFactory(requireActivity(), args.plantId) }公式のドキュメント
https://developer.android.com/guide/navigation/navigation-pass-data?hl=ja#Safe-args
によると、受信側デスティネーションのコード内で、getArguments() メソッドを使用して、バンドルを取得し、そのコンテンツを使用します。-ktx 依存関係を使用している場合、Kotlin ユーザーは、by navArgs() プロパティ デリゲートを使用して引数にアクセスすることもできます。なるほど-
ここではsafe args
という機能を利用して、遷移元から渡されたデータを遷移先でargs
という変数で取得し、
それをViewModel
に入れてますねー。
で、画面ではそのViewModel
を介してデータを使用するようになっています。まとめ
Sunflowerのリポジトリでは
Kotlin
でNavigation
を利用しているNavigation
を利用する時ために、以下のところを追加・変更している
- レイアウト
- ナビゲーショングラフ
- 呼び出し元
- 呼び出し先で引数の取得
Safe Args
という機能で遷移元から遷移先へデータを渡している以上です!
参考サイト
- Sunflowerリポジトリ
- Navigation: 公式ドキュメント
- 投稿日:2020-07-27T01:00:50+09:00
Sunflowerリポジトリで学ぶJetPack〜Navigation編
新型コロナの影響で自宅待機になってしまい、その間勉強するものとして
Sunflower
リポジトリを
勧めてもらいました。JetPackのライブラリのうち、今回は
Navigation
編です尚、引用しているソースは明記しているところ以外は、全て
Sunflower
のリポジトリのものです。環境
- Android Studioは
3.3
以上が必要- 確認時は
3.6.2
を使用Navgation を利用するのに必要なこと
大きく分けて3つ必要です
1. 準備(依存関係の記載)
1. レイアウトファイルにナビゲーションを使うことを記載する
3. ナビゲーション グラフのファイルを作る準備
依存関係の記載
- 公式によると、
build.gradle
に以下の4つの依存関係を記載する必要があります。
- Java language implementation
- Kotlin
- Dynamic Feature Module Support
- Testing Navigation
公式の
build.gradle
のサンプルbuild.gradledependencies { def nav_version = "2.3.0-alpha01" // Java language implementation implementation "androidx.navigation:navigation-fragment:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" // Kotlin implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" // Dynamic Feature Module Support implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version" // Testing Navigation androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" }gradleファイル
それでは、Sunflowerの
build.gradle
を見てみましょう。
Navigation
と関係ないところは省略build.gradle(sunflower)
build.gradlebuildscript { // Define versions in a single place ext { : navigationVersion = '2.2.0' : } dependencies { : classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion" } }公式のチュートリアルにはないものが含まれています!
こちらはsafe args
の機能を利用する際に記載の必要があるものです。
Navgation利用時に安全にパラメータを渡す機能です。
こちらはroot
のbuild.gradle
に記載する必要があるとのこと。
https://developer.android.com/guide/navigation/navigation-pass-data#Safe-argsbuild.gradle(:app)
app/build.gradledependencies { : implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion" implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion" :
$rootProject.navigationVersion
はbuild.gradle(sunflower)
で定義しているnavigationVersion = '2.2.0'
ですねここからわかること
- Sunflowerリポジトリでは、下記のものは利用していないので、依存関係の記載がない。
Java
Dynamic Feature Module Support
、Navigationのテスト
kotlin
は使っているので、依存関係の記載がある。layout
次にレイアウトファイルを見てみましょう。
最初に表示される画面から詳細画面に遷移するところで使っているはずなので、
最初に表示する画面を見てみます。
起動時のActivity
はGardenActivity
でそこでsetContentView
しているxmlファイルはactivity_garden.xml
です
レイアウトはこんな感じactivity_garden.xml
activity_garden.xml<fragment android:id="@+id/nav_host" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph="@navigation/nav_garden"/> </FrameLayout>お、やっぱりここに
Navgation
関連の設定をしています。必要なこと
android:name
にはandroidx.navigation.fragment.NavHostFragment
を設定する
- この
fragment
はNavigation
を使うよ!app:navGraph
には 利用するNavigationのXMLファイルを記載します。ここでNavGraph
と関連付けます。
- ナビゲーショングラフはこのファイルを使うよ!
その他
app:defaultNavHost="true"
を設定すると、戻るボタンで戻るようになります。
- 1つの画面に複数の
Fragment
があり、両方でNavgation
を利用している時などに、戻るボタンを効かなくする、 とかの時にはfalse
にする模様ナビゲーショングラフ
レイアウトで指定されていた
nav_garden
のファイルを見てみましょうNavigation Editor
こんな感じで、画面が複数と、その間を結ぶ矢印が表示されています。
XMLファイル
nav_garden.xml<navigation 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" app:startDestination="@id/view_pager_fragment"> <fragment android:id="@+id/view_pager_fragment" android:name="com.google.samples.apps.sunflower.HomeViewPagerFragment" tools:layout="@layout/fragment_view_pager"> <action android:id="@+id/action_view_pager_fragment_to_plant_detail_fragment" app:destination="@id/plant_detail_fragment" app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" /> </fragment> <fragment android:id="@+id/plant_detail_fragment" android:name="com.google.samples.apps.sunflower.PlantDetailFragment" android:label="@string/plant_details_title" tools:layout="@layout/fragment_plant_detail"> <argument android:name="plantId" app:argType="string" /> </fragment> </navigation>ポイント
- 最初に表示する画面が
<navigation>
タグのapp:startDestination
に記載されている。- 画面遷移に関連する画面が
<fragment>タグ
で記載されている。- 画面遷移の処理詳細が、遷移元の画面に
<action>
タグで記載されている。- 遷移先画面に渡されるパラメータが遷移先画面に
<argment>
タグで記載されている。※この辺りは、
Navigation Editor
で画面をクリックしてグイーンとやって・・・みたいな感じで設定できる部分もあるようです!
(iOS
でいうStoryboard
みたいな感じでしょうか?)
https://developer.android.com/guide/navigation/navigation-getting-started?hl=ja#Designate-startactionタグ設定値の記載方法について
特有の設定値については下記となっています。
※アニメーション関連の設定値が分かりづらかったので
こちらを参照しました
https://stackoverflow.com/questions/56285197/what-is-the-difference-between-enteranim-popenteranim-exitanim-popexitanim
https://developer.android.com/guide/navigation/navigation-animate-transitionsつまるところ、アニメーションはPUSH時とPOP時で分けて記述が可能ということのようです。
名称 内容 設定されている値 android:id
遷移アクションの名称(これをソース内に記載する) @id/action_view_pager_fragment_to_plant_detail_fragment
app:destination
遷移先の画面(宛先) @id/plant_detail_fragment
app:enterAnim
宛先に遷移する際のアニメーション @anim/slide_in_right
app:exitAnim
宛先からに出て行く時のアニメーション @anim/slide_out_left
app:popEnterAnim
宛先にPOPで遷移する時のアニメーション @anim/slide_in_left
app:popExitAnim
宛先からPOPで出て行く時のアニメーション @anim/slide_out_right
使用箇所について
- RecyclerViewの項目をタップした時に、
navigatioToPlant
で画面遷移を実施している。navigatioToPlant
のactionViewPagerFragmentToPlantDetailFragment(plantId)
で遷移先を決定するNavDirections
を取得している。NavDirections
の取得にはNavDirections
インターフェースを継承したクラスActionViewPagerFragmentToPlantDetailFragment
のコンストラクタActionViewPagerFragmentToPlantDetailFragment(plantId)
使用している。ActionViewPagerFragmentToPlantDetailFragment
クラスでは、NavDirections
インターフェースのgetActionId()
メソッドをオーバーライドしている。getActionId()
メソッドは action_view_pager_fragment_to_plant_detail_fragmentから取得している。
⬆︎先ほどナビゲーショングラフのXMLファイルで定義していたaction
のIDですね!(やっと出てきた!(^^;;)GardenPlantingAdapter.ktclass ViewHolder( private val binding: ListItemGardenPlantingBinding ) : RecyclerView.ViewHolder(binding.root) { init { binding.setClickListener { view -> binding.viewModel?.plantId?.let { plantId -> navigateToPlant(plantId, view) } } } private fun navigateToPlant(plantId: String, view: View) { val direction = HomeViewPagerFragmentDirections .actionViewPagerFragmentToPlantDetailFragment(plantId) view.findNavController().navigate(direction) }HomeViewPagerFragmentDirections.ktclass HomeViewPagerFragmentDirections private constructor() { private data class ActionViewPagerFragmentToPlantDetailFragment( val plantId: String ) : NavDirections { override fun getActionId(): Int = R.id.action_view_pager_fragment_to_plant_detail_fragment override fun getArguments(): Bundle { val result = Bundle() result.putString("plantId", this.plantId) return result } } companion object { fun actionViewPagerFragmentToPlantDetailFragment(plantId: String): NavDirections = ActionViewPagerFragmentToPlantDetailFragment(plantId) } }引数の設定
遷移先の画面のタグ内の
<argument>
タグ内に、
遷移先画面で受け取る引数を設定可能
名称 内容 設定されている値 android:name
引数の名前 plantId
app:argType
引数の型 string
遷移先での引数の取得
遷移先のコードは下記です。
PlantDetailFragment.ktprivate val args: PlantDetailFragmentArgs by navArgs() private val plantDetailViewModel: PlantDetailViewModel by viewModels { InjectorUtils.providePlantDetailViewModelFactory(requireActivity(), args.plantId) }公式のドキュメント
https://developer.android.com/guide/navigation/navigation-pass-data?hl=ja#Safe-args
によると、受信側デスティネーションのコード内で、getArguments() メソッドを使用して、バンドルを取得し、そのコンテンツを使用します。-ktx 依存関係を使用している場合、Kotlin ユーザーは、by navArgs() プロパティ デリゲートを使用して引数にアクセスすることもできます。なるほど-
ここではsafe args
という機能を利用して、遷移元から渡されたデータを遷移先でargs
という変数で取得し、
それをViewModel
に入れてますねー。
で、画面ではそのViewModel
を介してデータを使用するようになっています。まとめ
Sunflowerのリポジトリでは
Kotlin
でNavigation
を利用しているNavigation
を利用する時ために、以下のところを追加・変更している
- レイアウト
- ナビゲーショングラフ
- 呼び出し元
- 呼び出し先で引数の取得
Safe Args
という機能で遷移元から遷移先へデータを渡している以上です!
参考サイト
- Sunflowerリポジトリ
- Navigation: 公式ドキュメント