20210107のAndroidに関する記事は5件です。

【Flutter】build() でやってはいけない 3 つのこと

Flutter で仕事したい人のための Widget 入門 で説明した通り、Flutter では基本的に StatelessWidgetStatefulWidget を継承したクラスで build() をオーバーライドし、そこに UI を構築する処理を書いていきます。 (厳密には、 StatefulWidget の場合は State クラスの build()

例↓

login_page.dart
class 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() メソッドが呼ばれます。

_MyHomePageStatebuild() では、以下のように _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 の値がセットされる、というわけです。

他にも、 MediaQueryProvider など、 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 フレームワークの流れを詳しく解説した記事です。ちょっと長くて内容も「慣れてきた人」向けではありますが、いつかは読んでおくととても良い勉強になります。


  1. 実際には build() を直接呼び出すのではなく、 Flutter フレームワークが定期的にチェックしている「更新が必要な Widget リスト」に登録し、次のフレームでの更新対象に入れています。 

  2. このあたりの内容は Flutter の学び初めの段階から知っている必要はありませんが、興味がある場合は 併せて読みたい の参考資料を読んでみてください。 

  3. 対処方法やベストプラクティスは状態管理パッケージごとに異なると思いますので、利用するものに合わせてさらに研究してみてください。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.gradle
apply 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とか勉強になりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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をセッティングする。

image.png

こんな感じ

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開発ができます。

大学のレポートや研究の息抜きにアプリ開発できます。やったぜ

参考にした記事

https://qiita.com/offwhite/items/82b9ac5a751142c2d58e

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更だけど初DataBinding!

前からMVVMを意識した開発をしないとなあと思いつつ、、今まで来てしまったので勉強をかねて備忘録。

やりたいこと

  • ViewModelを使ってデータの操作がしたい!

  • レイアウトからもDataBindingしたい!

  • Fragmentからも操作したい!

完成イメージ
イメージ.gif

二つのタブを用意して、それぞれの画面でメッセージや背景色を変更させてます。
右下のFloatingActionButtonを押すとカウント処理が実行され、カウントがボタンに反映されるようにしました。(Snackbarでもカウント数を表示してますが、、邪魔でしたね:sweat:笑)
ViewModelを使ってるので、画面を回転しても数字は初期化されません!なんて便利なんでしょう。

さっそく

1. Androidプロジェクトを新規作成
ここではDataBindingを中心にやりたかったので、Tabbed Activityテンプレートを使用しました!
スクリーンショット 2021-01-06 18.12.41.png

2. ViewModelに必要な部品を用意する
テンプレートで必要なViewModelは用意されてるので、ちょいとViewModelの中をいじっていきます。
メッセージ用の変数と背景色用の変数とカウント用の変数を用意する。あってるかわかんないけど、こんな感じ。。

PageViewModel.kt
class 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の時はindex1
なのでおはよううううと背景色は#e6e6faがセットされるように設定しています。
※いや正確には0だけどSectionsPagerAdapterposition + 1してるから1なんだなあ〜:rolling_eyes:

SectionsPagerAdapter.kt
override 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をいじるよ
上で追加したmessegeTextcolorIndexを監視してタブが切り替わるたびに値がセットされるように修正する。

PlaceholderFragment.kt
override 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用に変換してくれるんですね。
スクリーンショット 2021-01-07 16.16.54.png

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)}"
カウントが増えていくたびに自動的に数字が変わるようにViewModelindexを読み込みます。

続いてMainActivityの修正!
DataBindingするときは以下のように設定するみたい。

val binding : ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

通常はこれですよね。

setContentView(R.layout.activity_main)

全体ではこんな感じ。

MainActivity.kt
class 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できました:relaxed:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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ファイルを変更してもビルド時に元通りになってしまう。

似たビルドエラー

【Unity】Androidビルドでエラー"maxAspectRatio not found"

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む