Androidオールスターズ2に参加しました。

Androidオールスターズ2に参加したので、簡単なレポートとメモを残しておきます。

AndroidのCIを始めよう

レポート

まとめ

  • CIそのものに関する話と、AndroidのCIに関する発表でした。
  • iQONを運営するVASILYさんの事例紹介もありました。
  • VASILYさんの取り組みはこちらに詳しく記載されています。

Flux de Relax

発表資料と発表で使ったコード

レポート

なぜFluxか

  • Viewの状態管理が難しい
    • Fragment問題など
  • アプリがスケールした時の状態管理がつらい

Fluxの各役割

  • Dispatcher
    • ActionとStoreを結ぶ
    • シングルトンで作る
    • EventBusを使っている
    • DispatcherのメンバにEventBusを持っている
    • Mockしやすいように作る
  • Action
    • 外部リソースからデータを取得し揃ったらDispatcherに渡す
    • Rxのメリット
      • 複数リソースの合成
      • リクエストのキャンセル
  • Store
    • Storeの更新はStore内部で行う
    • Storeの状態が変更したらViewへ通知
    • DispatherへCallbackを登録する
    • ObservableXXからRxへの移行
      • ReadOnlyを制御出来ない。Rxなら出来る
      • ただしListは難しい
      • ObservableListならいい感じにやってくれる
  • View
    • StoreのStateを合成するケースがあるので、ObservableXXよりもRxが良い
  • Flux
    • View間の依存関係がない
    • 実装を統一できる
    • 単一フローでおいやすい
    • シンプルなアプリだと冗長過ぎる
    • 開放ミスでメモリリーク
    • AndroidにFluxがないのでTryError

まとめ

  • AndroidでFluxの発表でした。
  • コードも公開されていますので、発表資料と合わせて読むと理解が進むと思います。

aptとKotlinでコードを(なるべく)書かないAndroid開発

発表資料

レポート

まとめ

  • 前半apt、後半Kotlinの発表でした。
  • aptはコードを自動生成する仕組みです。
  • いずれも なるべくコードを書かない という目的のツールとして紹介されていました。

はじめよう Localization - ツール/tips/運用ノウハウなど

発表資料

レポート

  • Localization(地域化)とInternationalization(国際化)がある。
  • Internationalizationにエンジニア要素がある。
  • Localizationにエンジニア要素は少ない。
  • Internationalizationに必要なこと
    • 多言語リソースの用意
    • Formatterを使った日付や数字
    • 柔軟性の高いレイアウトの設計
  • Internationalizationに必要なことの要素
    • Strings
    • 画像
    • マルチメディア、音声、動画
    • レイアウト
  • Analyze > Run Inspection by Name でハードコードされているstringを見つけることが出来る
  • システムの言語設定を English XA にしてアプリを起動してまともに表示されているとそれがハードコードされているとわかる。
    • EnglishXAは特殊な文字列リソースを使い、30%程の文字を追加する特殊な言語
  • strings.xmlプレースホルダーを設定できる。
    • <string name="buyitems">Buy %1$d %2$s</string>
    • 上記のように値を外から設定できる string を定義できる。
  • もっと使いやすいプレースホルダー Pharaseというライブラリがある。
  • 複数形は plurals を使う
  • 日付・時刻
    • DateFormat
    • API18からは getBestDateTimePatternという便利なメソッドがある
    • DateUtilsもある
      • getDateとgetDateRnage
  • 通過、数値
    • NumberFormat
  • 電話番号
    • PhoneNumberUtils
    • API21以前は、英語日本語以外はあてにならない
  • 画像
    • 文字つきの画像は避ける
  • 柔軟性の高いレイアウト
    • 英語の30%増しくらいで考える。日本語は文字数少ない。

翻訳管理

  • 翻訳に必要なコンテキスト(どういうケースで使われるStringなのか)をXMLのコメントで付与する。
  • 翻訳不要なStringをマーク。Androidが用意している。
  • 翻訳管理サービス
  • 翻訳作業も自動化。
  • エンジニアがStringリソースを追加する。
  • CIが翻訳管理サービスに投げる。
  • アプリに翻訳に協力するボタン用意すると結構協力してくれる。

