eaglesakuraの技術ブログ

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

GolandでGAE/Goのコードにブレークポイントを貼る

for Ubuntu

  • root権限で /etc/sysctl.d/10-ptrace.conf を編集して、 kernel.yama.ptrace_scope = 0 に変更 -> 再起動
  • あとは普通にdebugアイコンでApp Engineを起動するとブレークポイントで止まる
    • さすがに init() は止まらなかった

Kotlin Android-Parcelize拡張の後方互換問題を考える

Parcelize拡張概要

  • Androidアプリ開発者にとってメンドウな Parcelable の定形実装を自動化できる
  • Annotationをつけるだけで、フィールドを全部Parcelableに突っ込んでくkれる
    • 突っ込めないオブジェクトは警告が出る
    • Annotationで除外を指定できる

Example

// これだけでParcelableを実装できる
// BundleにもIntentにも突っ込める
@Parcelize
class ExampleUser(
    val name: String
) : Parcelable

何が問題か?

  • 生成されたオブジェクトをアプリ間で受け渡す際に、シリアライズ・デシリアライズの問題が発生する恐れがあるのでは?と考えた
  • フィールドが増減した時、フィールドの記述順が変更になった時、コイツは死ぬのでは?
  • 例えば、自社のアプリ間で通信したい場合とか。

問題のある例

// アプリAではこのバージョンのクラスを使用している
// ライブラリVersion 1.0
@Parcelize
class ExampleUser(
    val name: String
) : Parcelable
// アプリBでは、ちょっと機能が増えた
// ライブラリVersion 1.1
@Parcelize
class ExampleUser(
    val name: String,
    val age: Int // 年齢フィールドを増やしたぞ
) : Parcelable
  • この状態で、アプリAからアプリBにデータを渡すとどうなる?
  • 多分死ぬ

現状を確認する

生成されるClassをjad

  • こんなCreatorが自動生成される
  • in.readString() している
public final class ExampleUser
    implements Parcelable
{
    public static class Creator
        implements android.os.Parcelable.Creator
    {

        public final Object[] newArray(int size)
        {
            return new ExampleUser[size];
        }

        public final Object createFromParcel(Parcel in)
        {
            Intrinsics.checkParameterIsNotNull(in, "in");
            return new ExampleUser(in.readString());
        }

        public Creator()
        {
        }
    }
}

フィールドを増やすとどうなるか?

  • 案の定、順番通りにreadする
  • このタイミングでデータ長が足りずに壊れる
        public final Object createFromParcel(Parcel in)
        {
            Intrinsics.checkParameterIsNotNull(in, "in");
            return new ExampleUser(in.readString(), in.readInt());
        }

順番が変わるとどうなる?

  • 当然、順番が変わる
  • Classは死ぬ
        public final Object createFromParcel(Parcel in)
        {
            Intrinsics.checkParameterIsNotNull(in, "in");
            return new ExampleUser(in.readInt(), in.readString());
        }

どうするか?

  • 外見仕様を変えずに、互換性を何とかする
  • Parcelable互換で、高機能な Bundle とカスタムプロパティでなんとかする
  • bundle.get をすることで、古いデータ仕様の場合はdefaultを詰め込むことができる
@Parcelize
class ExampleUser internal constructor(
    private val bundle: Bundle
) : Parcelable {

    constructor(name: String, age: Int) : this(
        bundleOf(
            Pair("name", name),
            Pair("age", age)
        )
    )

    val age: Int
        get() = bundle.getInt("age", -1)

    val name: String
        get() = bundle.getString("name")!!
}
// jadるとこんな感じになる

    public final int getAge()
    {
        return bundle.getInt("age", -1);
    }

    public final String getName()
    {
        String s = bundle.getString("name");
        s;
        if(s == null)
            Intrinsics.throwNpe();
        return;
    }

    public ExampleUser(Bundle bundle)
    {
        Intrinsics.checkParameterIsNotNull(bundle, "bundle");
        super();
        this.bundle = bundle;
    }

    public ExampleUser(String name, int age)
    {
        Intrinsics.checkParameterIsNotNull(name, "name");
        this(BundleKt.bundleOf(new Pair[] {
            new Pair("name", name), new Pair("age", Integer.valueOf(age))
        }));
    }

Bundle in Bundleすると死ぬ

単純に実装すると、 Bundle.putParcelable() で突っ込んだBundleが死ぬ。 復元時に ClassLoader が読み込まれていないようだ。

なので、次のようにするとClassLoaderを解決できる

@Parcelize
class ExampleUser internal constructor(
    private val bundle: Bundle
) : Parcelable {
    init {
        // class loaderを指定して復元させる
        bundle.classLoader = javaClass.classLoader
    }
}
  • 実装はモノグサしたので、あとは interface なり abstract なりで隠蔽してキレイに見せかけてあげれば手抜きで後方互換もある程度担保できるんじゃないかな

Androidアプリのmicro-module設計

  • サーバーサイドの世界では、micro-servicesとか流行ってるね
  • Androidの世界で、microな設計思想は取り入れられるのか?

Androidアプリのmoduleはどこまで分けられるのか?

  • Domain
  • Bridge
    • ドメインで定義したServiceの実装詳細を書く
  • Factory
    • どのブリッジを使用するか選択する
    • SQLite使う? Realm使う? そういう切り分けを行う
  • Components
  • Application
    • 最終的な成果物作成モジュール
    • google-services.jsonを組み込んだり、Crashlyticsを組み込んだり
    • Flavorでapkを分けたりする

Componentsをどのように分けるか

Activityをinternal化する

  • もちろん、関連するFragmentも全部internalにする
// こうしたい
// こうすることで、画面設計の詳細が[app_components_splash]モジュールから漏れ出さない
internal class SplashActivity : Activity() {
}
  • Kotlinコードからアクセスできないだけで、AndroidManifestに書けばちゃんと動作する

Activity間の遷移をどうするか?

  1. [app]
  2. [app_components_splash]
  3. [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

play.google.com

有名そうな、allword

play.google.com

まさかの CammelCase

play.google.com

あんまりしたくない ${PRODUCT_NUMBER}

play.google.com

結論

  • プロジェクト内で統一し、好きにすればいいんじゃない
  • allword あたりで落ち着けよう

AndroidでWrite Once Test Anywhereは成功しなかった

経緯

  • JetpackにRobolectricが統合されたことにより、 @RunWith(AndroidJUnit4::class)JUnit Runnerが統一された
  • うまいこと実行できれば、CIで検証しやすくなる(Instrumentation TestをCIでやるのは金か手間か実行時間が必要)

駄目だった箇所

  • こういうExtensionを作って試した

github.com

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

  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層での結合テスト