kobakei's blog

たまに書きます

Androidアプリを設計する上で考慮したポイント

この記事はAndroid Advent Calendar 2017の13日目です。

僕が今業務で開発しているAndroidアプリの設計の紹介と、そこに行き着くまでの僕の設計に対する考え方を紹介します。

こんな設計にしています

f:id:keisukekobayashi:20171210023107p:plain

どういうポイントを考慮してなぜこの形に行き着いたのかを以下紹介します。

ビューとモデルのライフサイクルの違いに対応したい

Androidアプリにおいて、アプリのプロセスを通して保持したい情報がある一方、画面の状態などはユーザーの操作によって簡単に破棄されることがあります。つまり、モデルは長命だが、ビューやビューモデルは短命ということです。

この辺はこちらのスライドが神資料なので是非読んだことない人は読んでみるとよいと思います。少し古い資料ですが、十分に今でも通用する考え方がわかると思います。

では、長命のモデルから短命のビューに結果を返すときはどうすればよいでしょうか?

結論から言えば、ビューとモデルの境界にはRxJavaを採用しています。理由は、ビューが死んだときの非同期処理を中止するといったハンドリングが簡単だからです。そして、ビューとモデルの間にRxを採用するなら、モデルの内部もRxに統一した方が見やすいため、アプリ全体でRxを採用しました。 今ならArchitecture ComponentsのLiveDataで統一するのもアリだとは思いますが、非同期処理の結合周り(直列処理、並列処理)などはRxの方が向いている気もしています。

依存関係をシンプルに保ちたい

大事なのは依存関係を一方向にすることです。自分より上の層に依存するのは当然NGですが、自分と同じレイヤーにも絶対に依存してはいけません。すぐに循環参照が発生してカオスになるからです。

この設計の場合、ビュー、ビューモデル、モデルが一方向の依存になります。さらに、モデルはユースケースビジネスロジック)、リポジトリCRUDを責務とする層)、データ(APIやDAO)の3層構造にしています。

なお、Clean Architectureも検討したことはありますが、現状そこまで厳密にはやっていません。なぜなら、Clean Architectureを実現しようとした場合、タマネギの内側から外側へアクセスさせるためにインタフェースを用意するのがめんどくさかったからです。

この依存関係を一方向に保つためのツールとして、DI(Dagger)を使うのはおすすめです。循環参照のような上記のルールを守らない依存関係を作ろうとすると、コンパイル時にエラーにしてくれます。

同じコードを二度書きたくない

この設計の中で、ユースケース層は果たして本当に必要なのかと聞かれることがあります。ビューモデルが直接リポジトリを呼んでいいんじゃないかと。

答えはアプリごとに違うと思いますが、私の現在の環境ではユースケースはあったほうが良いと思います。例えば、ログインの直後にプッシュ通知のトークンを登録するなど、複数のリポジトリを一連の流れで呼ぶことがあります。ログイン処理をただ1つの画面からしか呼ばないのであればよいですが、複数の画面でログイン可能な仕様の場合、この一連の処理をまとめておくことで同じ処理を二度実装しなくてよくなります。

データバインディングもビューの処理を共通化するのに役立ちます。Glideを使った画像の読み込みなど、ほとんど同じ処理を画面ごとに描くのではなく、BindingAdapterを使ってコードは共通化し、XMLで定義できるようにします。

まとめ

以上まとめますと、以下のような点を考慮して今の設計に行き着きました。

  • ビューはすぐに死ぬ前提で考える
  • 依存関係をシンプルにする
  • できるだけコードを共通化する

何がベストな設計かはプロジェクトやチームによって異なるとは思いますが、是非参考にしてください。

Android開発における定番ライブラリ22選

Androidをはじめたばかり or これから始める人向けにまとめました。UI系ライブラリは種類が多すぎるので除外しています。

公式系

1. サポートライブラリ

developer.android.com

※種類が多いのでまとめて1つとカウントしました