まとめ

  • Internationalization(Localization)に関する発表でした。
  • AndroidではInternationalizationのためのフレームワークがたくさん用意されており、知らないこともたくさんあったので非常に勉強になりました。

設計のお話 MVPとDDDで

発表資料

レポート

  • 設計:イメージしたものを実現するための見通しを立てる行為
  • プログラミングの設計:上の定義に加え、システムには完了が変更への強さも求められる
  • VとCの責務を持った、Activity/FragmentからイベントをPresenterとして外出しするためにMVP
  • スレッド管理はアプリケーションレイア

まとめ

  • AndroidでMVP/DDDの発表でした。
  • DDDからはドメインモデルとレイヤードアーキテクチャを採用していました。
  • MVPですが、AndroidのDatabindingが便利だということで、ViewModelを用意し、Viewにバインドしていました。
  • Android + MVP + DDD の実践的な設計が紹介されていたと思います。

Android初心者を脱するための20のチップス

発表資料

レポート

Javaのチップス

  • アクセス修飾子を適切に
  • static修飾子
    • staticメソッドにしておけばテストコードを書きやすい
  • final修飾子
    • 変数、クラス、メソッド
  • コメント
  • アノテーション
  • Non-primitive は @Nullable を指定する
  • ジェネリクス

Androidのチップス

  • 適切なViewを使う
  • Viewの階層は浅く
    • FrameLayout
    • テーブルっぽいレイアウト
      • NG: LinearLayout
      • OK: GridLayout
  • 関係ないView属性を書かない
  • ViewもDRY
    • <include>
    • <merge>
  • toolsを使う
    • tools:layout_heightで指定してpreviewで確認できる
    • tools:contesxtでActivityを指定しThemeを適用する
    • tools:layoutでFragmentのレイアウトを指定する
  • lintを確認
  • proguardをかける
  • 依存ライブラリは外部モジュールで
  • ディスプレイサイズはResourcesから取得
  • TextUtilsを使う
  • タスクは積み直せる
    • Activityのタスク
  • static 生成メソッド
  • フォーマットかけよう
    • Alt + Command + l
    • フォーマット修正diffがあるとレビューつらい

まとめ

  • 今日から使えるチップスの発表でした。

全体を通して

Androidに関係する様々なジャンルの話を聞けて、非常に勉強になりました。

Kotlin sealed class パターンマッチの挙動

sealed class を when でパターンマッチした際に、想定外の挙動をしていたのでいろいろ検証してみました。

網羅チェック

網羅チェックが動きそうで動かなかったケース

  • ビルドエラーになりません。
sealed class Event {
    class Insert: Event()
    class Update: Event()
    class Delete: Event()
}

fun hoge(event: Event) {
    
    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Update -> { print("Update") }
//        is Event.Delete -> { print("Delete") }
    }
    
}

網羅チェックが動くパターン1

  • ビルドエラーになります。
sealed class Event {
    class Insert: Event()
    class Update: Event()
    class Delete: Event()
}

fun hoge(event: Event) {

    val x = when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Update -> { print("Update") }
//        is Event.Delete -> { print("Delete") }
    }

}

網羅チェックが動くパターン2

  • ビルドエラーになります。
sealed class Event {
    class Insert: Event()
    class Update: Event()
    class Delete: Event()
}

fun hoge(event: Event) {

    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Update -> { print("Update") }
//        is Event.Delete -> { print("Delete") }
    }.let {  }

}

参考にしたページ

ネストしたクラス

下記のようなページも有ったので検証してみました。

  • ビルドエラーになります。
sealed class Event {
    class Insert: Event() {
        class Single: Event()
        class Multiple: Event()
    }
    class Update: Event()
    class Delete: Event()
}

fun hoge(event: Event) {

    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Insert.Multiple -> { print("Multiple Insert") }
//        is Event.Insert.Single -> { print("Single Insert") }
        is Event.Update -> { print("Update") }
        is Event.Delete -> { print("Delete") }
    }.let {  }

}

複雑なクラス継承

継承した時の挙動が気になったので検証してみました。

sealed class の inner class を外で継承

  • ForceDeleteをwhenの条件に加えなくてもビルドエラーになりません。
  • ForceDelete の場合 Delete にマッチします。
