eaglesakuraの技術ブログ

技術的な話題とか、メモとか。

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解説

  1. 各層の役割
  2. Model
    1. ドメインサブドメイン、Serviceなど、プラットフォーム非依存コード
  3. ViewModel
    1. JetpackのViewModelやDataBinding
  4. View
    1. Androidのシステムコンポーネント(Activity, Service, ContentProvider...)
  5. Bridge
    1. Modelを実装した、実装・プラットフォーム依存層
    2. 通信をOkHttpに依存したり、Firebase SDKに依存したりする
  6. Factory
    1. BridgeとViewModelの中間を担う
    2. 実体(インスタンス)の正体を知っている唯一の存在
    3. DIライブラリを導入する唯一の層

Model層の役割

  1. DDDを読もう
  2. 命名の気になるところ
  3. Service? それともService?
    1. AndroidのServiceと標準的な名付け(サフィックス)が競合する
    2. 気にしない

Bridge-Factoryの役割

  1. 各層の依存関係
  2. 実装はBridge層が行う
  3. インターフェースの振り分けはFactory層が行う
  4. Bridge層の公開クラスと非公開クラス

View/ViewModel層の役割

  1. ActivityとFragmentの役割
  2. FragmentからActivityへメッセージを送る
  3. FragmentMainとCallbackパターン
  4. ViewModelを使うパターンと使わないパターン
  5. Eventパターン
  6. FragmentはいつstartActivityを呼ぶか?
  7. 論理画面内であれば呼ぶ
  8. 例えばGoogle認証は同じ論理画面なのでFragmentから呼ぶ

DIライブラリに依存しないDI

  1. Kotlin言語機能のみでの依存注入
  2. Factory層で完結する依存注入
  3. DIライブラリの詳細をViewModel層へ伝えない

プロジェクト構成例

  1. 前提条件
  2. 強力なPCを使うことが、実現への第一歩
  3. 設計力矯正ギブスをつける
  4. domain/bridge/factory/appで分ける
  5. build.graldeの implementationapi を使い分ける
  6. Kotlinの internal 仕様を活用する
  7. 小分けしたModuleの初期化例

共通UIウィジェットの扱い方

  1. 共有UIはBridge/Factoryに依存しない
  2. 依存しないのに、誰がModelとView/ViewModelを結ぶのか
  3. Callbackパターンを使う

QRコード読み取りの例

  1. Model層での設計
  2. Bridge層での実装
  3. Zxing実装版
  4. Google Play Mobile Vision実装版
  5. Factory層での振り分け

結合テスト

  1. Bridge層での単体テスト
  2. Factory層での結合テスト
  3. View/ViewModel層での結合テスト

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

最初にやるべきこと

  • Jetpack-KTXを探す
  • 公式拡張関数を探す

拡張関数(プロパティ)を使う場合

  • 単体で存続できるケースは拡張関数を許可する
  • 仮にこの拡張関数が属するライブラリと離婚することになっても、この処理は容易に移行できる
// 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との親和性が高い

同じ名前の拡張関数を見つけたら?

  • 公式(もしくはメンテナンス頻度が高いもの)を優先する

kotlin-coroutines 0.26.1での破壊的更新

coroutines 0.26.1の変更点

  • 多くのクラスやトップレベルfunctionやプロパティがdeprecatedになった

変更から見える方針

  • トップレベルの関数やプロパティを、いずれかのobject等に所属させることが主な目的に見える

主なDeprecated

  • UI, CommonPool 等の標準Dispatcherが非推奨
    • Dispatchers.Main, Dispatchers.Default が追加された
  • launch{}, async{} 等のトップレベル関数が非推奨
    • GlobalScope.launch, GlobalScope.async が追加された
  • isActive が拡張関数になった
    • ビルドが通らないので、ガイドに従って書き換える

互換性

  • Deprecated属性になっただけで、0.24のときのような内部の破壊的仕様変更はされていない
  • 様子見しつつ、移行して行こう。

移行時の注意

  • GlobalScope.launchCoroutineScope.launch を使い分ける
    • CoroutineScope.thisが届く範囲のlaunch/async等はそのまま動作する
    • 「とりあえずlaunchって書けば非同期になるよ」みたいなことが言えなくなる
  • Android Studioの場合、 Alt + Enter で移行先を示してくれる
    • UIDispatchers.Main にしてくれたり、いろいろ
    • デフォルト引数が設定されている場合、勝手にデフォルト値が転記されてしまうので、全自動は無理
    • CommonPoolは補完されないので手動で Dispatchers.Default にする
  • withContext{} で渡されるthisのスコープが変わった
    • ブロック内でフィールド変数にアクセスしてる場合、スコープ範囲外でエラーになる
    • this@HogeFuga.fieldName = Hoge みたいに書き換える

悲観的推測

  • GlobalScopeの場合とCoroutineScope.thisのLaunchでJobの親子関係が異なるかもしれない?
    • ちゃんと移行しないと、 job.cancel() の挙動とかに影響あるかも
  • 0.24 -> 0.25のときのように破壊的変更が次期バージョンでRevertされる恐れもある

Google Play Services Gradle Plugin 4.1.0の問題点

既知の問題点

  • Gradle Pluginバージョンは com.android.tools.build:gradle:3.3.0-alpha10
  • google-services.jsonを検索しない
    • 考えうる限りいろんなpathにおいたけど反応なし
    • ログも --debug オプションで見たが吐き出されない
  • 完全にnot workingらしい
  • 4.0.2は期待通り動作する

諦めた

  • アップデートに期待する

AS3.3 canary-11の問題点

問題点

  • Find Action(Ctrl + Shift + A)が正常動作しない
  • たまに正常動作する
  • だいたい正常動作しない
  • Crash Reportが吐き出されてるので、どっかのPluginと競合している
    • Reportは送った
  • 以前はFlutter Pluginと競合していたので、uninstall済み
  • Dartも怪しかったのでuninstallしたが、改善されず

諦めた

  • C12に期待する