kobakei's blog

たまに書きます

DroidKaigi 2018でリジェクトされたネタ

DroidKaigi 2018で、「開発者が知っておきたい通知の歴史」というセッションが採択されました。通知のAPIや仕様の変遷を辿り、後方互換姓を考慮した実装をするにはどんな風に書けば良いのかを紹介するセッションです。特に最近Android開発を初めて昔の通知を知らない人に是非聞いてもらいたいと思っています。

一方で、他に応募していた3本のセッションがリジェクトされたので、どんな内容を話すつもりだったかを備忘録的に残しておきます。今後shibuya.apkなど他の勉強会で機会があればショートバージョンにして話すかもです。

データバインディングをもっと使いこなそう

データバインディングの初心者や基本的な使い方しか知らない人向けに、実践的な使い方を紹介する予定でした。具体的には、以下の様な内容をカバーします。

  • BindingAdapter & InverseBindingAdapter
  • ObservableList と RecyclerView
  • アニメーション関連。特にOnRebindCallback
  • MVVMパターン with data binding

過去発表した以下のセッションの内容+αなイメージでした。気になる方はこちらのスライドも見てください。

speakerdeck.com speakerdeck.com

Androidアプリ開発をラクにするCI/CD

今の会社で実戦投入しているCI/CDの事例や小技を紹介するセッションの予定でした。CircleCI Meetupで発表したこのスライドをベースにするつもりでした。

speakerdeck.com

バイスファームのセッションを除くと、CI/CD系のセッションは今回一件も採択されていなかったので需要がなかったのかも?一昔前に比べると情報が見つけやすい気はしますね。

Robolectricで単体テストを書けるようにするには

Robolectricそのもののセッションではなく、Robolectricでテストを書きやすい状態にするための設計のセッションの予定でした。DIの導入やMVWパターン、staticメソッドの潰し方やテストしづらい外部SDKFacebook SDKなど)のラッパーを書く、などの話をする予定でした。改めて応募を見返すと内容が薄そうに見えたので、どこかの勉強会でLTで話せばちょうどいい分量のような気がしました。

総括

結果1勝3敗ということで、DroidKaigi全体の採択率通りの結果でした。来年はもうちょい数を絞って練った応募を出せるようにしたいです。

Kotlin 1.1.4のParcelizeを試してみた

Kotlin 1.1.4でParcelableがサポートされたので、移行してみました。

注意:ParcelableサポートはまだExperimentalなので、仕様が変わる可能性があります。

導入

app/build.gradleに以下を追加します。

...
apply plugin: 'kotlin-android-extensions'

android {
    ...
    androidExtensions {
        experimental = true
    }
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.4-3"
    ...
}

Entity classをParcelableを継承するようにし、@Parcelizeアノテーションを追加します。

@Parcelize
data class Hoge constructor(
    val foo: String,
    val bar: String
) : Parcelable

あとはビルド時にCREATORが自動生成されます。便利ですね。

問題: Dateが使えない

プロパティにDateがあるとビルドエラーが発生しました。例えばこんな感じのクラス。

@Parcelize
data class Hoge constructor(
    val foo: String,
    val bar: String,
    val createdAt: Date
) : Parcelable

DateクラスはSerializableなクラスなので、Parcelableに突っ込めるはずなんですが謎。 Parcelerjohncarl81/parcelerとは別物)というシリアライズ/デシリアライズを自分で書くためのインタフェースが提供されているので、companion objectに継承させて解決できます。

// Dateをread/writeするための拡張関数
fun Parcel.writeDate(date: Date) {
    writeLong(date.time)
}
fun Parcel.readDate(): Date {
    return Date(readLong())
}

@Parcelize
data class Hoge constructor(
    val foo: String,
    val bar: String,
    val createdAt: Date
) : Parcelable {

    companion object : Parceler<Hoge> {
        override fun create(parcel: Parcel): Hoge {
            return Hoge(
                        parcel.readString(),
                        parcel.readString(),
                        parcel.readDate()
            )
        }
        override fun Hoge.write(parcel: Parcel, flags: Int) {
            parcel.writeString(foo)
            parcel.writeString(bar)
            parcel.writeDate(createdAt)
        }
    }

}

Parcelerを用意するのめんどくさいので、そのうち修正してほしいところですね。

問題: Proguard有効時にビルドエラー

Progaurdを有効にしてビルドするとビルドエラーが出ました。こちらのチケットのコメントにある設定を足すととりあえず解決できます。