sealed class Event {
    class Insert: Event()
    class Update: Event()
    open class Delete: Event() // 継承できるように open を指定する。
}

class ForceDelete: Event.Delete()

fun hoge(event: Event) {

    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Update -> { print("Update") }
        is Event.Delete -> { print("Delete") }
    }.let {  }

}
hoge(ForceDelete()) // Delete

sealed class の inner class を sealed class 内で継承

  • Deleteをwhenの条件に加えなくてもビルドエラーになりません。
  • hoge(ForceDelete()) では Delete が実行されます。
  • 1つ前の外で継承したケースと同じ挙動です。
sealed class Event {
    class Insert: Event()
    class Update: Event()
    open class Delete: Event()
    class ForceDelete: Event.Delete()
}


fun hoge(event: Event) {

    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Update -> { print("Update") }
        is Event.Delete -> { print("Delete") }
    }.let {  }

}
hoge(ForceDelete()) // Delete

sealed class の inner class を継承した場合の when の評価を検証

  • 次のコードでは Delete は Update にマッチします。
sealed class Event {
    class Insert: Event()
    open class Update: Event()
    class Delete: Event.Update()
}


fun hoge(event: Event) {

    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Update -> { print("Update") }
        is Event.Delete -> { print("Delete") }
    }.let {  }

}

hoge(Event.Delete()) // Update
  • 次のコードでは Delete は Delete にマッチします。
sealed class Event {
    class Insert: Event()
    open class Update: Event()
    class Delete: Event.Update()
}


fun hoge(event: Event) {

    when(event) {
        is Event.Insert -> { print("Insert") }
        is Event.Delete -> { print("Delete") }
        is Event.Update -> { print("Update") }
    }.let {  }

}
    

hoge(Event.Delete()) // Delete

この通り、When-isの順番により処理が変わってしまうため、 sealed class の inner class を open にして継承することは避けるべきです。

最後に

いくつか微妙だなと感じる仕様があったので以下の仕様変更があればなと思います。

  • whenで評価した値を使わなくてもsealed classの網羅チェックが働くようにする。
  • sealed class の inner class は open にできないようにする。

こういうのどこでリクエストすればいいのでしょうか。

Kotlin + DataBinding + RecyclerView のサンプル。

標題のサンプルが見つからなかったので作りました。 github.com

このコードを元に、第3回Kotlin勉強会 @ Sansan - connpassでLTしてきたのですが、

このようなフィードバックを頂きましたので、合わせて参考にして頂ければと思います。

またLTで使った資料はこちらです。 speakerdeck.com

Android の Fragment で出来るを余白を消す

Androidアプリ開発でFragmentを使用した際に画面の上下左右に余白が出来てしまいました。
こちらのページに記載されている通り、 res/values/dimens.xml の設定を下記の通り変更することで余白が消えました。

<dimen name="activity_horizontal_margin">0dp</dimen>
<dimen name="activity_vertical_margin">0dp</dimen>

デフォルトでは 0dp のところに 16dp が指定されていました。

Android の Minimum Required SDK / Target SDK / Compile With

Android ではSDKの指定がいろいろあり、それぞれについて紹介します。

Minimum Required SDK

アプリがサポートする最低レベルのSDKを選択します。
ここで選択したバージョンより低いOSではアプリがインストールできません。

バージョンの選び方。バージョンアップ。

バージョンが低いと開発工数が増えますが、高くするとアプリを提供できる範囲が狭くなるため、
工数と端末シェアのバランスを見てバージョンを選びます。
端末シェアが低くなったバージョンはサポート外にし、バージョンを上げていくと良いと思います。

Target SDK

アプリをテストするプラットフォームのSDKを選択します。
Android SDK の過去のバージョンの SDK のアプリを実行できる機能があるため、ここで選択したものよりも新しいSDKの端末でアプリを実行することが出来ます。

バージョンの選び方。バージョンアップ。

リリース前は最新バージョンを選択すれば良いと思います。
Google公式には最新にするように書かれていますが、SDKのバージョンアップによりバグが発生しないとも限らないので慎重に更新を行う必要があると思います。
こちらのバージョンを上げる際は一緒に Compile With のバージョンを上げると良いと思います。

SDKに新たな機能が実装されたら?

