Androidアプリのmicro-module設計
- サーバーサイドの世界では、micro-servicesとか流行ってるね
- Androidの世界で、microな設計思想は取り入れられるのか?
Androidアプリのmoduleはどこまで分けられるのか?
- Domain
- Bridge
- ドメインで定義したServiceの実装詳細を書く
- Factory
- どのブリッジを使用するか選択する
- SQLite使う? Realm使う? そういう切り分けを行う
- Components
- Application
Componentsをどのように分けるか
Activityをinternal化する
- もちろん、関連するFragmentも全部internalにする
// こうしたい // こうすることで、画面設計の詳細が[app_components_splash]モジュールから漏れ出さない internal class SplashActivity : Activity() { }
- Kotlinコードからアクセスできないだけで、AndroidManifestに書けばちゃんと動作する
Activity間の遷移をどうするか?
- [app]
- [app_components_splash]
- [app_components_main]
// app/build.gradle // [app_components_splash]と[app_components_main]を完全に非依存としたい dependencies { implementation project(':app_components_splash') implementation project(':app_components_main') }
- 通常はこうしたいが
- 無策にやると、これはできない
// SplashActivity.kt から、 MainActivity.kt は見えない! // なぜなら、別moduleかつ非依存かつinternalで定義されているから!! startActivity(Intent(context, MainActivity::class.java))
Component間に規約を設ける
- app_components_main/AndroidManifest.xmlにmeta-dataを追加
- こうすることで、Contextから拾うことができる
<?xml version="1.0" encoding="utf-8"?> <manifest > <application android:theme="@style/Theme.AppCompat.NoActionBar"> <activity android:name=".MainActivity"> <meta-data android:name="com.eaglesakura.modules.COMPONENT_KEY" android:value="com.eaglesakura.modules.screen.MAIN" /> </activity> </application> </manifest>
Contextからmeta-data経由でIntentを生成する
// こんな感じでpackageInfo取得 -> activityInfo検索 -> Intent生成が行える packageInfo = context.packageManager.getPackageInfo( context.packageName, PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_META_DATA ) for (activityInfo in (packageInfo.activities ?: emptyArray())) { val componentKey = activityInfo.metaData?.getString("com.eaglesakura.modules.COMPONENT_KEY") if("com.eaglesakura.modules.screen.MAIN" == componentKey) { return Intent().also { it.component = ComponentName(context.packageName, activityInfo.name) } } }
突き詰めるとどうなるか
- 1画面1module、1service1module、1provider1module...というレベルまで細分化できる
- [app] モジュールは、最終的なビルドフレーバーを分けたりapplication-idを確定させる程度の存在になる
利点はあるのか?
- UIやシステムコンポーネントレベルでの依存性が無くなるため、moduleの交換が容易になる
- 1画面だけ何か新しいライブラリを入れてみよう、といったことが行える
- デザインの変更
- 「この画面だけに必要なリソース」等を分けやすい
- 特定moduleを捨てる決断が容易になる
- リファクタリングを小スケールで行える
- module間が粗結合になるため、担当者を分けやすい
- 実際に分けてみると、依存しているライブラリが「この画面のためだけに導入した」という感じになる
- 意図せず使ってしまって密結合になるケースがある
- そういったケースを防げる
欠点は何か?
- moduleがめっちゃ増える
- PCへの負荷が増える
- Android Studio 3.2世代ならまあ問題ない
- i7 7500U, RAM16GB + Ubuntu環境でそれなりに開発できる
- 物理4コア以上でしょ、いまどき?
- Gradleの
implementation
api
, Kotlinのinternal
に対する知識が最低限必要になる- 何がinternalなのか、ではない
何がpublicなのか
に注目して設計し、それ以外は全部privateかinternalにする
- メモリを食う
- CircleCIのデフォルトメモリ(4GB)だと稀によく落ちる場合がある
- res/*.xml が増える
CIのメモリ不足対処
- どうしてもmodule数が増えるとメモリ使用量も増える
- 予算や規模の都合でresource_classが変更できない場合
- compileとassemble/testを分ける
# *.classを先行して生成する ./gradlew --no-daemon :app_components_main:compileDebugSources # *.classのキャッシュを使うことで、メモリ使用量を抑える ./gradlew --no-daemon :app_components_main:testDebug
結論として、この設計は使い物になるか?
- 今の所使えそう
AndroidManifestやApplicationIDに記述されるPackage名にsnake_caseはアリかナシか
Package名の命名規則を、メジャーな形に統一したい
- プロジェクト内で統一したかった
package.name.hoge.fuga
package.name.hoge_fuga
package.name.hogeFuga
あたりの競い
- けど
snake_case
はLintで怒られる allword
は単語登録が毎度必要になるcammelCase
は個人的にあんまり使ったこと無い
有名そうな、snake_case
有名そうな、allword
まさかの CammelCase
あんまりしたくない ${PRODUCT_NUMBER}
結論
- プロジェクト内で統一し、好きにすればいいんじゃない
allword
あたりで落ち着けよう
AndroidでWrite Once Test Anywhereは成功しなかった
経緯
- JetpackにRobolectricが統合されたことにより、
@RunWith(AndroidJUnit4::class)
にJUnit Runnerが統一された - うまいこと実行できれば、CIで検証しやすくなる(Instrumentation TestをCIでやるのは金か手間か実行時間が必要)
駄目だった箇所
- こういうExtensionを作って試した
// build.gradle android { sourceSets { androidTest.java.srcDirs += ["src/test/java"] } }
@RunWith(AndroidJUnit4::class) class HogeTest { @Test fun fugaTestCase() = compatibleTest { // 両方で実行 } @Test fun barTestCase() = instrumentationTest { // 実機で実行 } }
- 結論から言えば、うまくいかなかった
- 実行コード内でCoroutineがUI Dispatcherで
withTimeout()
していると、すぐさまタイムアウト扱いになる- コードを追っかけたら、Kotlin Coroutines内部でpostDelayedしているが、Robolectricは瞬時に実行するので待ち合わせがうまくいかない
- 丁寧に
InstrumentationとLocalの両実行
Instrumentationだけで実行
をうまく選択すればまあなんとか、という感じ
Android Studioの対応が不完全だった
- androidTestのsourceSetsに加えられたソースコードは、実行時に必ずInstrumentationTestが対象となる
- Run用のConfigurationを自分で書けばもちろん実行できるけど
- 毎度それを書くのは面倒
今後どうするか?
- とりあえず実行可能箇所が増えるのは嬉しいので、しばらく両実行のコードを増やすようにしてみる
AndroidでKotlin1.3-RCへの移行を試みる
早期移行する理由
- coroutinesのstable版へ移行したい
- 移行時の問題点を洗い出しておきたい
対応箇所
/build.gradle
buildscript { // 本体とcoroutinesのそれぞれのバージョン // coroutinesは標準機能に昇格したが、Android(及びJVM)用ライブラリは別途配布されている ext.kotlin_version = '1.3.0-rc-146' ext.kotlin_coroutines_version = '0.30.2-eap13' ext.army_knife_version = '0.6' repositories { google() jcenter() mavenCentral() // eap版リポジトリを追加する maven { url "https://dl.bintray.com/kotlin/kotlin-eap" } } }
/module/build.gradle
kotlin { experimental { // coroutinesはexperimentalじゃなくなった! // coroutines "enable" } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" }
ソースコード
// coroutinesはexperimentalじゃなくなった!! // import kotlinx.coroutines.experimental.* import kotlinx.coroutines.*
移行の感触
- package名の移行だけでひとまずビルドは通る
いつか書きたいMVVM/BFアーキテクチャの話
MVVM/BFによるAndroidアプリのClearn ArchitectureとDomain Driven Design導入
- 今年一番スッキリした設計パターン
- いつかアウトプットしたい
- いつだろうか?
Toc
MVVM/BF解説
- 各層の役割
- Model
- ViewModel
- JetpackのViewModelやDataBinding
- View
- Bridge
- Modelを実装した、実装・プラットフォーム依存層
- 通信をOkHttpに依存したり、Firebase SDKに依存したりする
- Factory
- BridgeとViewModelの中間を担う
- 実体(インスタンス)の正体を知っている唯一の存在
- DIライブラリを導入する唯一の層
Model層の役割
Bridge-Factoryの役割
- 各層の依存関係
- 実装はBridge層が行う
- インターフェースの振り分けはFactory層が行う
- Bridge層の公開クラスと非公開クラス
View/ViewModel層の役割
- ActivityとFragmentの役割
- FragmentからActivityへメッセージを送る
- FragmentMainとCallbackパターン
- ViewModelを使うパターンと使わないパターン
- Eventパターン
- FragmentはいつstartActivityを呼ぶか?
- 論理画面内であれば呼ぶ
- 例えばGoogle認証は同じ論理画面なのでFragmentから呼ぶ
DIライブラリに依存しないDI
- Kotlin言語機能のみでの依存注入
- Factory層で完結する依存注入
- DIライブラリの詳細をViewModel層へ伝えない
プロジェクト構成例
- 前提条件
- 強力なPCを使うことが、実現への第一歩
- 設計力矯正ギブスをつける
- domain/bridge/factory/appで分ける
- build.graldeの
implementation
とapi
を使い分ける - Kotlinの
internal
仕様を活用する - 小分けしたModuleの初期化例
共通UIウィジェットの扱い方
- 共有UIはBridge/Factoryに依存しない
- 依存しないのに、誰がModelとView/ViewModelを結ぶのか
- Callbackパターンを使う
QRコード読み取りの例
- Model層での設計
- Bridge層での実装
- Zxing実装版
- Google Play Mobile Vision実装版
- Factory層での振り分け
結合テスト
CircleCIの無料プランでメモリが足りずにgradleタスクが失敗する場合の対処
タスクを分割する
- 重いタスクがあったら、細分化してやる
# 変更前(全タスクが一度に実行) ./gradlew test
# 変更後(個別に実行) ./gradlew :app_domain:test ./gradlew :app:test
config.ymlを見直す
- メモリ量は4GB以内で調整
environment: _JAVA_OPTIONS: -Dfile.encoding=UTF-8 GRADLE_OPTS: "-Xmx3g" TZ: Asia/Tokyo
KILL THEM ALL
# タスク実行前にdaemonをKILL pkill -KILL -f java ./gradlew :app_domain:test # また実行前にdaemonをKILL pkill -KILL -f java ./gradlew :app:test
daemonなしでは生きられない
--no-daemon
オプションを指定すると、gradle-kotlinビルドプラグインがクラッシュする
daemonは醜く太る
- daemonを起動しっぱなしにすると、ビルドのたびにメモリが圧迫される
- Android Studioでも同様の現象があるので、開発中にOOMが出たら再起動してみよう。多分治る。
太ったdaemon KILL THEM ALL
pkill -KILL -f java
ですべて粛清する- また1から世界を作ろう
メモリが足りないなら
- リリカル
- トカレフ
- キルゼムオール
Kotlinの拡張関数とUtilクラスの使い分け ver 2018.09
最初にやるべきこと
拡張関数(プロパティ)を使う場合
- 単体で存続できるケースは拡張関数を許可する
- 仮にこの拡張関数が属するライブラリと離婚することになっても、この処理は容易に移行できる
// DO val Context.debugMode: Boolean get() = packageManager.getApplicationInfo(packageName, 0)?.let { appInfo -> return appInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE == ApplicationInfo.FLAG_DEBUGGABLE } ?: false if(context.debugMode) { ... }
Utilクラスにまとめる場合
- 自身のライブラリや、他のライブラリへの依存がある場合はUtilクラスにまとめる
// DO NOT fun Context.getMyInfo(): MyInfo { return MyInfo(this) } val myInfo = context.getMyInfo()
// DO val myInfo = HogeUtil.getMyInfo(context)
拡張関数(プロパティ)とクラスの使い分けに迷ったら
- 文脈として自然であると考えられるなら、拡張関数を許可する
- 拡張関数のほうがIntellisenseとの親和性が高い
同じ名前の拡張関数を見つけたら?
- 公式(もしくはメンテナンス頻度が高いもの)を優先する