kobakei's blog

プログラミングの話や技術系イベントの話をゆるく書くブログです

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 までお願いします。

DroidKaigi 2017に参加した感想

聞いたセッション

1日目

  • 逆引き マテリアル デザイン
  • Android Security 最前線!!
  • Androidリアルタイム通信アプリ作成Tips
  • Data Bindingで開発を気持ちよくしよう
  • 実践アニメーション
  • オフラインファーストなアプリケーション開発
  • React Nativeはクロスプラットフォームモバイルアプリ開発の夢を見るか
  • What’s New in RxJava 2.0

2日目

  • Android ORMの選び方
  • 未熟なチーム開発
  • Data Bindingで実現するMVVM Architecture
  • 4年続くアプリにおけるチーム開発
  • コマンドなしでぼくはAndroid開発できない話
  • Fireside Chat
  • テスト0から目指すクラッシュフリー率99%

感想

結論:とてもよかった

Androidエンジニアこんなにいたんだ

過去最多の参加者ということで、かなり広い会場に人がぎっしりいて「日本にこんなにAndroidエンジニアいたのか」という驚き。最近どこの会社もAndroidエンジニア足りてないって話を聞きますけどみんな普段どこにいるんですかね。。。

セッションのアジェンダをもうちょっと詳しく/正確にして欲しい

セッションについては、公式サイト/アプリで見るアジェンダを見て予想した内容と、実際に話してる内容に結構ずれがあったのが気になりました。例えばHogeというワードがアジェンダには入ってないのにトークではHogeを使う前提で話が進む、みたいな。同じ時間帯に見たいセッションがかぶることが多く、どのセッションを見るべきかを判断するためにもこの辺の情報はできるだけ正確にして欲しいなーという印象。

MVVM + data binding

個人的には最近MVVM+data bindingに取り組んでいることもあって、「Data Bindingで実現するMVVM Architecture」のセッションが一番期待していました。セッション本体はどちらかというとMVVM初めてやる人向けかなーという感想でしたが、Office Hourでの会話がとても濃く、こういうのはネットでスライドみるだけじゃなくて実際に参加してみないと体験できないなと思いました。このセッション立ち見がかなりいたので、もっと大きい部屋でもよかったんじゃないですかね。部屋割りどうやって決めてるのか知らないけど。

もっとこんなのが聞きたい

来年に期待。

  • 設計やデザインパターンを適用として、詰まったところ&どうやって解決したか、みたいな話
  • レガシーなアプリをモダンな設計にリプレースした話
  • あまり有名じゃないけど実はおすすめなライブラリの話
  • 泥臭い話

来年は登壇したい

あと、今年CFP応募してみたんですが、残念ながらRejectされた*1ので普通に聞くだけでの参加でした。 来年こそはスピーカーとして参加したいと思います。

*1:RejectConやりたい