-dontwarn my.package.entity.**

johncarl81/parcelerから移行してみて

johncarl81/parcelerだとEntity自体はParcelableにはならなかったため、Bundleに詰めるときなどは必ずParcels.wrapでParcelableに変換してから詰めてたんですが、KotlinのParcelizeだと変換が不要なので、ActivityやFragmentでParcelableを扱うときは大分見やすくなると思います。

一方で、Parcelizeでは本来そのまま扱えるはずのDateが上手く扱えないなど、使いづらい点が残っていると感じます。この辺はまだExperimentalということで、今後改善されていくことを期待しています。

追記: Jelly Beansでインストールできなくなる模様

https://youtrack.jetbrains.com/issue/KT-20034

まだ本番投入するのは早そうです。

CircleCI Japan Meetup: Mobile Editionに参加してきた

CircleCI主催のミートアップで、「Everything of CI/CD in Kyash Android」という内容で発表してきました。

speakerdeck.com

以前potatotipsでCirlceCI 2.0について発表したスライドをCirlceCIの中の人に見つけていただいて、発表しないかと声をかけていただきました。 どんな雰囲気か、どんな人が来るのか全く読めなかったので、AndroidのCIやCDについて広く浅く網羅した内容にしました。 何人かお話させていただいた感じ、AndroidiOS以外のエンジニアが「モバイルのCIてどんな感じなのか気になる」という動機で参加されてる人も多そうだったので、結果的には発表内容は合ってたんじゃないかと思いました。

以下、スライドには載ってない口頭で補足した内容など。

Jacoco

Jacocoでコードカバレッジ出力を導入したんですが、カバレッジのHTMLをCircleCIのアーティファクトにコピーするのに1~2分かかってたので辞めました。 Coverallsみたいなサービスにレポート送信した方が正しそうですが、無料で使えなさそうなので一旦諦めました。

UIテスト

UIテストは登録フローなどデグレが発生すると致命的なフローについてのみ書いています。ので、単体テストと違ってgit pushのたびには走らせていません。リリース前にFirebase Test Labを手動で走らせていますが、CircleCI 2.0から実行できればリリースブランチにpushしたときに自動でFirebase Test Labを実行するとかしたいですね。CircleCI 2.0からFirebase Test Labを実行する方法はまだ未検証ですが、Circle CI 1.0のドキュメントしか現状ないのでドキュメント待ちになりそうです。

CircleCI 2.0でgit tagトリガーでジョブ実行

半月ほど前に試した時はまだ動いてなかったんですが、中の人が「それ先週くらいから直ってます」っておっしゃってたので今なら動いているかもです。 検証したら追記します。

