真面目なブログはこっち 👉 blog.s64.jp

Android Emulatorで回線速度をシミュレートする手順

  1. 任意のAndroid Emulatorを起動する
  2. 右側ツールバーの「...」ボタンをクリック
  3. 左側メニューの「Cellular」を開く
  4. Full, LTE, HSDPAなどから選択

Android 10 (API 29) 以上で開発者オプションを開く

  1. 設定画面を開く
    • 上部ステータスバーを2段階ほど広げれば大概は歯車アイコンが居る
  2. 下の方にいる "About ${端末名}" というアイテムをタップする
  3. "Build number" を "You are now a developer!" と表示されるまで連打する
  4. 1階層上へbackし、"System" というアイテムをタップする
  5. "Advanced" というアイテムをexpandすると、"Developer options" が追加されている

h.s64.jp

h.s64.jp

Android 8 Oreo (API 26) 以上 Android 9 Pie (API 28) 以下で開発者オプションを表示する手順

  1. 設定画面を開く
    • 上部ステータスバーを2段階ほど広げれば大概は歯車アイコンが居る
  2. 一番下の "System" を開く
  3. "About ${端末名}" をタップする
  4. "Build number" を "You are now developer!" とtoastされるまで連打する
  5. 1階層上へbackすると、"Developer options"というアイテムが追加されている

h.s64.jp

h.s64.jp

Android 7 Nougat (API 25) 以下で開発者オプションを表示する手順

  1. 設定画面を開く
    • 上部ステータスバーを2段階ほど広げれば大概は歯車アイコンが居る
  2. 一番下の "About ${端末名}" というアイテムをタップ
  3. Build numberを "You are now developer!" というtoastが出るまで連打する
  4. 1階層上へbackすると、"Developer options" というアイテムが追加されている

h.s64.jp

h.s64.jp

ライブラリ寿命と付き合う


どこまでいったってポエムなんだけど、じゃあ結局どういう風に考えようかっていう頭の中のことのdumpです。まとまってないけどごめんね。

モバイルアプリケーション開発におけるRxへの依存箇所でいうと、

  • 複雑な非同期的状態変化によるイベント処理を隠蔽する
  • ライフサイクルとの同期を容易にする

という目的で記述された箇所とそこから続く処理全体へ続いている場合が多いと思う。あとはMVVMアーキテクチャ採用でデータバインディングやる都合上とかか。
Rxは非同期的なイベントの繋ぎ込みをめちゃくちゃ容易にできる反面、上手くハンドルしないとそれを行うためのコード全体に影響を与えてしまいうる。
こうしてプロジェクトの至るところで利用されたRxライブラリはツールの置換を困難にする場合がある。

その意味で影響範囲の広いライブラリを用いることは悪と言えるかもしれないのだけど、じゃあRxに相当するライブラリを現実問題として外せるんだっけ? というとかなり難しいケースがある。
現代のアプリケーションはたくさんの状態を持っていて、たくさんの非同期的なイベントを処理しなきゃいけない。さらにモバイルアプリケーションの事情になると、限られたリソースで最大限のパフォーマンスを出すべくライフサイクルなんて言い方をされる制約とも戦う必要がある。
要はモバイルアプリの世界は究極のステートフルで、人間には早すぎる場合すらある。

とするとRxに相当するものとは (新たな実用的パラダイムが提唱されない限り) 付き合い続けることになる。要はRxを使わざるを得ないコードベースにおいては、Rxはライブラリではなく既にパラダイムであり、言語の一部にすら相当しうるのである。そうやすやすとRxへの依存度を下げようなどと考えないほうがよい。

じゃあRx以外使いようがないんだっけ? というとそんなこともない。むしろここには再考の余地が大いにある。
Androidアプリケーション開発の文脈で言うと、同様の機能を提供するものは複数ある。

  • Rx (RxJava)
  • LiveData
  • Kotlin Coroutine

まずLiveDataはGoogleが提供しているAndroid向けのデータホルダライブラリだ。これはRxをモデルにしており、密にAndroidのライフサイクルと結合している。Androidアプリ開発に限れば最適な選択肢になりうる。

そしてKotlin CoroutineはKotlin言語機能のひとつとして提供される非同期処理ライブラリで、特定スレッドに依存しない部分や中断可能である部分などを利用し、Androidのライフサイクルに同期させることが容易である。