新たな機能が実装された場合は、バージョンを上げなければならないかもしれませんが、サポートライブラリの提供がある場合はバージョンを上げずにそちらを利用する選択肢もあります。
SDKバージョンを上げる場合はアプリ全体をテストし直すようにしましょう。

Compile With

アプリをビルドするコンパイラSDKを選択します。

バージョンの選び方。バージョンアップ。

こちらは Target SDK と同じで、リリース前は最新バージョンを選択すれば良いと思いますが、SDKのバージョンアップによりバグが発生しないとも限らないので、リリース後は慎重に更新を行う必要があると思います。
先程も記載しましたが、こちらのバージョンを上げる際は一緒に Target SDK のバージョンを上げると良いと思います。

番外編

SDKバージョン20から提供された機能をSDKバージョン10の端末でも使えるか?

サポートライブラリが提供される場合はそちらを使い、SDKバージョン10の端末でも使えます。
サポートライブラリがない場合は頑張って自作することになります。

参考にしたサイト

KotlinのExtensionでDataBindingのBindingAdapterを使う。

本記事では DataBinding の方法については省略します。
DataBinding の方法については以下の記事を参照下さい。

BindingAdapterとは?

カスタムセッターを定義するものです。
BindingAdapterアノテーションを付けた関数を定義すると、その関数をレイアウトのxmlから参照できます。 Javaでの関数の定義は以下のとおりです。

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}

xml側での呼び出しは以下のとおりです。

    <TextView
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"
        android:id="@+id/firstNameTextView"
        android:paddingLeft="@{1000}"
        />

android:paddingLeft="1000" ではなく android:paddingLeft="@{1000}" と設定する点に注意してください。

参考にしたページ

KotlinでのBindingAdapterを定義する。

※オススメしない方法ですが検索するとこの方法がヒットするので記載します。
Kotlin で Jave のような staticメソッドを作るには companion object を使うようですが、BindingAdapter で companion object を使うとエラーになります。
そこで以下の通りobject指定でクラスを作り、実装します。

object CustomBindingAdapter {
    @BindingAdapter("android:paddingLeft")
    @JvmStatic
    fun setPaddingLeft(view: View, padding: Int) {
        view.setPadding(padding,
                view.paddingTop,
                view.paddingRight,
                view.paddingBottom)
    }
}

xml側の変更はありません。

参考にしたページ

KotlinのExtensionsを使う。

上記の通りで実装できますが、Kotlin で BindingAdapter を使う場合は、Extension を使えばいいよね。という投稿をいくつか見ました。

Extension を使った実装は以下のとおりです。

@BindingAdapter("android:paddingLeft")
fun View.setPaddingLeft(padding: Int) {
    this.setPadding(padding,
            this.paddingTop,
            this.paddingRight,
            this.paddingBottom)

}

こちらの方が余計な object 宣言のなく簡潔に実装できます。

なぜこれで動くのか?

Kotlin の Extension は Java の staticメソッドに展開されます。
展開されたコードが最初に紹介した Java での関数定義と同じになるため、 Kotlin の Extension で BindingAdapter を定義できます。

参考にしたページ

Kotlinプロジェクト(Android Studio 2.1.1) に Crashlytics を導入

Kotlin で Crashlytics が正常動作検証した際に、build.gradleの修正に手間取ったのでメモしておきます。
手間取ったのはおそらく Kotlin が原因ではなく、 Android Studio の build.gradle の構成が変わってことが原因です。

  • build.gradle(Project)
buildscript {
    ext.kotlin_version = '1.0.2'
    repositories {
        jcenter()
        maven { url 'https://maven.fabric.io/public' } // この行を追加。
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'io.fabric.tools:gradle:1.+' // この行を追加。

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
  • build.gradle(Module)
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'io.fabric'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        applicationId "com.example.firstkotlin"
        minSdkVersion 17
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.4.0'
    compile 'com.android.support:design:23.4.0'
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

    // 以下の3行を追加
    compile('com.crashlytics.sdk.android:crashlytics:2.5.5@aar') {
        transitive = true;
    }
}

repositories {
    mavenCentral()
    maven { url 'https://maven.fabric.io/public' } //この行を追加
}

Buildが失敗する場合は一度 Android Studio を再起動してみてください。
私は再起動後、正常に動作するようになりました。