- 投稿日:2021-01-07T22:58:33+09:00
【Flutter】build() でやってはいけない 3 つのこと
Flutter で仕事したい人のための Widget 入門 で説明した通り、Flutter では基本的に
StatelessWidget
やStatefulWidget
を継承したクラスでbuild()
をオーバーライドし、そこに UI を構築する処理を書いていきます。 (厳密には、 StatefulWidget の場合はState
クラスのbuild()
)例↓
login_page.dartclass LoginPage extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ TextFormField( decoration: InputDecoration( hintText: 'chooyan@example.com', ), ), const SizedBox(height: 32), ElevatedButton( onPressed: () {}, child: Text('ログインメールを送る'), ), ], ), ); } }初めて Flutter を触る人にとっては、この
build()
を そのページの「初期化処理」と捉えてしまいがち なのではないかと思います。自分もそうでした。しかし、
build()
を「初期化処理」だと考えて「やってはいけないこと」を書いてしまうと、思わぬパフォーマンスの低下や不具合を引き起こしてしまう可能性があります。この記事では、そんな
build()
でやってはいけない 3 つのパターン を理由つきで説明していきます。
build()
は何度でも呼ばれるまず知っておかなければならないのは、
build()
は何度でも呼ばれる、ということです。最初の1回だけではありません。そもそも Flutter は、「状態」が変わるたびに何度も Widget を破棄し、新しい「状態」を使って Widget を作り直すことで画面を変化させる 設計になっています。
例えば Flutter プロジェクトを新規作成した時にテンプレートとして作られる「カウンターアプリ」では、
FloatingActionButton
として配置されたカウントアップボタンをタップするたびに_counter
(状態を表す変数)をインクリメントして_MyHomePageState
の中のbuild()
メソッドが呼ばれます。
_MyHomePageState
のbuild()
では、以下のように_counter
変数の値をText
で画面に表示するようにコーディングされていますので、Text( '$_counter', style: Theme.of(context).textTheme.headline4, ),
build()
が呼ばれるたびにその時点での_counter
の値を持ったText
が作り直され、画面の表示内容が変化する仕組みになっています。
build()
が呼ばれるタイミングそんな
build()
が呼ばれるタイミングは様々です。一番分かりやすいのは、
StatefulWidget
(が生成するState
)でsetState()
が呼ばれたときです。カウンターアプリでも、 FloatingActionButton がタップされた時に呼び出される
_incrementCounter
メソッドにはvoid _incrementCounter() { setState(() { _counter++; }); }とコーディングされていて、
setState()
を呼び出すことと、_counter
をインクリメントすることが書かれています。Flutter では、この
setState()
内に書いた処理を実行したあとにbuild()
を呼び出す1作りになっているため、新しくbuild()
で作られる Text にはインクリメントした後の_counter
の値がセットされる、というわけです。他にも、
MediaQuery
やProvider
など、InheritedWidget
が保持する値が更新された場合や、同様のことが Widget ツリーの先祖で発生してbuild()
が伝播してきた場合など、いろいろなタイミングがあります。2特にアニメーションが発生する場合、60 fps、つまり 16 ミリ秒に 1 回という速度(参考)で毎フレームこの
build()
が呼ばれる場合もあります。とにかく、
build()
は様々なタイミングで、高速に何度でも呼ばれる可能性がある 、ということを忘れてはいけません。build() でやってはいけない 3 つのこと
build()
が高速に何度でも呼ばれる可能性がある ということを踏まえると、以下のような処理はそこに書いてはいけないことが見えてきます。1. 初期化処理
build()
は画面が出ている間(場合によっては画面に見えていない間も)何度でも呼ばれる可能性があります。そのため、状態を管理する変数を初期化するようなコードは書いてはいけません。int _count; @override Widget build(BuildContext context) { _count = 0; // 0で初期化 return Container( child: (省略) ); }このように書いてしまうと、何かのきっかけで
build()
が呼ばれ直した瞬間にそれまでのカウントが 0 に戻ってしまいます。また、 Firebase やローカルのデータベースとの接続処理であったり、表示するデータの取得処理などもここに書いてしまうと繰り返し呼び出されてバグの原因につながります。サーバーにアクセスする処理であればサーバーの負荷にも影響が出てしまうでしょう。
このような初期化処理は、基本的に
State
クラスが持っているinitState()
をオーバーライドするなど、確実にその画面が初めて生成されるタイミングに 1 回だけ呼ばれる場所に書くのが良いでしょう。2. ひとつだけあれば良いオブジェクトの生成
たとえば GoogleMaps やアニメーション関係の Controller であったり、一度生成してキャッシュしておく Widget の生成処理などはここに書かいてはいけません。
// 良くない例 class MapSampleState extends State<MapSample> { Completer<GoogleMapController> _controller; @override Widget build(BuildContext context) { // build ごとに mapController を生成してはいけない _controller = Completer(); return GoogleMap( (省略) onMapCreated: (GoogleMapController controller) { _controller.complete(controller); }, ); } }// 良くない例 class FixedComponentState extends State<FixedComponent> { List<Widget> cachedMenu; @override Widget build(BuildContext context) { // 使い回す Widget をここで生成してはいけない cachedMenu = [ Text('メニューその1'), Text('メニューその2'), Text('メニューその3'), ]; return Column( children: cachedMenu, ); } }Controller が複数生成されてしまってその Controller 自体の初期化処理が何度も無駄に実行されてしまったり、せっかくキャッシュしておいた Widget が何度も再生成 & 上書きされてキャッシュの意味がなくなってしまいます。
このような場合は、例えば以下のようにフィールドの宣言と同時にインスタンスの生成までやってしまうと良いでしょう。
// 改善例 (google_maps_flutter 公式のサンプルコード通り) class MapSampleState extends State<MapSample> { Completer<GoogleMapController> _controller = Completer(); @override Widget build(BuildContext context) { return GoogleMap( (省略) onMapCreated: (GoogleMapController controller) { _controller.complete(controller); }, ); } }// 改善例 class FixedComponentState extends State<FixedComponent> { // FYI: 可能な限り const もつけるとなお良い List<Widget> cachedMenu = [ Text('メニューその1'), Text('メニューその2'), Text('メニューその3'), ]; @override Widget build(BuildContext context) { return Column( children: cachedMenu, ); } }3. その他重い処理
その他、多量な計算や通信処理、ファイルの読み書きなど、重い処理を書けば書くほど画面の変化を端末に反映させるのが滞ってしまいます。なるべく
build()
には Widget の生成以外の処理を書かないようにしましょう。もし重い処理を書く必要があるのであれば、やはり
initState()
など適切な場所でasync
つきのメソッドにまとめて処理が終わり次第setState()
で結果だけを反映させるような工夫を入れます。処理が終わるまでは CircularProgressIndicator でクルクルを表示するなどすると良いでしょう。class ArticleListState extends State<FixedComponent> { List<Article> articles; Future<void> _loadArticles() async { final result = await ArticleLoader().all(); // 記事を全件取得する処理 setState(() { articles = result; } } @override void initState() { _loadArticles(); super.initState(); } @override Widget build(BuildContext context) { // articles が null ならクルクルを表示、あれば ArticleList を表示 return articles == null ? CircularProgressIndicator() : ArticleList(articles); } }まとめ
Flutter を入門すると、まずソースコードを書き始めるのが
build()
の中(StatelessWidget でも StatefulWidget でも)な関係上、どうしてもここが「最初に1回」やる処理を書く場所だと誤解してしまいがちです。しかし実際は、 Flutter の仕組みとして Widget は使い捨てであり、
build()
は何度でも呼び出されます。このことを初学者はまず知る必要があるでしょう。公式ドキュメントにも、以下のように記述されています。This method can potentially be called in every frame and should not have any side effects beyond building a widget.
訳) このメソッドは毎フレーム呼び出される可能性があり、Widget を構築する以外の副作用を発生させてはいけません。これは Flutter の根本的な設計の都合ですので、 Provider などの状態管理パッケージを使う場合でも同様の発想が必要になります。3
「1回だけやれば良い処理や重い処理は
build()
に書いてはいけない、なぜならbuild()
は高速に連続で呼び出される可能性があるから」 ということを頭に置いておくだけでも、 Flutter アプリ開発をする上で発生する多くの不具合を回避できるでしょう。(2021.1.10 追記)
この記事の内容を気をつけていても、アプリの規模が大きくなるのにしたがってbuild()
による画面の更新が重くなってしまう場合があります。そんな時に使えるテクニックと考え方を以下の記事に書いてみましたので、読んでみてください。【Flutter】 無駄なリビルドを防ぐたった1つの方法
(追記ここまで)併せて読みたい
この記事の内容をさらに詳しく理解する手助けになる記事です。それぞれの記事には公式サイトも参照先に挙げられていますので、そこまで読めればだいぶ理解を深められるでしょう。
記事中に出てきた
InheritedWidget
の役割や使い方を解説した記事です。 InheritedWidget は実際にアプリ開発者が直接使うことは少ない Widget のため参考資料も少なめですが、この記事は実際に動作するコードつきで段階的に説明してくれているので、 InheritedWidget というものを理解する上でとても勉強になると思います。InheritedWidget の中身にフォーカスした記事です。Flutter の実装なども読みながらその仕組みを追っていきます。なお、↑の記事とは違い「どうやって使うのか」には一切触れていません。
setState()
の実装を追いながらその仕組みを説明した記事です。フレームワーク内部のソースコードに興味のある方は読んでみると、 Flutter のソースコードの追い方などが見えてくるかもしれません。Flutter の Widget が画面に表示されるまでの Flutter フレームワークの流れを詳しく解説した記事です。ちょっと長くて内容も「慣れてきた人」向けではありますが、いつかは読んでおくととても良い勉強になります。
- 投稿日:2021-01-07T17:44:35+09:00
Play Billing Library 3.0でstatic responseな購入のテストをする
背景
Androidのアプリ内課金は2021年11月1日以降、BillingLibrary3.0以上を搭載していないとアプリのアップデートがリジェクトされるようなる。
https://android-developers.googleblog.com/2020/06/meet-google-play-billing-library.htmlその課金ライブラリの移行にあたり、AIDLまでの時代に出来ていた、Static Responseと呼ばれるもので実際には課金が行われないテストが出来るか確認したかった。
Static Responseとは
アプリ内課金では、sku(課金アイテムID)を指定して、登録済の課金アイテムを購入しますが、以下のようなskuを指定することで固定のレスポンスを得ることが出来るというものです。
android.test.purchased
android.test.canceled
android.test.refunded
android.test.item_unavailable
参考)https://stuff.mit.edu/afs/sipb/project/android/docs/google/play/billing/billing_testing.html
これらを指定すると、Playストアの課金画面が以下のようになり、実際には課金が行われない、フェイクの画面であることが分かるようになります。
テスト購入が可能なテストユーザー登録という方法もあるのですが、アカウント管理が別部署だったりして手続きが煩雑な場合など、まずはStatic Responseでフローを確認したいという状況は多々あると思いますので、そんな場合に参考になれば幸いです。
環境
ツール・ライブラリなど バージョン Android Studio 3.6.3 Kotlin 1.3.71 Gradle 5.6.4 Android Gradle Plugin 3.6.3 Billing Library 3.0.0 参考までに、プロジェクトとアプリのの
build.gradle
を載せておきます。
プロジェクトの
build.gradle
project/build.gradle// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = '1.3.71' ext.lifecycleVersion = "2.2.0" ext.gradle_version = '3.6.3' ext.roomVersion = "2.2.5" repositories { google() jcenter() } dependencies { classpath "com.android.tools.build:gradle:$gradle_version" 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
app/build.gradleapply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { compileSdkVersion 29 buildToolsVersion "29.0.3" dataBinding { enabled true } defaultConfig { applicationId "com.example.billingsample" minSdkVersion 23 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } apply from: '../key.gradle', to: android buildTypes { debug { signingConfig signingConfigs.debug } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } flavorDimensions 'lib' productFlavors { aidl { dimension = 'lib' manifestPlaceholders = [appName: 'AIDLサンプル'] } pbl { dimension = 'lib' manifestPlaceholders = [appName: 'PBLサンプル'] } } sourceSets { aidl { java { srcDirs 'src/aidl/java', 'src/aidl/java/' } } pbl { java { srcDirs 'src/pbl/java', 'src/pbl/java/' } } } kotlinOptions { jvmTarget = '1.8' } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' // appended implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2' implementation 'com.google.android.material:material:1.2.1' // ViewModel and liveData implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion" implementation 'androidx.fragment:fragment-ktx:1.2.5' implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" // Databinding kapt "androidx.databinding:databinding-common:$gradle_version" // Room implementation "androidx.room:room-runtime:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion" implementation "androidx.room:room-ktx:$roomVersion" // coroutine implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2' // Only for PBL flavor def billing_version = "3.0.0" pblImplementation "com.android.billingclient:billing:$billing_version" pblImplementation "com.android.billingclient:billing-ktx:$billing_version" }可否
まず出来るのかできないかですが、結論から言うと、可能です。
方法
PBL版では、購入時に
SkuDetails
という情報が必要になります。
これは、BillingClient#querySkuDetailsAsync
という関数で事前に引っ張っておくのが通常かと思います。
Static Responseも、同じようにこの関数を挟んでSkuDetails
を取れば、BillingClient#launchBillingFlow
に渡すことが出来て購入のテストが可能になります。1.SkuDetailsを先に得る
private val skuDetailsMap = mutableMapOf<String, SkuDetails>() private val skuList = listOf("android.test.purchased") private fun querySkuDetails(){ val params = SkuDetailsParams.newBuilder().setSkusList(skuList).setType(BillingClient.SkuType.INAPP).build() billingClient.querySkuDetailsAsync(params) { billingResult, skuDetailsList -> when (billingResult.responseCode) { BillingClient.BillingResponseCode.OK -> { if (skuDetailsList.orEmpty().isNotEmpty()) { skuDetailsList?.forEach { skuDetailsMap[it.sku] = it } } } else -> { Log.e(TAG, billingResult.debugMessage) } } } }2.SkuDetailsを使用して課金フローを起動する
fun purchaseStaticItem(activity: Activity) { // 購入用のパラメータを作成 val skuDetails = skuDetailsMap["android.test.purchased"] skuDetails?.let { val purchaseParams = BillingFlowParams.newBuilder() .setSkuDetails(it) .build() billingClient.launchBillingFlow(activity, purchaseParams) } }これだけです。
参考プロジェクト
AIDL版とPBL版をProduct Flavorで切り替えられるサンプルプロジェクトを作りました。
移行の参考などにして貰えたらと思います。注意点としては、AIDL版のStatic Responseによる購入は、なぜか消費しなくても何度も買えてしまいます。以前は毎回消費が必要だったように記憶しているのですが、AIDLの内部バージョンで違うのでしょうかね。
また、買い切りタイプやサブスク(定期購読)には対応しておらず、消費可能アイテムにのみ対応しています。
コミットAPIと書いているのは、サーバー側の処理を呼び出す想定で作ったサンプルだからですが、実際には何も呼んでいません。通常はサーバー側で署名やレシートの検証、ポイント等を付与を行うと思うのでこのタイミングでよしなにやってください。https://github.com/les-kasa-1010/BillingLibrarySample3
参考サイト
Static Responseの件
https://www.366service.com/jp/qa/417a0cdef1cc694aaa23928a67faf9d6
中国語の質問板を機械翻訳したもののようですが、非常に参考になりました。coroutine関係
https://star-zero.medium.com/callback%E5%BD%A2%E5%BC%8F%E3%81%AE%E3%82%82%E3%81%AE%E3%82%92coroutines%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8B-9384dfa6ad77
コールバック形式のものの処理結果をViewModelで待って受け取る方法の参考にしました。https://stackoverflow.com/questions/61388646/billingclient-billingclientstatelistener-onbillingsetupfinished-is-called-multip
PurchasesUpdatedListener#onPurchasesUpdatedの購入結果をViewModelで非同期処理を待って受け取る方法の参考にしました。
質問者さんのコードが参考になりました。(質問者さんの質問本題はよく読んでいませんw)余談
Android Extensionsがdeprecatedになるようですが、そのまま使っています。
coroutineも私が以前触ったときとはまたバージョンが変わっているようで、StateFlow
とか勉強になりました。
- 投稿日:2021-01-07T17:37:11+09:00
Android Studio +GitをProxy設定&Proxy設定解除
出来事
昨今の大学生は全員サイバーテロや違法サイトに興味あると思っているのか知らないですが、
大学の学内でのHTTP通信は、すべて監視用のProxyサーバーを経由しなければなりません。僕は誰かを幸せにするAndroidアプリを開発したいだけなのに。。。。
3億番煎じかもしれませんが、今回はAndroid開発をProxyを経由して行う方法とProxy設定後の正しい解除の方法を書いていきます。
Android開発の場合
・Git
・Gradle
この2つのProxyの設定が主に必要であります。
Gitを使っていない人は、Gitはやらなくていいと思います。Proxyの設定をするとき
1. Android Studio のSettingでProxyをセッティングする。
こんな感じ
2. Project直下の
gradle.properties
を変更systemProp.http.proxyHost=proxy.hoge systemProp.http.proxyPort=8080 systemProp.https.proxyHost=proxy.hoge systemProp.https.proxyPort=8080多くは語るまい。
3. Git
自分はWindowsなので、GitBashかWslで以下コマンドを打ちます。
git config --global http.proxy http://proxy.hoge:8080 git config --global https.proxy http://proxy.hoge:8080自分の場合、
.sh
ファイルでまとめて使うようにしています。直接2回コマンドを叩いても問題ありません。
Proxyを解除するとき
家やカフェなど、Proxyサーバーを経由しない場所のネットワークを使う場合、
Proxyサーバを経由させる設定のままですと、永遠に通信が終わりません。
自分の場合、アプリのビルドがいつまで経っても終わりませんでした。
ゆえに解除させる必要があります。
やることを載せておきます
1. Android Studio のSettingで
No Proxy
にチェックを入れる2. Project直下の
gradle.properties
からProxyの記述を削除ようは、Proxyの設定を行う際と逆のことをするだけです。しかし、これだけではProxyの設定が解除されきらないことがあります。
先ほどやったことをすべて直したというのに、Buildが終わりません。
結果、もう一つ手順が必要ということがわかりました。
3 ~/.gradle/gradle.properties を変更する
ホームディレクトリ直下に、隠しフォルダとして、
.gradle
が存在すると思います。こちらの中にも、
gradle.properties
が存在します。僕の場合は、こちらのファイルにProject直下のgradle.propertiesと同じProxyの設定が書いてありました。
## こいつらを消す systemProp.http.proxyHost=proxy.hoge systemProp.http.proxyPort=8080 systemProp.https.proxyHost=proxy.hoge systemProp.https.proxyPort=8080自分の場合、この3つの手順を行えば、Proxyの設定が完全に解除されます。
逆にProxyの設定の時点で躓いた方は、ホームディレクトリ直下の.gradleファイルにこちらのProxyの設定を見るべきです。
一応Gitも載せておきます。
git config --global --unset http.proxy git config --global --unset https.proxy厄介なポイントとして、設定する時と設定を解除する手順で違う場所が存在するところですね。
これを使えば、Proxyサーバー経由を必要とする場所においてもAndroid開発ができます。
大学のレポートや研究の息抜きにアプリ開発できます。やったぜ
参考にした記事
- 投稿日:2021-01-07T16:39:48+09:00
今更だけど初DataBinding!
前からMVVMを意識した開発をしないとなあと思いつつ、、今まで来てしまったので勉強をかねて備忘録。
やりたいこと
ViewModelを使ってデータの操作がしたい!
レイアウトからもDataBindingしたい!
Fragmentからも操作したい!
二つのタブを用意して、それぞれの画面でメッセージや背景色を変更させてます。
右下のFloatingActionButtonを押すとカウント処理が実行され、カウントがボタンに反映されるようにしました。(Snackbarでもカウント数を表示してますが、、邪魔でしたね笑)
ViewModelを使ってるので、画面を回転しても数字は初期化されません!なんて便利なんでしょう。さっそく
1. Androidプロジェクトを新規作成
ここではDataBindingを中心にやりたかったので、Tabbed Activityテンプレートを使用しました!
2. ViewModelに必要な部品を用意する
テンプレートで必要なViewModelは用意されてるので、ちょいとViewModelの中をいじっていきます。
メッセージ用の変数と背景色用の変数とカウント用の変数を用意する。あってるかわかんないけど、こんな感じ。。PageViewModel.ktclass PageViewModel : ViewModel() { private var count = 0 private val _index = MutableLiveData<Int>().also { mutableLiveData -> mutableLiveData.value = 0 } val index: LiveData<Int> get() = _index private val _colorIndex = MutableLiveData<String>() val colorIndex: LiveData<String> get() = _colorIndex private val _messageText = MutableLiveData<String>().also { mutableLiveData -> mutableLiveData.value = "" } val messageText: LiveData<String> get() = _messageText fun setIndex(index: Int) { if(index == 1) { _messageText.value = "おはようううう" _colorIndex.value = "#e6e6fa" } else { _messageText.value = "こんにちわああ" _colorIndex.value = "#fff0f5" } } fun count() { _index.value = count++ } }
setIndex
はFragmentを構成する時に呼ばれる。タブ1の時はindex
が1
なのでおはようううう
と背景色は#e6e6fa
がセットされるように設定しています。
※いや正確には0
だけどSectionsPagerAdapter
でposition + 1
してるから1
なんだなあ〜SectionsPagerAdapter.ktoverride fun getItem(position: Int): Fragment { // getItem is called to instantiate the fragment for the given page. // Return a PlaceholderFragment (defined as a static inner class below). return PlaceholderFragment.newInstance(position + 1) }3. Fragmentをいじるよ
上で追加したmessegeText
とcolorIndex
を監視してタブが切り替わるたびに値がセットされるように修正する。PlaceholderFragment.ktoverride fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val root = inflater.inflate(R.layout.fragment_main, container, false) val textView: TextView = root.findViewById(R.id.section_label) val layout: ConstraintLayout = root.findViewById(R.id.constraintLayout) pageViewModel.messageText.observe(this, Observer<String> { textView.text = it }) pageViewModel.colorIndex.observe(this, Observer<String> { layout.setBackgroundColor(Color.parseColor(it)) }) return root }ここまでで、タブの切り替えで文字と背景色が変わるように実装が完了!
カウントはまだだよ。4. Activityをいじるよ
まずレイアウトから値がすぐ反映できるようにDataBindingを有効にする。
以下を追記したら、Sync Now
!build.gradle(app)plugins { ・・・略・・・ id 'kotlin-kapt' } ・・・略・・・ android { ・・・略・・・ dataBinding { enabled true } }続いて、レイアウトに適用させるよ!
ここでは、MainActivity
のレイアウトにバインドさせたいからactivity_main
を開いて、レイアウトにカーソルを合わせたとこで、option + command
を押すよ。(はい、macです)
そうすると、以下みたいなConvert to data binding layout
っていうのがでてくるのでそれを選択すると簡単にDataBinding用に変換してくれるんですね。
dataタグ内にDataBindingしたい変数を定義して、こんな感じにカウントボタンを用意しました!
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <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="vm" type="jp.co.test.tabapp.ui.main.PageViewModel" /> </data> <androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.google.android.material.appbar.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/Theme.TabApp.AppBarOverlay"> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:minHeight="?actionBarSize" android:padding="@dimen/appbar_padding" android:text="@string/app_name" android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title" /> <com.google.android.material.tabs.TabLayout android:id="@+id/tabs" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?attr/colorPrimary" app:tabSelectedTextColor="@color/teal_700" app:tabTextColor="@color/white" /> </com.google.android.material.appbar.AppBarLayout> <androidx.viewpager.widget.ViewPager android:id="@+id/view_pager" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end"> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/fab_margin" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="@{String.valueOf(vm.index)}" android:elevation="16dp" android:textColor="@color/white" android:textAppearance="?android:attr/textAppearanceMedium" /> </FrameLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout> </layout>この一番下にある
TextView
のとこがカウント数を表示する部分です。
android:text="@{String.valueOf(vm.index)}"
カウントが増えていくたびに自動的に数字が変わるようにViewModel
のindex
を読み込みます。続いてMainActivityの修正!
DataBindingするときは以下のように設定するみたい。val binding : ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)通常はこれですよね。
setContentView(R.layout.activity_main)全体ではこんな感じ。
MainActivity.ktclass MainActivity : AppCompatActivity() { private lateinit var pageViewModel: PageViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) pageViewModel = ViewModelProvider(this).get(PageViewModel::class.java) val binding : ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.vm = pageViewModel binding.lifecycleOwner = this val sectionsPagerAdapter = SectionsPagerAdapter(this, supportFragmentManager) binding.viewPager.adapter = sectionsPagerAdapter binding.tabs.setupWithViewPager(binding.viewPager) binding.fab.setOnClickListener { view -> pageViewModel.count() Snackbar.make(view, "count = " + pageViewModel.index.value, Snackbar.LENGTH_LONG) .setAction("Action", null).show() } } }これ重要です。
binding.lifecycleOwner = this
これがなかったせいで、カウントボタン押してるのに数字がUIに反映されなくて詰まってました〜。笑おわり
まだまだ流れが理解できず混乱したりするし、
本当にこのやり方でいいのかわかりませんが、とりあえずDataBindingできました
- 投稿日:2021-01-07T01:17:32+09:00
[Unity] Prime31(ios)でのandroidのビルドエラー
状況
UnityにPrime13(iOS : Etcetera Plugin)を追加した。
iosでのビルドは問題なくできたが、androidのビルド時にエラーが起きたエラー内容
What went wrong:
Execution failed for task ':launcher:processReleaseResources'.
Android resource linking failed
/Users/ユーザー名/Desktop/Unity/プロジェクト名/Temp/gradleOut/launcher/build/intermediates/merged_manifests/release/AndroidManifest.xml:52: AAPT: error: unexpected element found in .
解決法
プロジェクトファイル内の
AndroidManifest.xml
のコードを変えた(変更前)AndroidManifest.xml<manifest> <application android:label="@string/app_name" android:hasCode="false" android:debuggable="false"> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> ... </application> </manifest>(変更後)AndroidManifest.xml<manifest> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application android:label="@string/app_name" android:hasCode="false" android:debuggable="false"> ... </application> </manifest>注意点
プロジェクト名/Assets/Plugins/Android/AndroidManifest.xml
AndroidManifest.xml
のファイルを変更する。
エラーログにあるパスのAndroidManifest.xml
ファイルを変更してもビルド時に元通りになってしまう。似たビルドエラー