これらはRxに依存した既存コードが起こしている問題すべてを解決させることができるか? 答えはNOだ。LiveDataはRxよりもライトではあっても、同様にイベントの伝播を行うために複数モジュール間 〜 アプリケーション全体で依存しうるし、Kotlin Coroutineはさらにさらにライトではあっても結局複数モジュールごとに個別でlaunchを行うことになりうる。

しかし思い返してみれば、Rxは既にそのアプリにおいてはひとつの言語に相当する役割を担っているはずだ。だとすれば、言語選択と同じように考えればアドバンテージが見つかるのではないか。

アプリケーション開発における言語選択というのは面白くて、あくまでツールでしかないにもかかわらず「そのアプリの生き方」を決める。
言語機能として何が提供されるか? それが生産性の向上に繋がるか? というのはライブラリやツールの選定基準と何も変わらないが、このツールはコードの書き方を決定する。コードはビジネスそのものを作る。資産を作っているのである。
もしその言語の実行環境が未来永劫に失われたとしたら、ビジネス全体を別の言語へ移植するという大規模な作業をすることになる。
そのようなことが起こらないよう、十分に情報量があるか、メンテナンスできるだけのリソースが確保できるか、将来に渡っての品質が担保されているか、などを慎重に調査したはずだ。

言語もツールであり、言語に相当する役割を担ったライブラリがあるならば、それは同じ選定基準を持って選択すればよい。すなわち、一緒に生きられるかだ。今メンテナンスしているコードもいつかは寿命が尽きる。その寿命に耐えうるかで言語選定をしたのと同様に選べば良い。

さきほど挙げた3つはどれもOSSだ。すなわち自分自身がメンテナンスをする限り寿命は永遠だ。しかし現実問題としてそれは難しい。

Androidアプリであれば、Androidプラットフォームの寿命が尽きたと同時にそのアプリの寿命は尽きる。それほどまでにAndroid向けアプリはAndroidプラットフォームに依存したコードベースとなっている。だとすればLiveDataはAndroidプラットフォームのためだけにGoogleが開発しているという点で、アプリの寿命に耐えうるかもしれない。

そのAndroidアプリがKotlinで書かれているとしたら、Kotlin言語の寿命が尽きたと同時にそのアプリの既存コードの寿命は尽きる。だとすればKotlin Coroutineはコードの寿命に耐えうるかもしれない。

もちろん期待してはいけない。プラットフォームのAPIから独立したライブラリは容易にクローズできるし、言語機能の一部が非推奨になることもよくある話だ。

しかしもしあなたがRxに依存したコードと別れるべきだと捉えるのだとしたら、そのアプリはRxと一生を共にできなかったのかもしれないし、次のパートナー選びは慎重になるべきだ。一生付き合うことになるかもしれないのだから。

もちろん、リアクティブプログラミングに相当するパラダイムから離れるという選択肢も忘れてはいけない。たとえばFlux、Reduxというアーキテクチャはこれに取って代わるものかもしれない。それにRxの台頭以前はそれがなくともなんとかやってきたはずだ。

さらに言えば、リアクティブプログラミングとの付き合い方を見直すという方法もある。各モジュール内で発行し、サブスクリプションを管理し、他のモジュールへ当該パラダイムを漏れ出させないという方法だ。Kotlin Coroutineなら使い方次第ではこうなるかもしれない。
しかしそれは本当にやりたかったことだろうか。結局のところリアクティブプログラミングのパラダイムによって実現したかったイベント伝播の容易性は失われ、本来のboilerplateを書き続けることになるのではないだろうか。

もし既存のコードがこれまでRxによるイベント伝播にベッタリだとしたら、それらを剥がすコストを払えるだろうか?これは言語を変えるのに相当する労力が必要かもしれない。
もし最初に言語選択を誤ったのだとしたら、はたして他言語への移植を本当にすべきなのか、そのコストは効果に見合うものなのか、よく考えたほうがいい。

How to print your gradle's property in shell

邦題を付けるとしたら、「シェルでGradleのプロパティを取得する方法」とかかな。


Write this:

// root build.gradle
task printMySomething {
    doLast {
        println rootProject.ext.mySomething // set variable to you want
    }
}

And do this:

./gradlew --console=plain --quiet :printMySomething

That's all!