Android後方互換性を維持するために、Googleが提供しているライブラリ群です。機能ごとにパッケージが分かれていますが、ほぼ必須のものが多いです。

  • appcompat
  • support-v4
  • multidex
  • design
  • recyclerview
  • cardview
  • etc

2. Data Binding

developer.android.com

レイアウトにデータを紐付けることで、データの更新に応じて自動的にビューを更新することができるライブラリです。レイアウトの属性を増やしたり、findViewByIdを簡略化するだけでも使えます。

以前はButterKnifeというライブラリが使われることが多かったですが、data bindingの方が多機能なので最近はdata bindingの方が人気があると思います。

3. Architecture Components

developer.android.com

Androidフレームワークのライフサイクル関連の問題を扱うことができます。まだ情報が少ないので定番とは言いにくいですが、間違いなくこれから主流になると思います。主に以下4つのコンポーネントからなりますが、LifecycleとViewModelが優先度が高く、Roomが最も優先度が低いです。

  • Lifecycle
  • ViewModel
  • LiveData
  • Room

JSON

4. Gson

github.com

JSON文字列をPOJO (Plain Old Java Object)に変換したり、逆にPOJOJSON化するのに使います。

僕の観測範囲ではGsonが最もメジャーどころですが、他にもJackson, Moshiなどがあります。

Parcelable

5. Parceler

github.com

AndroidにはParcelableというシリアライズの仕組みがあり、画面遷移時のデータの受け渡しなどに使います。Parcelableを使うためにはボイラープレート(お決まりの定型文となるコード)を書く必要があるのですが、Parcelerはこれを自動的に生成してくれます。

KotlinではParcelizeという標準の仕組みがありますが、まだExpetimentalなので実戦投入にはご注意ください。

通信

6. OkHttp

square.github.io

Squareが提供しているHTTPクライアントです。ほとんどのAndroidアプリが採用しているといっても過言ではないかと思います。

7. Retrofit

square.github.io

インタフェースを定義するだけでAPIクライアントを生成してくれるライブラリです。ほとんどの人はOkHttpとセットで使用していると思います。また、後述のRxJavaと組み合わせて、APIの結果をRxJavaのストリームで受け取ることも出来ます。

画像

8. Glide

bumptech.github.io

簡単にサーバー上の画像を読み込んでImageViewに表示することができます。OkHttpと連携することで、画像のダウンロードをOkHttpに任せることも出来ます。

SquareのPicassoもメジャーですが、最近アップデートされていないので今ならGlideを使うほうが無難だと思います。

データベース

9. Orma

github.com

AndroidSQLite用のORMです。高速なIOや自動マイグレーションなど、豊富な機能がたくさんあります。

Architecture ComponentsのRoomも似ていますが、マイグレーション周りはOrmaの方が便利だと思うので僕はOrmaをおすすめします。

10. Realm

realm.io

こちらはNoSQLなデータストアです。高速なIO、簡単に暗号化できる点などが人気があるようです。Android組み込みのSQLiteではなく、完全に別のデータベースを組み込むのでアプリサイズが大きくなるかもしれません。

依存性注入

11. Dagger 2

google.github.io

依存性注入ライブラリの定番です。導入は難しいですが、一度導入してしまえばクラス間の依存関係を綺麗に保ち、テストを書きやすい状態を維持できます。

SquareのDaggerとGoogleのDaggerがありますが、Googleの方が新しくAPIやパフォーマンスが改善されているので基本はGoogleの方を使用しましょう。

非同期処理

12. RxJava

github.com

Reactive Streamsという非同期処理ライブラリのJava実装です。APIやデータベース処理などをストリームとして扱うことができます。RxAndroidと大体セットで使います。

13. EventBus

greenrobot.org

Pub/Subのライブラリです。AndroidフレームワークのBroadcastReceiverに似ていますが、Context以外のクラスでも受信できたり便利です。

使いすぎるとコードがカオスになるので用法・用量を守って以下略。