(追記 2017/08/28) ↑動いてなかった(´・ω・`)

端末に画像を保存するときにどこに保存するか

SNSやメッセージアプリでは、友人から送信されてきた写真を端末に保存する機能が付いてることが多いと思いますが、端末のどこに画像を保存するのが一般的なのかを調べたのでその備忘録です。

そもそもの仕様

Environmentクラスには、用途ごとの保存先パスの定数が用意されています。その中で、画像に使えそうなのは以下かと思います。

  • DIRECTORY_DCIM
  • DIRECTORY_DOCUMENTS
  • DIRECTORY_DOWNLOADS
  • DIRECTORY_PICTURES

PICTURESはおそらくベストな保存先でしょう。ここはユーザーが自由に使える画像を置く場所で、メディアスキャナーがこのフォルダの中を自動でスキャンしてくれます。(メディアスキャンが走らないと、ギャラリーアプリなので画像を見ることが出来ません)

DCIMは、端末をカメラとしてマウントするときの画像や動画の保存先とドキュメントに書いてあるので、カメラアプリが撮影した写真を保存するときに使うものです。実際端末標準のカメラアプリで撮影した写真はここに保存されていました。

DOCUMENTSは、ユーザーによって生成されたドキュメントの保存先です。画像以外のファイルに使うのかなーという印象です。

DOWNLOADSは、ユーザーがダウンロードしたファイルを保存する場所です。意外に使われてませんでした。Chromeはここに画像を保存しますが、Chromeは画像以外にもPDFなどもダウンロードするので、PICTURESではなくDOWNLOADSに保存しているのだと推測しています。

各アプリの実装

※僕の端末(Huawei mate 9)のファイルエクスプローラで確認

LINE

/Pictures/LINE に保存されます

Twitter

/Pictures/Twitter に保存されます

Facebook

Facebookは変な実装で、

  • タイムラインの写真を端末に保存する時はDCIM
  • 自分が投稿するときの下書きの写真はPictures

という実装でした(正しくは逆じゃね?)

Instagram

友達の写真を保存する機能はなかった。。。

自分で投稿する写真は /Pictures/Instagram に保存されました。

Chrome

  • web上の画像を保存すると、Download直下に保存されました

まとめ

Pictures/<アプリ名> に保存するのが無難。

Androidアプリのパッケージ構成の個人的ベストプラクティス

以前Androidのパッケージ構成について考えてみたという記事を書いたんですが、書いたときから結構時間が経って考え方が変わった箇所もあるので、再度まとめようと思います。

前提

アプリケーションのアーキテクチャには、MVVM with data bindingを採用しています。また、モデルは階層化アーキテクチャ(っぽい何か)を採用しています。

結論

こんな感じになりました。ここでは以下で説明する順に並べていますが、Android Studio上ではアルファベット順に並びます。

com.example.myapp
|- di
|  |- scope
|
|- model
|  |- entity
|  |- usecase
|  |- repository
|  |- dao
|  |- net
|
|- ui
|  |- base
|  |- util
|  |- view
|  |- foo
|  |- bar
|  |- ...
|
|- service
|
|- receiver
|
|- App.java

各パッケージ

di

Dagger 2のComponent / Moduleをここに置きます。

scope

DaggerのScopeアノテーションを自作したときはここに置きます。

model

Modelの各クラスがここに入ります。

entity

データオブジェクト

usecase

ビジネスロジックを表すクラス。1つの処理で1クラス。例えば、「サインインする」というビジネスロジックには、SignInUseCaseというクラスを作成します。

repository

オブジェクトのCRUDを提供するクラス。対応するエンティティごとに1クラス。後述のdaoとnetパッケージのクラスを抽象化するクラスです。

dao

データベース(主にSQLite)にクエリを投げてレコードを取得し、エンティティに変換するクラス。もちろんその逆(データベースへのWrite)も行います。

net

サーバーと通信してレスポンス(主にJSON)を取得し、エンティティに変換するクラス。もちろんその逆(POST / PUT / DELETEメソッドのAPIコール)も行います。

ui

UIそのものと、UIに関連するロジックをここに書きます。

MVVMの場合、VとVMをここに書いていきます。なぜビューとビューモデルでパッケージを分けないかというと、大体のビューは対応するビューモデルがいるので、パッケージを同じにしておいたほうが開発するときに作業しやすく、あとから見やすいためです。同じ理由で、Activity、Fragment、Adapterも同じパッケージに置きます。ちなみに、モデルは複数の画面から呼び出されることが多いので、独立したmodelパッケージ以下に配置しています。

base

BaseActivityやBaseFragmentなど、ベースクラスを置きます。

util

ビューやビューモデルで共通に利用するユーティリティを置きます。Data bindingのBindingAdapterなどもここに置きます。

view

カスタムビューをここに書きます。

それ以外

画面単位でパッケージを作成します。例えば、ユーザー登録画面ならsignup、商品の購入画面ならpurchaseなど。その画面のActivity, Fragment, ビューモデルがすべてそこに入ります。

service

AndroidのServiceをここに書きます。model以下でもいいような気もしますが、ライフサイクルを持つという点ではActivityに近いので、あえて独立させました。

receiver

AndroidのBroadcastReceiverを書きます。大体上に同じです。

アプリケーションクラス

これはアプリケーションのパッケージの直下に置きます。

Android開発でRxJavaを使うと嬉しいこと

RxJavaをAndroid開発で使い始めてしばらく経ったので、RxJavaを使うと何がどう変わったのかをまとめます。

※RxJavaを使ったことがない人向けの戯言です。

非同期処理の待ち合わせが書きやすい

よくある例として、2つのREST APIを並列実行して、両方の結果が帰ってきてから画面を更新する、みたいな例を考えます。

これをRxJavaを使わずに普通に書くと、

  • それぞれのAPIごとにスレッドを作る
  • CountDownLatchを使って、両方のスレッドの完了を待ち合わせる

という風になります。結構つらいです。

RxJavaでは、非同期処理を簡単に並列化できます。

// api1, api2 はAPIコールのObservable
Observable.combineLatest(api1, api2, Pair::create)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(...)

僕はCountDownLatchで3つのAPIを待ち合わせるコードを書いててウワアアア!!!ってなって、RxJavaの導入を決断しました。

画面のライフサイクルに合わせて非同期処理を止めやすい

Androidを書いてる人が一度は見たことがあるのが、非同期処理のコールバックでUI操作しようとしたけど、ユーザーがバックボタンを押したなどの理由で画面がすでにないためExceptionになるというクラッシュです。

これを回避しようとすると、コールバックが呼ばれたときに画面が破棄されていないかをチェックすることが必要になります(getActivity() != nullみたいなやつ)が、そもそも画面が破棄されたときに自動で非同期処理を止めてくれたら嬉しいですよね。RxJavaと一緒にRxLifecycleというライブラリを使えば、ActivityやFragmentの破棄時に非同期処理を中止することが簡単にできます。

myObservable
    .compose(RxLifecycle.bindUntilEvent(lifecycle, ActivityEvent.DESTROY)) // ここ
    .subscribe();

composeの行を足すだけで、Activity/Fragmentのライフサイクルにバインドできます。

リスト操作を簡単にしたい

Javaの配列やリストはメソッドが貧弱なので、他の言語を触ったことがある人ならmapやfilterなどのメソッドが欲しくなると思います。

RxJavaを使えば、配列やリストをより簡単に扱えます。例えばこんな感じ。

List<Integer> list = Observable.just(1, 2, 3, 4, 5)
                .filter(integer -> integer % 2 == 0) // 2, 4
                .map(integer -> integer * 2) // 4, 8
                .toList() // リストへ変換
                .blockingGet(); // 同期的に取得

ただ、リスト操作を便利にするだけなら最近ではLightweight Stream APIを使うほうがスマートな気もしますし、Kotlinであれば最初からリスト関連のAPIが充実していたりもします。

まとめ

ということで、ここで書いたような課題を感じている方は、RxJava試してみるといいのではないでしょうか。 僕もまだ定番の使い方しかマスターできてませんが、考え方を一度理解すればコードが書きやすくなると思います。

shibuya.apk 13で登壇しました #shibuya_apk

Intermediate Level Data Bindingというタイトルで発表しました。

speakerdeck.com

また、サンプルコードはこちらにあります。

github.com

基本的にはデータバインディング使ったことない/簡単なとこでは使ってる人に、BindingAdapterやリストとの連携などもう少し突っ込んだ使い方を紹介するというセッションでした。

かなり発表時間が伸びてしまい、ちょっと後半駆け足になってしまい、楽しみにしてくれてた方申し訳ありませんでした。懇親会で何人かの方から「もっと見たかった」「データバインディングについて相談したい」といって頂けたのがせめてもの幸いでした。

ざっくりとですが、昨日のセッションで話したかったことを以下にまとめます。

会場アンケート

  • データバインディングを使っている: 6割
  • BindingAdapterを使ったことがある: 6割
  • InverseBindingMethodなどを使ったことがある: ほとんどいない(1,2人だけ)
  • ObservableListを使ったことがある: 2割

BindingAdapter

データバインディングの属性を自分で追加する仕組みです。Picassoによる画像読み込みをBindingAdapter化するのは割とメジャーですでに使ってる人も多いかもしれませんが、リスナーや双方向データバインディングを追加する方法は結構めんどくさいので、これらの実装方法を紹介しました。「データバインディング」と言いつつ、ビューモデルなどのデータとバインディングしない使い方もあり、単なるXML属性の拡張としても使えるので、今のアプリの設計を大幅に変えなくてもXML属性の拡張として導入しやすいと思います。

リストとの連携

ObservableListとRecyclerView.Adapterを組み合わせる方法を紹介しました。ObservableListには要素の追加・削除・移動などのリスナを登録できるので、その中でRecyclerView.Adapterのnotify…を呼ぶというのが肝です。当日紹介しませんでしたが、MVVMで実装する場合は、画面全体のビューモデルとリストアイテム一個のビューモデルを用意する実装を自分はよく使います。この辺の設計についてはこれだけで話せる内容なので、別の機会に発表したいと思います。

詰まりやすいところ

(完全に時間がなくて飛ばしました)

時間があれば、データバインディングによるUI更新時のチラツキを抑える方法と、スレッドに関する注意を話す予定でした。

  • 即時UI更新には、ViewDataBinding#executePendingBindings() を呼ぶ
  • コレクションを操作するときはメインスレッド。それ以外はどのスレッドからでも操作できる

ということが伝えたかったことです。

まとめ

ということで、一歩先を行くデータバインディングについてお話しました。 何か質問などがある方は、 Twitter: ksk_kbys までお願いします。