Dynamic Deliveryで `resource integer/google_play_services_version not found.` が出る時の対処

AndroidのDynamic Feature Moduleで開発をする際、下記のようなエラーが出る時がある:

Android resource linking failed
/Users/***/***/app/build/intermediates/merged_manifests/debug/AndroidManifest.xml:***: error: resource integer/google_play_services_version (aka ***:integer/google_play_services_version) not found.
error: failed processing manifest.

詳細を開くと、下記のようになっている:

org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':app:processDebugResources'.
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:95)
    at org.gradle.api.internal.tasks.execution.ResolveTaskOutputCachingStateExecuter.execute(ResolveTaskOutputCachingStateExecuter.java:91)
    at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:57)
    at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:119)
    at org.gradle.api.internal.tasks.execution.ResolvePreviousStateExecuter.execute(ResolvePreviousStateExecuter.java:43)
    at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:93)
    at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:45)
    at org.gradle.api.internal.tasks.execution.ResolveTaskArtifactStateTaskExecuter.execute(ResolveTaskArtifactStateTaskExecuter.java:94)
    at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:56)
    at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:55)
    at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:67)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:49)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:315)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:305)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:175)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:101)
    at org.gradle.internal.operations.DelegatingBuildOperationExecutor.call(DelegatingBuildOperationExecutor.java:36)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:49)
    at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:43)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:355)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:343)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:336)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:322)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker$1.execute(DefaultPlanExecutor.java:134)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker$1.execute(DefaultPlanExecutor.java:129)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:202)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.executeNextNode(DefaultPlanExecutor.java:193)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:129)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
    at java.lang.Thread.run(Thread.java:745)
Caused by: com.android.builder.internal.aapt.v2.Aapt2Exception: Android resource linking failed
/Users/***/***/app/build/intermediates/merged_manifests/debug/AndroidManifest.xml:***: AAPT: error: resource integer/google_play_services_version (aka ***:integer/google_play_services_version) not found.
    
error: failed processing manifest.
    at com.android.builder.internal.aapt.v2.Aapt2Exception$Companion.create(Aapt2Exception.kt:45)
    at com.android.builder.internal.aapt.v2.Aapt2Exception$Companion.create$default(Aapt2Exception.kt:39)
    at com.android.build.gradle.internal.res.Aapt2ErrorUtils.rewriteException(Aapt2ErrorUtils.kt:97)
    at com.android.build.gradle.internal.res.Aapt2ErrorUtils.rewriteLinkException(Aapt2ErrorUtils.kt:73)
    at com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask$AaptSplitInvoker.invokeAaptForSplit(LinkApplicationAndroidResourcesTask.kt:808)
    at com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask$AaptSplitInvoker.run(LinkApplicationAndroidResourcesTask.kt:669)
    at com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask.doFullTaskAction(LinkApplicationAndroidResourcesTask.kt:262)
    at com.android.build.gradle.internal.tasks.IncrementalTask.taskAction(IncrementalTask.java:106)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:73)
    at org.gradle.api.internal.project.taskfactory.IncrementalTaskAction.doExecute(IncrementalTaskAction.java:47)
    at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:41)
    at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:28)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$2.run(ExecuteActionsTaskExecuter.java:284)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:301)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:293)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:175)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:91)
    at org.gradle.internal.operations.DelegatingBuildOperationExecutor.run(DelegatingBuildOperationExecutor.java:31)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:273)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:258)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.access$200(ExecuteActionsTaskExecuter.java:67)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$TaskExecution.execute(ExecuteActionsTaskExecuter.java:145)
    at org.gradle.internal.execution.impl.steps.ExecuteStep.execute(ExecuteStep.java:49)
    at org.gradle.internal.execution.impl.steps.CancelExecutionStep.execute(CancelExecutionStep.java:34)
    at org.gradle.internal.execution.impl.steps.TimeoutStep.executeWithoutTimeout(TimeoutStep.java:69)
    at org.gradle.internal.execution.impl.steps.TimeoutStep.execute(TimeoutStep.java:49)
    at org.gradle.internal.execution.impl.steps.CatchExceptionStep.execute(CatchExceptionStep.java:33)
    at org.gradle.internal.execution.impl.steps.CreateOutputsStep.execute(CreateOutputsStep.java:50)
    at org.gradle.internal.execution.impl.steps.SnapshotOutputStep.execute(SnapshotOutputStep.java:43)
    at org.gradle.internal.execution.impl.steps.SnapshotOutputStep.execute(SnapshotOutputStep.java:29)
    at org.gradle.internal.execution.impl.steps.CacheStep.executeWithoutCache(CacheStep.java:134)
    at org.gradle.internal.execution.impl.steps.CacheStep.lambda$execute$3(CacheStep.java:83)
    at java.util.Optional.orElseGet(Optional.java:267)
    at org.gradle.internal.execution.impl.steps.CacheStep.execute(CacheStep.java:82)
    at org.gradle.internal.execution.impl.steps.CacheStep.execute(CacheStep.java:36)
    at org.gradle.internal.execution.impl.steps.PrepareCachingStep.execute(PrepareCachingStep.java:33)
    at org.gradle.internal.execution.impl.steps.StoreSnapshotsStep.execute(StoreSnapshotsStep.java:38)
    at org.gradle.internal.execution.impl.steps.StoreSnapshotsStep.execute(StoreSnapshotsStep.java:23)
    at org.gradle.internal.execution.impl.steps.SkipUpToDateStep.executeBecause(SkipUpToDateStep.java:96)
    at org.gradle.internal.execution.impl.steps.SkipUpToDateStep.lambda$execute$1(SkipUpToDateStep.java:91)
    at java.util.Optional.orElseGet(Optional.java:267)
    at org.gradle.internal.execution.impl.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:91)
    at org.gradle.internal.execution.impl.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:36)
    at org.gradle.internal.execution.impl.DefaultWorkExecutor.execute(DefaultWorkExecutor.java:34)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:91)
    ... 35 more