言語機能系

14. ThreeTen ABP

github.com

Java 8の日付関連のAPIのバックポートライブラリです。

15. Lightweight Stream API

github.com

Java 8のストリームAPIのバックポートライブラリです。mapfilterなど他のプログラミング言語のリストにあるメソッドが使えるようになります。

Kotlinの場合は、Kotlinのコレクションが多機能なので不要です。

ランタイムパーミッション

16. PermissionsDispatcher

permissions-dispatcher.github.io

Android 6から追加されたランタイムパーミッションに対応するためのライブラリです。アノテーションを付けるだけで、パーミッション周りのコードを自動生成できます。

17. RxPermissions

github.com

PermissionDispatcherと同じですが、RxJavaの形式でハンドリングできます。

デバッグ

18. Timber

github.com

ロガー。リリースビルドとデバッグビルドでログの出力を切り替えたり、本番だけCrashlyticsなどにログを送信したりできます。

19. Stetho

facebook.github.io

PCにつないでGoogle Chromeデバッグできます。通信のリクエスト/レスポンスの中身を見たり、アプリのデータベースの中身を見たり、UIの構造を調べたり出来ます。

20. LeakCanary

github.com

メモリリークを自動的に検出してくれるライブラリです。

テスト

21. Robolectric

Robolectric

Android単体テストJVMで実行するためのライブラリです。Android単体テストは通常Androidフレームワーク上でしか実行できないのですが、それだとテストの実行時にエミュレータの起動や実機の接続が必要になるため何かと不便です。Robolectricを使ってJVM上で実行することで、簡単かつ高速に単体テストを実行できます。

22. Mockito

site.mockito.org

テスト時にモックを簡単に作成できるライブラリです。

iOSはじめました

最近仕事でiOSを書き始めた。正直まだ全然書けないのでひたすら本読んだりググったりしながら、練習がてら簡単そうな機能から作っている。

AutoLayout辛い

AndroidのConstraintLayoutの元ネタのレイアウトの仕組み。XcodeGUIでUIを組み立てていくのにまだ慣れない。些細なことでレイアウトが崩れるし、Storyboard開くだけでGitでdiffが出たりして辛みがある。iOS書いてると「なんてAndroid Studioはいい子だったんだ」と再認識するんだけど、原因の8割くらいはAutoLayout関連な気がする。

AutoLayout含め、iOS開発では「お前らXcodeGUIで色々できるようにしといたからそれ使って開発しろよ。それ以上のことやりたい?知るか」的なAppleの思想を感じる。Androidは逆に最低限のGUIしか提供してない代わりに、XMLいじれば大抵の要素をいじれるようにしてくれてる気がする。ConstraintLayoutも、結局XML触るしね。

Swiftはそんなに抵抗ない

もともと書いてなかったけどコード読む機会はそこそこあったので、割とすんなり入れてる。AutoLayoutで消耗しすぎて進捗は著しく悪いんだけど、慣れてくれば普通に書けると思う。今年はKotlinも書き始めたんだけど、Javamよりは文法近いのでなおさら抵抗がないのかもしれない。

ネットの記事やサンプルにたまに出てくるObjectibve-Cは、4~5年前くらいにちょっと書いてたこともあってすでに克服していたのはよかった。

昔より物覚えが悪くなったかもしれない

今回久しぶりに新しい技術を勉強してるわけだけど、20代の頃の自分だったらもっとすんなり習得できたんじゃないかという気がしていて、歳とったんだなとしみじみした。10年前のフレッシュな脳みそが欲しい。

あとは「一人前にiOS書けるようになった」と自信を持つには、簡単な趣味アプリでもいいのでゼロから書いて公開する体験が必須だと思ってるんだけど、いいネタが無い。毎年Appleにお布施を払わないといけないというのも腰が重い理由かも。誰か儲かるアプリのアイデアください。

広告を非表示にする

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/<アプリ名> に保存するのが無難。