Caused by: com.android.builder.internal.aapt.v2.Aapt2Exception: Android resource linking failed
/Users/***/***/app/build/intermediates/merged_manifests/debug/AndroidManifest.xml:***: error: resource integer/google_play_services_version (aka ***:integer/google_play_services_version) not found.
error: failed processing manifest.

    at com.android.builder.internal.aapt.v2.Aapt2Exception$Companion.create(Aapt2Exception.kt:45)
    at com.android.builder.internal.aapt.v2.Aapt2Exception$Companion.create$default(Aapt2Exception.kt:39)
    at com.android.builder.internal.aapt.v2.Aapt2DaemonImpl.doLink(Aapt2DaemonImpl.kt:191)
    at com.android.builder.internal.aapt.v2.Aapt2Daemon.link(Aapt2Daemon.kt:103)
    at com.android.builder.internal.aapt.v2.Aapt2DaemonManager$LeasedAaptDaemon.link(Aapt2DaemonManager.kt:176)
    at com.android.builder.core.AndroidBuilder.processResources(AndroidBuilder.java:858)
    at com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask$AaptSplitInvoker.invokeAaptForSplit(LinkApplicationAndroidResourcesTask.kt:797)
    ... 79 more

原因

これはAndroidアプリのManifestに本来設定されるべきGoogle Play Servicesのバージョン番号メタデータが付与されていないことによるエラーである。
シングルモジュールでの一般的なAndroidアプリ開発では、必要とされるシーンでは自動的に設定される値である。というのも、play-services-basement内に含まれるManifestの内容がManifest Mergerによって自動的に設定されるからだ。

対してDynamic Feature Moduleを用いたマルチモジュール構成では Base APK (多くの場合 :app モジュール) がいわば単一のアプリのように振る舞うため、ビルド時点で必要な値であるにも関わらず アプリに値が含まれないことになってしまう。

たとえば今回 広告系のSDKを新たに導入しようとして発生したのなら、下記のような構成にしているのではないだろうか。

/app/build.gradle:

dependencies {
    ...
    api "com.google.android.play:core:1.6.1"
    ...
}

/feature/build.gradle:

dependencies {
    ...
    implementation 'com.google.android.gms:play-services-ads:18.1.1'
    ...
}

対処

当該の値を追加するモジュールのみ、Base Moduleに含んでしまえばよい。

/app/build.gradle:

dependencies {
    ...
    api "com.google.android.play:core:1.6.1"
    api 'com.google.android.gms:play-services-basement:17.1.0'
    ...
}

/feature/build.gradle:

dependencies {
    ...
    implementation 'com.google.android.gms:play-services-ads:18.1.1'
    ...
}