2018年5月18日金曜日

IO recap : Android vitals: debug app performance and reap rewards (Google I/O '18)



星1つのレビューでは42%のユーザーが安定性やバグについて言及している
星5つのレビューでは73%のユーザーがスピード、デザイン、使いやすさについて言及している

ANR 率やクラッシュ率が上がると、ユーザーがアプリで費やす時間が有意に減る


Android vitals とは、Android デバイスの安定性とパフォーマンスを向上するための Google が主導する取り組み
もっとも重要なパフォーマンスメトリクスであるバッテリー、安定性、レンダリングの情報を開発者にわかりやすく提供する
データ提供を opt in している1億以上のユーザーからの情報

昨年 Android vitals をリリースしてから 10,000 以上の開発者がコンソールからパフォーマンスを理解した
昨年に比べ、スピード、デザイン、使いやすさについて言及している星5つのレビューは 4% 増え、安定性やバグに言及している星1つのレビューは 18% 減り、リソースの使用について言及している星1つのレビューは 21% 減った


Starbucks アプリは ANR rate が 70% 減り、Crash rate が 85% 減った
ANR は 3rd party のライブラリで起こっていたため、自分たちの観測に引っかかっていなかった
Android vitals はプラットフォームレベルのツールなので、Starbucks の 3rd party crash SDK では検出できていなかったクラッシュを見つけることができた
なぜならそのクラッシュは 3rd party crash SDK が開始される前に起こっていたため

Kiloo の Subway Surfers というゲームでは ANR を 95% 減らすことができた


昨年 Android vitals を公開したとき、バッテリー、安定性、レンダリングの3つの項目があった
新しくアプリのスタートアップ時間(App startup time)と権限(Permissions)が追加された

Vitals
  • バッテリー(Battery)
  • 安定性(Stability)
  • レンダリング(Rendering)
  • New: アプリのスタートアップ時間(App startup time)
    • コールド スタートアップ時間が長い(Slow cold start)
    • ウォーム スタートアップ時間が長い(Slow warm start)
    • ホット スタートアップ時間が長い(Slow hot start)
  • New: 権限(Permissions)
    • 権限リクエストの拒否率(Permission request denials)

バッテリー(Battery)

  • 停止した wake lock
  • 過度の wakeup
  • 過度のバックグラウンドでの Wi-Fi スキャン
  • 過度のバックグラウンドでのネットワーク使用

安定性(Stability)

  • ANR 発生率
  • クラッシュ発生率

レンダリング(Rendering)

  • フリーズした UI フレーム
  • 遅いレンダリング

New: アプリのスタートアップ時間(App startup time)

  • コールド スタートアップ時間が長い : 5秒以上
  • ウォーム スタートアップ時間が長い : 2秒以上
  • ホット スタートアップ時間が長い : 1.5秒以上

New Metric: コールド スタートアップ時間が長い
  • 5秒以上かかると遅いと判断
  • コールドスタート :
    • Activity が起動してから running になるまで
    • Activity launched → onCreate() → onStart() → onResume() → Activity running
    • アプリがしばらく使われておらず、アプリがメモリ上にいない状態からスタート

New Metric: ウォーム スタートアップ時間が長い
  • 2秒以上かかると遅いと判断
  • ウォームスタート :
    • Activity が起動してから running になるまで
    • Activity launched → onCreate() → onStart() → onResume() → Activity running
    • アプリが最近使われており、アプリがメモリ上にいる状態からスタート(アプリはkillされていない)

New Metric: ホット スタートアップ時間が長い
  • 1.5秒以上かかると遅いと判断
  • ホットスタート :
    • onRestart() から running になるまで
    • onRestart() → onStart() → onResume() → Activity running
    • アプリと Activity がメモリ上にいる状態からスタート

New: 権限(Permissions)

アプリのコアバリューに必要な権限だけをリクエストし、必要に応じて権限リクエストの正当な理由をランタイム時に提供する
  • 権限リクエストの拒否率
〜40%のユーザーが権限を拒否した理由として、その権限は不必要だと思ったと回答している

権限の詳細ビューでは権限をグループに分けて表示しているので、どの権限がユーザーにとって納得感があり、どの権限が不必要だと思われているかがわかる

Android Vitals の詳細の内訳

  • 一般的な内訳
    • APK versionごと
    • デバイスごと
    • Android versionごと
  • Wake locks, wakeups
    • tag ごと
  • ANR率
    • Activity 名ごと
    • ANR type ごと
    • Clusters
  • クラッシュ率
    • Clusters

カテゴリーベンチマーク

自分のアプリの vital が特定のカテゴリーの中でどのくらい良いかを見ることができる
vital の各 metric でパーセンタイル 25, 50, 75 の値を見ることができる

概要画面で全ての vital がリストされ、直近の30日とその前の30日の値、ベンチマークの値を見ることができる



概要画面の主な指標(Core Vitals)には Google Play でのアプリの表示やランキングに影響するパフォーマンス指標が表示される
主な指標が下位25%より悪くなると Bad behavior として表示される

異常検知(Anomaly Detection)

新リリースやリグレッションの結果値に急変があるとアラートを出す
  • ANRやクラッシュ率の大きな変化
  • 主な指標(Core Vitals)の大きな変化
概要画面右上の[通知設定]からAlertをメールで受け取るよう設定できる




主な指標(Core Vitals)を改善するには

ANRの原因
  • Network / Disk operations
  • Long calculations
  • InterProcess Communication (IPC)
  • Locks and Synchronization
  • Slow BroadcastReceiver handling

Network / Disk operations

例: SharedPreferences インスタンスを生成する時点で Disk 処理が行われる override fun onCreate(state: Bundle?) { // この時点で Disk 処理が行われる prefs = PreferenceManager.getDefaultSharedPreferences(this) } どのメソッドが Network 処理や Disk 処理をするのか理解するのは難しいので StrictMode を利用する class MyApplication : Application() { override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .penaltyDeath() .build()) } } }

Long calculations

例: 数独ゲームの盤面生成に時間がかかる

Android Profiler で CPU の使用をチェックする
ちなみに Android Studio 3.2 Canary ではスタートアップ時間をプロファイルできるようになっている

StrictMode にはこのメソッドを呼ぶと遅くなるということを指定できる class GenerateBoardSource() { fun generateBoard(seed: Long) : SudokuGame { StrictMode.noteSlowCall("Generating Sudoku board") return SudokuSolver.generate() } } StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() ... .detectCustomSlowCalls() ... .build())

InterProcess Communication (IPC)
  • 他のアプリを呼び出す場合、基本こちらに制御権がない
  • 呼び出し先が Network 処理や Disk 処理をするかもしれないので別スレッドで呼び出す


Locks and Synchronization
  • これはとても難しい問題
  • deadlock になったり main thread をブロックするかもしれない
  • デバッグが難しい
  • Android Vitals が提供する trace file の情報がデバッグに役立つかもしれない


Slow BroadcastReceiver handling
  • Android Manifest に BroadcastReceiver を登録した場合、onReceive メソッドは main thread で呼び出される
  • 実行に時間がかかる処理を onReceive() でやるべきではない
  • 10秒以内に処理を終えないと ANR になる
  • Notification に表示する画像を Disk から読み出すなどちょっとした Disk 処理が必要な場合は onReceive() で goAsync() を呼び、別のスレッドを立ち上げ、終わったら PendingResult.finish() を呼ぶ
  • https://developer.android.com/guide/components/broadcasts#effects-process-state
  • あまり長いと結局システムに kill されるので、長い処理が必要なら JobScheduler や WorkManager を使う



クラッシュ対策
  • クラッシュの対応として Activity のライフサイクルでむやみに null チェックや例外の握りつぶしをするべきではない
  • 車輪の再発明をしない : 問題を解決する利用できるライブラリを使う
    • Lifecycle handling (LiveData, ViewModel)
    • Database object mapping (Room)
    • Data paging (Paging)
    • *NEW* Fragment transitions, up/back, deep link handling (Navigation)
    • *New* Job scheduling (WorkManager)
  • 3rd party の優れたライブラリもたくさんある
  • Kotlin を使う : でも全てのクラッシュを防げる銀の弾丸ではないよ!
  • private / hidden API を使わない


バッテリー対策

停止した wake lock

wake lock が取得されたが適切に release されなかった
  • wake lock を使わない
  • 画面をつけっぱなしにしたいなら Activity の Window に FLAG_KEEP_SCREEN_ON を指定する
  • 自分で Service を管理せず job を schedule する
  • AlarmManager で BroadcastReceiver を起こすようにすると onReceive() の間 AlarmManager は wake lock を hold してしまう
  • wake lock を使わなければ permission も必要なくなる
  • どうしても wake lock を使わないといけないなら、常に PARTIAL_WAKE_LOCK を使うこと
  • wakeLock.acquire() にタイムアウトをセットすること
  • static な descriptive tag を渡すこと(Android Vitals でのデバッグがしやすくなる)
  • try { ... } finally { wakeLock.release() } すること


過度の wakeup
  • もっとも大きい原因は AlarmManager の *_WAKEUP アラーム
  • 可能ならなくす(Remove)
  • 頻度を減らす(Reduce)
  • FCM, WorkManager, JobScheduler, SyncManager などに置き換える(Replace)
  • Android Studio 3.2 Canary に追加された Energy Profiler で wake lock に関する問題をデバッグできる


関連





2018年5月17日木曜日

IO recap : Migrate your existing app to target Android Oreo and above (Google I/O '18)



新規アプリは2018年8月以降
既存アプリのアップデートは2018年11月以降
targetSdkVersion を >= 26 にしないといけない話

Permissions

Runtime permissions : ユーザーは設定からon/offできる
Special permissions : 画面の上にdrawするやつとか

Alarms

WorkManager を使う

BroadcastReceivers

Android Manifest で register したほとんどの implicit receiver はもはや受け取れなくなる
例外もある、ACTION_BOOT_COMPLETED とか

BroadcastReceiver の使用を避ける例として、JobScheduler を使ってネットワーク状態の変更を検知し、 val jobScheduler = getSystemService(Context.JOB_SCHDULER_SERVICE) as JobScheduler val jobInfo = JobInfoBuilder(JOB_ID, serviceComponent) .setRequiredNetwork(JobInfo.NETWORK_TYPE_ANY) .build() BroadcastReceiver は Android Manifest で disabled にしておき(android:enabled="false" を追加) <receiver android:name=".NetworkConnectionReceiver" android:enabled="false"> <intent-filter> <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> </intent-filter> </receiver> JobScheduler で receiver を有効にする fun setNetworkReceiverState(enabled:Boolean) { val componentName = ComponentName(package, NetworkConnectivityReceiver::class.java.name) val state = if (enabled) { PackageManager.COMPONENT_ENABLED_STATE_ENABLED } else { PackageManager.COMPONENT_ENABLED_STATE_DISABLED } packageManager.setComponentEnabledSetting( componentName, state, PackageManager.DONT_KILL_APP ) }

ACTION_MY_PACKAGE_REPLACED で全ての処理をやるのではなく ChangedPackages でも判断できる val packages : ChangedPackages = packageManager.getChangedPackages(prefs.getPackageSequenceNumber()) prefs.setPackageSequenceNumber(packages.getSequenceNumber())

Background Limits

Foreground Service にすべきものならそうする

Foreground のもの
  • Visible App
  • Foreground Service
  • Foreground Client に bound されている Service
  • ForeGround Client への Content Provider
  • AccessibilityService, NotificationListenerService, AbstractAccountAuthenticator, WallpaperServiceなどの例外もある
Background のもの
  • Not visible
  • Non-Foreground Service
  • JobService
  • BroadcastReceiver
O以降では、Background から Service を start しようとすると IllegalStateException が投げられる

Grace Period : Service が background に置かれてから1分程度は生きている

Whitelist
  • Notification action
  • High Priority FCM message
  • SMS/MMS delivery
Background Service を使わずに Background で仕事をさせるには
  • background task には WorkManager を使う
  • IntentService は JobIntentService に置き換える

JobScheduler の振る舞いについてよく理解するまで Android L で JobScheduler を使わないほうがいい
L と M の first release では JobScheduler は同じ constrains の2つの job を正しく実行しない問題がある
この問題の workaround として、同じ constrains の2つの job をスケジューリングすればよい(がどうするかは言ってない)
MR1 以降は JobScheduler はちゃんと動いているが、minimum latecy を 0 にセットするのはやめたほうがよい
失敗したときの処理は backoff でやること
backoff を超えて reschedule しないこと

PendingIntent の向き先を Service から explicit な BroadcastReceiver に変更し、30秒以内に goAsync() を呼び出す
または BroadcastReceiver 内で WorkManager を使う

外部からの time-sensitive な trigger が必要なら Firebase Cloud Messaging を使う
high priority messages はデバイスが DOZE でも起きるので使いすぎはよくない
10秒以内に実行し終えるならそのままそこで処理をし、それ以上かかるなら WorkManager を使う

ユーザーが明示的に始めた時間のかかる処理は Foreground Service で実行する
Maps Navigation, fitness tracking, playing music など

Photo Broadcasts

N 以降では Photo Broadcasts が起こらないので、代わりに ContentUris をトリガーとした work を使う val constrains = Constraints.Builder() .constraints.addContentUriTrigger(SOME_URI, true) ... .build() val work = OneTimeWorkRequest.Builder(MyWork::class.java) .setConstraints(constrains) .build()

Background Location

O 以降のデバイスでは Background Location の制限は targetSdkVersion によらず適用される

対応として
  • 1. Geofencing を使う : 100個までしか Geofencing を active にできないので、必要に応じて動的に変えるなどの対策をとる
  • 2. Beacon による Nearby Notification を使う
  • 3. FusedLocationProvider の Batch 処理 fun createLocationRequest() { ... val request = LocationRequest() request.interval = 10L * 60L * 1000L request.maxWaitTime = 30L * 60L * 1000L }
  • 4. Passive Location を使う fun createLocationRequest() { ... val request = LocationRequest() request.interval = 10L * 60L * 1000L request.maxWaitTime = 30L * 60L * 1000L request.fastestInterval = 2L * 60L * 1000L }
アプリの location を更新するのはネットワーク処理などの重たい処理と紐づいているべき

Battery

バッテリーに関する機能
  • Doze(M+)
  • Doze on the go (N+)
  • App Standby (M~O)
  • App Standby Buckets (P+)
App Standby Buckets (P+)
  • 使用履歴に基づく制限
  • アプリは Standby Bucket のどこかに割り当てられる
  • 割り当てられた Bucket によって適用される制限が変わる


Battery Saver (P+)
  • Screen Off のときは Location を取らない
  • 全てのアプリが App Standby
  • Background のアプリは Network 処理をできない
  • (OLED Devices では)可能なら Dark Theme が有効になる

Testing

Testing Doze $ adb shell dumpsys deviceidle force-idle Testing App Standby $ adb shell dumpsys battery unplug $ adb shell am get-inactive <package-name> $ adb shell am set-inactive <package-name> true Testing App Standby Buckets 1. $ adb shell dumpsys battery unplug 2. $ adb shell am get-standby-bucket <package name> 10 ACTIVE 20 WORKING_SET 30 FREQUENT 40 RARE 3. $ adb shell am set-standby-bucket <package name> <bucket> 4. API: UsageStatsManager.getAppStandbyBucket() Testing Battery Saver $ adb shell dumpsys battery unplug $ adb shell settings put global low_power 1 <do your tests> $ adb shell dumpsys battery reset API: PowerManager.isPowerSaveMode() PowerManager.ACTION_POWER_SAVE_MODE_CHANGED アプリに Dark Theme があるなら Save Mode のときは Dark Theme にするという選択肢
OLED Devices なら電池の節約になる

Modern features

  • Notification Channels
  • Display Cutout : Developer Options で Cutout モードにできる
  • Picture in Picture
  • Multi-display

non-SDK interface

DP1 で non-SDK interface の使用を制限し、使われていたら Toast や log で警告を出すようにした

DP2 ではメソッドが単に動かなくなるので、アプリがクラッシュすることになる

将来的には StrictMode に新しい VM policy を追加する
これを使って全ての non-SDK API を検出できる StrictMode.setVmPolicy( StrictMode.VmPolicy.Builder() .detectNonSdkApiUsage().build()) non-SDK の使用がライブラリ内で起こるかもしれないので、これでチェックすることが重要

https://developer.android.com/distribute/best-practices/develop/target-sdk



2018年5月16日水曜日

IO recap : What's new with ConstraintLayout and Android Studio design tools (Google I/O '18)




design 時の tools: 属性
  • tools:context
  • tools:itemCount
    • ListView, RecyclerView のプレビューで表示するitemの数
  • tools:layout
  • tools:listitem
    • ListView, RecyclerView のプレビューで表示する各itemのレイアウト
  • tools:listheader
  • tools:listfooter
  • tools:showIn
    • 指定した layout リソースに include された状態のプレビューになる
  • tools:menu
  • tools:minValue
  • tools:maxValue
  • tools:openDrawer
  • tools:text
    • プレビューでの文字
  • tools:textColor
    • プレビューでの文字色


サンプルデータ

[File] - [New] - [Sample Data Directory] から sample data を置く場所を作る
場所としては app/ 直下に sampledata/ が作られる



ここに material_colors という名前のファイルを作って、各行にカラーコードを書く #F44336 #9C27B0 #3F51B5 #673AB7 RecyclerView の各 item の ImageView に tools:tint="@sample/material_colors" と指定すると、行ごとに @sample/material_colors 内の色が順番に割り当てられる

dimensions なら 4dp 8dp 16dp ... のようなファイルを用意する

画像の custom sample data はディレクトリ(例えば albumcovers/)を作ってサンプルデータの画像をそこに配置する
ディレクトリ名に _ や大文字を使うと動かないので注意



JSON の custom sample Data では複数のサンプルをまとめて定義できる

albumname.json { "songs" : [ {"title": "Nougat", "author" : "Dylan Dalton"}, {"title": "Ice Cream", "author": "Isa Henderson"}, ... ]} tools:text="@sample/albumname.json/title" tools:text="@sample/albumname.json/author"

Predefined Sample data

Images, Text, olors, Dates...
  • @tools:sample/first_names
  • @tools:sample/last_names
  • @tools:sample/full_names
  • @tools:sample/cities
  • @tools:sample/us_phones
  • @tools:sample/us_zipcodes
  • @tools:sample/date/day_of_week
  • @tools:sample/date/ddmmyy
  • @tools:sample/date/hhmm
  • @tools:sample/date/hhmmss
  • @tools:sample/date/mmddyy
  • @tools:sample/lorem
  • @tools:sample/lorem/random
  • @tools:sample/avatars
  • @tools:sample/backgrounds/scenic
  • ...
Android Studio 3.2 では resource picker の Drawable のところに sample data category が追加された



Android Studio 3.2 の新しい design time helper では、ImageView を選択して表示されるレンチアイコンをクリックして、sample data を切り替えたり、data set のうち一つだけを使うようにできる
Browse をクリックすると、resource picker が開く



design time helper は TextView でも使える







RecyclerView でも使える

listitem に使うレイアウトや itemCount を指定できる
用意されているテンプレを指定すると、そのレイアウトリソースが layout/ にコピーされる
(たまにレンチが出なくなり、まだかなり不安定)












ConstraintLayout

去年(2017) ConstraintLayout 1.0 をリリースした
先月(2018/4) 1.1 をリリースした
implementation 'com.android.support.constraint:constraint-layout:1.1.0'

ConstraintLayout 2.0

Helpers
  • 画面に表示されないがUIを作成するのを助けるもの
  • Guideline とか Barrier とか
  • Viewの参照を保持してあれこれする
  • ConstraintHelper を継承して Custom の Helper を作れる
Helper の3つのカテゴリー
  • Layout Manipulation
    • LinearLayout のような配置を助ける Helper とか FlexBox 的な配置を助けるものとか
  • Post-Layout Manipulation
    • flying object みたいなエフェクトをかけるやつとか
  • Rendering or Decorating
    • Viewの代わりに描画するやつ
Helper の例

Layers
  • Helper の一種
  • View のセットに対する表示・非表示・変形などをサポートする




Circular Reveal
  • Rendering or Decorating 系の Helper
  • 既存の circular reveal code を利用
  • 参照しているViewにだけ適用


Lava Decorator
  • Rendering or Decorating 系の Helper
  • View の background を透明にして、この Decorator が Lava っぽい描画を実現する




Bottom Panel Decorator
  • ボトムパネルの background にインタラクティブなエフェクトを描画するやつ


Helper で view の実態と behavior を分離できる



ConstraintLayout 2.0

ConstraintLayout 2.0 では State を XML で指定できる <ConstraintLayoutStates> <State android:id="+id/small" app:constraints="@layout/layout_small" /> <State android:id="+id/large" app:constraints="@layout/layout_large" /> </ConstraintLayoutStates> fun onCreate(savedInstanceState : Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.layout); cl = findViewById(R.id.root) cl.setLayoutDescription(R.xml.layout_states) } fun change(v : View) { cl.setState(closed ? R.id.large : R.id.small) closed = !closed } 特定の constraint set が適用されるときの region を指定できる <ConstraintLayoutStates> <State app:constraints="@layout/layout_small" > <Constraints app:constraints="@layout/layout_small" app:region_widthLessThan="550dp" /> <Constraints app:constraints="@layout/layout_large" app:region_widthLessThan="450dp" /> </State> </ConstraintLayoutStates> fun onConfigurationChanged(newConfig : Configuration) { super.onConfigurationChanged(newConfig) cl.setState(newConfig.screenWidthDp, newConfig.screenHeightDp) } cl.setOnConstraintsChanged { state, layoutId -> TransitionManager.beginDelayedTransition(cl) }

MotionLayout
  • ConstraintLayout 2.2 予定
  • ConstraintLayout の subclass
  • ConstraintLayout の全てのプロパティを持っている
  • 2つの State 間のアニメーションを代わりにやってくれる
MotionLayout が複数の View と Helper を持ち、MotionScene が2つの ConstraintSet と OnTouch と KeyFrame を持つ
ConstraintSet に custom 属性が増え、アニメーションをカスタマイズできる
MotionLayout は Nest できる




Motion Editor
  • keyframe を追加してアニメーションを編集できる
  • going work now

Codelab



2018年4月21日土曜日

kotlin.math を使う

PI などの定数や sin などの計算をするとき Java の Math クラスを使うことが多いと思いますが、Kotlin 1.2 から数学関数と定数を提供する kotlin.math パッケージが追加されています。

この中には E や PI などの定数、三角関数、双曲線関数、べき乗、対数、丸め、符号と絶対値が含まれています。

試しに全部使ってみました。基本 Float を使ってますが Double も用意されています。 package net.yanzm.sample import kotlin.math.* fun main(args: Array) { println(E) // 2.718281828459045 println(PI) // 3.141592653589793 // unit in the last place println(PI.ulp) // 4.440892098500626E-16 // the remainder of division println(10f.IEEErem(3f)) // 1.0 // // sign and absolute value // println(10.sign) // 1 println((-10).sign) // -1 println(sign(10f)) // 1.0 println(sign(-10f)) // -1.0 println((1.1f).withSign(-2f)) // -1.1 println((1.1f).withSign(2f)) // 1.1 println((-1.1f).withSign(-2f)) // -1.1 println((-1.1f).withSign(2f)) // 1.1 println(10.absoluteValue) // 10 println((-10).absoluteValue) // 10 println(abs(10)) // 10 println(abs(-10)) // 10 // // rounding // println(ceil(1.1f)) // 2.0 println(ceil(1.5f)) // 2.0 println(ceil(-1.1f)) // -1.0 println(ceil(-1.5f)) // -1.0 println(floor(1.1f)) // 1.0 println(floor(1.5f)) // 1.0 println(floor(-1.1f)) // -2.0 println(floor(-1.5f)) // -2.0 println(truncate(1.1f)) // 1.0 println(truncate(1.5f)) // 1.0 println(truncate(-1.1f)) // -1.0 println(truncate(-1.5f)) // -1.0 println(round(1.1f)) // 1.0 println(round(1.5f)) // 2.0 println(round(-1.1f)) // -1.0 println(round(-1.5f)) // -2.0 println((1.1f).roundToInt()) // 1 println((1.5f).roundToInt()) // 2 println((-1.1f).roundToInt()) // -1 println((-1.5f).roundToInt()) // -2 println((1.1f).roundToLong()) // 1L println((1.5f).roundToLong()) // 2L println((-1.1f).roundToLong()) // -1L println((-1.5f).roundToLong()) // -2L // // trigonometric // println(sin(PI / 2)) // 1.0 println(asin(1f)) // 1.5707964 println(cos(PI)) // -1.0 println(acos(-1f)) // 3.1415927 println(tan(PI / 4)) // 0.9999999999999999 println(atan(1f)) // 0.7853982 println(atan2(5f, 5f)) // 0.7853982 // // hyperbolic // println(sinh(0.8813736f)) // 1.0 println(asinh(1f)) // 0.8813736 println(cosh(0f)) // 1.0 println(acosh(1f)) // 0.0 println(tanh(0.54930615f)) // 0.5 println(atanh(0.5f)) // 0.54930615 // // exponentiation and power // println(exp(0f)) // 1.0 println(exp(1f)) // 2.7182817 println(expm1(0f)) // 0.0 println(expm1(1f)) // 1.7182817 println(2f.pow(2)) // 4.0 println(sqrt(4f)) // 2.0 // sqrt(x^2 + y^2) println(hypot(3f, 4f)) // 5.0 // // logarithmic // println(ln(E)) // 1.0 println(ln(1f)) // 0.0 // ln(x + 1) println(ln1p(E - 1)) // 1.0 println(ln1p(0f)) // 0.0 println(log(10f, 10f)) // 1.0 println(log(1f, 10f)) // 0.0 println(log(2f, 2f)) // 1.0 println(log(1f, 2f)) // 0.0 // log(x, 10) println(log10(10f)) // 1.0 println(log10(1f)) // 0.0 // log(x, 2) println(log2(2f)) // 1.0 println(log2(1f)) // 0.0 // // max, min // println(max(1, 2)) // 2 println(max(1f, 2f)) // 2.0 println(min(1, 2)) // 1 println(min(1f, 2f)) // 1.0 // // nearest // println((1.1f).nextDown()) // 1.0999999 println((-1.1f).nextDown()) // -1.1000001 println((1.1f).nextUp()) // 1.1000001 println((-1.1f).nextUp()) // -1.0999999 println((1.1f).nextTowards(0f)) // 1.0999999 println((-1.1f).nextTowards(0f)) // -1.0999999 }


2018年4月12日木曜日

ViewPager + Fragment で AAC の ViewModel を使う

各ページが Fragment な ViewPager があるとします。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val pager: ViewPager = findViewById(R.id.pager) pager.adapter = MyPagerAdapter(supportFragmentManager) } class MyPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) { override fun getItem(position: Int): Fragment { return PageFragment.newInstance(position) } override fun getCount(): Int { return 10 } } } FragmentStatePagerAdapter を使った場合、不要になったページの Fragment インスタンスは破棄されます。
(FragmentPagerAdapter では一度作った Fragment インスタンスは保持されます)

そのため各ページの Fragment 内で ViewModel を取得するときに、以下のように ViewModelProviders.of() にその Fragment のインスタンスを指定してしまうと、 再度そのページが必要になったときに ViewModel も新しく作り直されてしまいます。 class PageFragment : Fragment() { ... override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) val viewModel = ViewModelProviders.of(this) .get(PageViewModel::class.java) val position = arguments!!.getInt("position") Log.d("PageFragment", "$position : $viewModel") } companion object { fun newInstance(position: Int): PageFragment { return PageFragment().apply { arguments = Bundle().apply { putInt("position", position) } } } } } 例えば最初に 1ページ目が表示され、そこから3ページ目に移動して1ページ目に戻ってくると、ViewModel のインスタンスが新しくなってしまい以前のデータを使えません。

D/PageFragment: 0 : net.yanzm.sample.PageViewModel@2dc8dc7
D/PageFragment: 1 : net.yanzm.sample.PageViewModel@389f31d
D/PageFragment: 2 : net.yanzm.sample.PageViewModel@60a3f63
D/PageFragment: 3 : net.yanzm.sample.PageViewModel@ad7a419
D/PageFragment: 0 : net.yanzm.sample.PageViewModel@8d76ebf ← 2dc8dc7 ではなく、別のインスタンスが来てしまう

そこで ViewModelProviders.of() に ViewPager を持つ Activity を指定すると、ViewModel のインスタンスが Activity にひも付くので、 ViewPager のページ移動で Fragment が作り直されても ViewModel は以前のインスタンスが取れるようになります。 val viewModel = ViewModelProviders.of(activity) .get(PageViewModel::class.java) しかし別の問題が起こります。各ページが同じ PageFragment クラスなので、今度は全部のページで同じ ViewModel のインスタンスが来てしまいます。

D/PageFragment: 0 : net.yanzm.focussample.PageViewModel@a704306
D/PageFragment: 1 : net.yanzm.focussample.PageViewModel@a704306
D/PageFragment: 2 : net.yanzm.focussample.PageViewModel@a704306
D/PageFragment: 3 : net.yanzm.focussample.PageViewModel@a704306
D/PageFragment: 0 : net.yanzm.focussample.PageViewModel@a704306

各ページで別の ViewModel インスタンスを使いたいので、ViewModel のインスタンスを取得するときにキーを指定するようにします。 val position = arguments!!.getInt("position") val viewModel = ViewModelProviders.of(activity) .get(position.toString(), PageViewModel::class.java) // position をキーとして指定 各ページで別の ViewModel インスタンスを使い、ViewPager のページ移動で Fragment が作り直されても ViewModel は以前のインスタンスが取れるようになりました。

D/PageFragment: 0 : net.yanzm.focussample.PageViewModel@a704306
D/PageFragment: 1 : net.yanzm.focussample.PageViewModel@2dc8dc7
D/PageFragment: 2 : net.yanzm.focussample.PageViewModel@a052ff4
D/PageFragment: 3 : net.yanzm.focussample.PageViewModel@389f31d
D/PageFragment: 0 : net.yanzm.focussample.PageViewModel@a704306 ← 同じ a704306 が返ってきている。


ViewPager を持っているのが Fragment の場合は、parentFragment を指定すれば ViewModel を ViewPager を持っている Fragment にひも付けることができます。 val position = arguments!!.getInt("position") val viewModel = ViewModelProviders.of(parentFragment!!) .get(position.toString(), PageViewModel::class.java) // position をキーとして指定



2018年4月11日水曜日

reified を使って lazy で intent から extra を取り出す部分を共通化する

lazy で intent から extra を取り出す部分を reified を使って Activity の拡張関数として定義してみました。 inline fun <reified T> Activity.lazyWithExtras(key: String): Lazy<T> { return lazy { intent.extras.get(key) as T } } class ProfileActivity : AppCompatActivity() { private val name: String by lazyWithExtras(EXTRAS_NAME) private val age: Int by lazyWithExtras(EXTRAS_AGE) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val textView = TextView(this) setContentView(textView) textView.text = "$name $age" } companion object { private const val EXTRAS_NAME = "name" private const val EXTRAS_AGE = "age" fun createIntent(context: Context, name: String, age: Int): Intent { return Intent(context, ProfileActivity::class.java).apply { putExtra(EXTRAS_NAME, name) putExtra(EXTRAS_AGE, age) } } } } Fragment では arguments は NonNull 前提としました inline fun <reified T> Fragment.lazyWithArgs(key: String): Lazy<T> { return lazy { arguments!!.get(key) as T } } class ProfileFragment : Fragment() { private val name: String by lazyWithArgs(ARGS_NAME) private val age: Int by lazyWithArgs(ARGS_AGE) override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) textView.text = "$name $age" } companion object { private const val ARGS_NAME = "name" private const val ARGS_AGE = "age" fun newInstance(name: String, age: Int): ProfileFragment { return ProfileFragment().apply { arguments = Bundle().apply { putString(ARGS_NAME, name) putInt(ARGS_AGE, age) } } } } }

2018年4月10日火曜日

Activity のパッケージを変えるときは activity-alias で古い Intent に対応できる

アプリのリニューアルやリファクタリングで Activity のパッケージを変更したいことがあります。
このときに問題になるのが古いショートカット機能やウィジェットです。

例えばアプリのパッケージが com.example.app で、MainActivity のパッケージも com.example.app だとします。

古いショートカット機能で MainActivity を開くショートカットを作ると、それには次のような Intent がセットされています。 val intent = Intent(context, MainActivity::class.java) // この intent の component は // ComponentName("com.example.app", "com.example.app.MainActivity") この状態で MainActivity の場所を com.example.app.ui に変更してアプリをアップデートすると、ショートカットをタップしても MainActivity が起動しなくなってしまいます。
Intent の ComponentName に対応する "com.example.app.MainActivity" が無いからです。

そこで activity-alias を使って "com.example.app.MainActivity" が指定されたときは "com.example.app.ui.MainActivity" を指すように alias を定義します。

activity-alias-element

<activity-alias> タグは <application> タグの中で使います。

<activity-alias> の android:targetActivity でこの alias 先の Activity を指定します。この指定先の Activity は <activity-alias> よりも先(上)に <activity> で定義されている必要があります。 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.example.app"> <application ...> <activity android:name=".ui.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity-alias android:name=".MainActivity" android:targetActivity=".ui.MainActivity" /> </application> </manifest>


2018年3月30日金曜日

Wallpaper の取得に permission が必要になっていたのでコードの変遷を調べてみた

昔は WallpaperManager の getDrawable() では READ_EXTERNAL_STORAGE permission が必要なかったのですが、targetSdkVersion をあげたところ必要だと怒られてしまったので、コードの変遷を追ってみました。

以下のコードを試します。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val d = WallpaperManager.getInstance(this).drawable } }

targetSdkVersion = 27

targetSdkVersion = 27 で実行すると、READ_EXTERNAL_STORAGE permission が無いと怒られました。

Caused by: java.lang.SecurityException: read wallpaper: Neither user 10278 nor current process has android.permission.READ_EXTERNAL_STORAGE.

targetSdkVersion = 26

targetSdkVersion = 26 (compileSdkVersion は 27)で実行すると怒られませんでした。代わりに Warning がログに出ます。

W/WallpaperManager: No permission to access wallpaper, suppressing exception to avoid crashing legacy app.

android-27 での変更

26と27の間でコードの変更がありました。

android-26 public Drawable getDrawable() { Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM); if (bm != null) { Drawable dr = new BitmapDrawable(mContext.getResources(), bm); dr.setDither(false); return dr; } return null; } static class Globals extends IWallpaperManagerCallback.Stub { ... public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault, @SetWallpaperFlags int which, int userId) { ... synchronized (this) { if (mCachedWallpaper != null && mCachedWallpaperUserId == userId) { return mCachedWallpaper; } mCachedWallpaper = null; mCachedWallpaperUserId = 0; try { mCachedWallpaper = getCurrentWallpaperLocked(userId); mCachedWallpaperUserId = userId; } catch (OutOfMemoryError e) { Log.w(TAG, "No memory load current wallpaper", e); } if (mCachedWallpaper != null) { return mCachedWallpaper; } } ... } android-27 public Drawable getDrawable() { Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM); if (bm != null) { Drawable dr = new BitmapDrawable(mContext.getResources(), bm); dr.setDither(false); return dr; } return null; } private static class Globals extends IWallpaperManagerCallback.Stub { ... public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault, @SetWallpaperFlags int which, int userId) { ... synchronized (this) { ... try { mCachedWallpaper = getCurrentWallpaperLocked(context, userId); mCachedWallpaperUserId = userId; } catch (OutOfMemoryError e) { Log.w(TAG, "Out of memory loading the current wallpaper: " + e); } catch (SecurityException e) { if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) { Log.w(TAG, "No permission to access wallpaper, suppressing" + " exception to avoid crashing legacy app."); } else { // Post-O apps really most sincerely need the permission. throw e; } } if (mCachedWallpaper != null) { return mCachedWallpaper; } } ... } android-26 のコードに比べて catch (SecurityException e) { ... } の部分↓が増えていました。 targetSdkVersion が27以降なら SecurityException をそのまま流すように変わったということです。 } catch (SecurityException e) { if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) { Log.w(TAG, "No permission to access wallpaper, suppressing" + " exception to avoid crashing legacy app."); } else { // Post-O apps really most sincerely need the permission. throw e; } } さらに、getCurrentWallpaperLocked() の引数に context が増えています。

android-26 private Bitmap getCurrentWallpaperLocked(int userId) { ... try { Bundle params = new Bundle(); ParcelFileDescriptor fd = mService.getWallpaper(this, FLAG_SYSTEM, params, userId); ... } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } return null; } android-27 private Bitmap getCurrentWallpaperLocked(Context context, int userId) { ... try { Bundle params = new Bundle(); ParcelFileDescriptor fd = mService.getWallpaper(context.getOpPackageName(), this, FLAG_SYSTEM, params, userId); ... } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } return null; } IWallpaperManager.getWallpaper() にも新しい引数が増えており、この context を使ってその引数に context.getOpPackageName() を渡しています。


ちなみに 27 (Android 8.1)では WallpaperColors API が追加されています。

結論

Wallpaper の Drawable を取得するには、READ_EXTERNAL_STORAGE permission を Manifest に追加して、Runtime Permission 処理を書く必要があります。


2018年3月27日火曜日

Android で Parameterized テスト(JUnit4, Robolectric)を行う

JUnit4

https://github.com/googlesamples/android-testing/tree/master/runner/AndroidJunitRunnerSample
にサンプルがあります。

具体的には CalculatorAddParameterizedTest.java がテストクラスで、
テスト対象のクラスは Calculator.java です。

上記のサンプルを見ればだいたいわかるのですが、Kotlinで書いた以下のFizzBazzをテストしてみたいと思います。 class FizzBazz { fun fizzbazz(value: Int): String { return when { value % 15 == 0 -> "fizzbazz" value % 3 == 0 -> "fizz" value % 5 == 0 -> "bazz" else -> value.toString() } } }
  • 1. テストクラスに @RunWith アノテーションで Parameterized を指定する。
  • 2. Parameterized テストで使う値をコンストラクタの引数として受け取り保持する。
  • 3. @Parameters アノテーションをつけた static メソッドを用意する(つまり @JvmStatic が必要)。このメソッドではコンストラクタ引数に対応した値の配列のIterableを返す。
  • 4. 保持した値を使ってテストする。
/** * @RunWith で Parameterized を指定する * * Parameterized テストで使う値をコンストラクタの引数として受け取る * ここでは fizzbazz() に渡す値と、fizzbazz()の戻り値の期待値の2つを引数として受け取る */ @RunWith(Parameterized::class) class FizzBazzParameterizedTest( private val value: Int, private val expected: String ) { private lateinit var fizzBazz: FizzBazz @Before fun setUp() { fizzBazz = FizzBazz() } @Test fun testFizzBazz() { assertThat(fizzBazz.fizzbazz(value)).isEqualTo(expected) } companion object { /** * @Parameters と @JvmStatic をつける * @return [Iterable] コンストラクタに渡す値のIterableを返す */ @Parameters @JvmStatic fun data(): Iterable<Array<Any>> { return listOf( arrayOf(1, "1"), arrayOf(2, "2"), arrayOf(3, "fizz"), arrayOf(4, "4"), arrayOf(5, "bazz"), arrayOf(15, "fizzbazz") ) } } }

テスト結果




Robolectric

BlockJUnit4ClassRunnerWithParameters を継承した Runner であれば、@Parameterized.UseParametersRunnerFactory でその Runner を返す ParametersRunnerFactory を指定することで、その Runner で Parameterized テストを実行することができます。

RobolectricTestRunner は SandboxTestRunner を継承しているためこの方法は使えません。 代わりに Robolectric には ParameterizedRobolectricTestRunner が用意されています。


TextUtils を使った次の EmptyChecker をテストしてみます。 class EmptyChecker { fun isEmpty(value: String?): Boolean { return TextUtils.isEmpty(value) } } 使い方は Parameterized とほぼ同じです。
違いは @RunWith に ParameterizedRobolectricTestRunner を指定することと、@ParameterizedRobolectricTestRunner.Parameters を使うことです。 /** * @RunWith で ParameterizedRobolectricTestRunner を指定する * * Parameterized テストで使う値をコンストラクタの引数として受け取る * ここでは isEmpty() に渡す値と、isEmpty()の戻り値の期待値の2つを引数として受け取る */ @RunWith(ParameterizedRobolectricTestRunner::class) class EmptyCheckerParameterizedTest( private val value: String?, private val expected: Boolean ) { private lateinit var emptyChecker: EmptyChecker @Before fun setUp() { emptyChecker = EmptyChecker() } @Test fun testFizzBazz() { assertThat(emptyChecker.isEmpty(value)).isEqualTo(expected) } companion object { /** * @ParameterizedRobolectricTestRunner.Parameters と @JvmStatic をつける * @return [Iterable] コンストラクタに渡す値のIterableを返す */ @ParameterizedRobolectricTestRunner.Parameters @JvmStatic fun data(): Iterable<Array<Any?>> { return listOf( arrayOf<Any?>(null, true), arrayOf<Any?>("", true), arrayOf<Any?>("a", false) ) } } }

テスト結果




2018年2月27日火曜日

Kotin メモ : coerceIn, coerceAtLeast, coerceAtMost

coerceIn

値を指定した Range 内に強制する println(11.coerceIn(0..10)) // 10 println((-1).coerceIn(0..10)) // 0

coerceAtLeast

値を指定した下限値以上に強制する println(11.coerceAtLeast(0)) // 11 println((-1).coerceAtLeast(0)) // 0

coerceAtMost

値を指定した上限値以下に強制する println(11.coerceAtMost(10)) // 10 println((-1).coerceAtMost(10)) // -1


2018年2月9日金曜日

Android アプリの開発でドメイン駆動設計に取り組む話

前回に引き続き、スピーチ原稿と合わせて公開します。

(講演ではアドリブもあるので原稿とは微妙に異なることをご了承ください)

追記 : 前回の内容を読んでいない方は、先にそちらを読んでください。

前回の Droid Kaigi で私は「ドメイン駆動設計とは何か」という話をしました。
本当は前回のCfPを出す時点でAndroidアプリ開発での実装の話もいれようかなと思っていたのですが、ドメイン駆動設計が何かをきちんと説明するだけでいっぱいいっぱいでした。

今回は前回の続きなので、簡単に前回の復習からはじめます。前回の話の完全版は私のブログに書いてありますので、ぜひ読んでください。
前回の説明を復習すると、ドメイン駆動設計とは

ドメインエキスパートの言葉を観察し、ドメインを構成するユビキタス言語を見つけ、
ユビキタス言語を使ってドメインを適切に反映した我々のソフトウェアに役立つドメインモデルを作り、
作ったドメインモデルを正確に表現するようコードを実装し、
これを繰り返してドメインモデルと実装の両方を洗練させていく設計手法です。
ドメイン駆動設計では、実践するために役立つさまざまな手法が出てきます。これらは主に戦略的設計と戦術的設計の2つに分けることができます。
ドメインモデルを作り上げるために役立つ手法が戦略的設計、ドメインモデルからそれを表現した実装を行っていくのに役立つ手法が戦術的設計です。
戦略的設計のユビキタス言語、境界づけられたコンテキスト、コンテキストマップについては前回お話しました。
今回は戦術的設計についてお話します。
ドメイン駆動設計では、ドメインモデルをそのまま表現するように実装します。
究極的にはコードを読めばそのドメインモデルがわかるし、そのドメインモデルを理解しているならエンジニアじゃなくてもなんとなくテストコードが読めるという状態です。

じゃあ具体的にどうすれば、ドメインモデルをそのまま表現した実装にできるのかなって思いますよね。
そこで、こういうふうに実装するとうまくドメインモデルを表現できたよ、という実装パターンが紹介されています。

それが戦術的設計です。
戦術的設計としてたくさんパターンが紹介されているのですが、これらを取り入れなくても、ドメインモデルをうまく表現した実装ができるなら、それはちゃんとドメイン駆動設計です。
例えばドメインイベントは、エリック・エヴァンスの本が出版された後に新しく付け加えられています。

戦術的設計のパターンはたくさんありますから、すべてがAndroidアプリの実装に役立つとは限りません。
例えば、実践ドメイン駆動設計にはMongoDBやSpringでの例が出てくるパターンがあるのですが、Androidアプリの開発に置き換えて考えるのはなかなか難しいです。

よって、この講演ではこれらを網羅的に紹介したりはしません。

この講演では、
長年の開発で大きく、複雑になってきたAndroidアプリにドメイン駆動設計のエッセンスを取り入れてみたいけど、どこからはじめればいいかさっぱりわからない。

という方に向けて
この一年半、私がドメイン駆動設計を念頭に、既存のAndroidアプリの実装をリファクタリングして、これはよかったなという戦術的設計のパターンと、どう取り入れたかの例を紹介します。

作り直すときの話ではありません。

いままで5つ以上のアプリで、ドメイン駆動設計をどう取り入れるか試行錯誤してきました。
複雑に絡み合ったアプリのコードを少しづつ紐解いて、UIに隠されていたドメインモデルを実装としてどう表現してきたかという話です。
もう少し前提条件というか状況をはっきりさせておきます。
こういう方法よかったよと言う場合、無条件にすべてに適用できるものではありません。どういう状況のときによかったのかをはっきりさせておかないと、お互いに不幸になります。

特にドメイン駆動設計はさまざまなソフトウェアの開発で取り組まれていますから、同じ戦術的設計のパターンであっても、対象とするドメインだけでなくプラットフォームやフレームワークによっても最適な実装が異なるでしょう。

この講演で紹介する実装例は、以下の状況を前提としています。

まず、Androidアプリの開発での話です。iOSアプリやサーバーサイドの開発には参考にならない可能性があります。

次に、少なくとも1年以上継続的に開発しているアプリを前提としています。
これは「ある程度モデルらしきものがメンバーの共通認識としてあり、コードにもそれっぽいものがあるが、ドメインモデルとしてコードでうまく表現されていない」という状況になるには1年ぐらい必要かなと思ったからです。実際にはもっと年数の経っているアプリで取り組んでいます。

そして重要な前提条件が、サーバーとAPIを介してやりとりする、よくあるクライアントアプリであるという点です。ゲームアプリなどにはあまり参考にならないかもしれません。

サーバー側が別チームというのは、前回お話した境界づけられたコンテキストに関係する部分です。サーバーとアプリで別の境界づけられたコンテキストになっているという状況です。

では、さっそく本題に入りましょう。

最初の戦術的設計パターンは「ドメインを隔離する」です。

前回の話で、「ドメインを隔離する」というのは
「ビジネスロジックをドメインモデルとして隔離する」ということだとお話しました。
なぜ隔離するのか

本では次のように書かれています。
「ドメインロジックがプログラムの他の関心事と混ざり合っていたら、設計と実装をこのように一致させることは現実的ではない」

ここでのドメインロジックは、ドメインモデルに属するロジックのことです。ドメインモデルに関連するロジック、ドメインが持っているロジックとも言えます。
例えば、ドメインモデルに属するロジックが UI に書かれていると、ドメインモデルをそのまま表現した実装にできません。
設計と実装を一致させるために隔離する、ということがわかりました。

そうすると次は、どうやって隔離するのか、です。

本では次のように書かれています。
「ドメイン層を隔離した状態に保つことさえできれば、どのアプローチでもかまわない。」
「DDDの大きな利点のひとつが、特定のアーキテクチャに依存しないということだ。」

この文章は前回も紹介しました。
つまりドメインを隔離する方法は問わないということなんです。
好きな方法でやればよいのです。なので前回は具体的な方法は紹介しませんでした。

エリック・エヴァンスのドメイン駆動設計を読んだ方なら、じゃあアレはなんなんだ、と思うかもしれません。
そう、レイヤ化アーキテクチャです。
なぜエリック・エヴァンスのドメイン駆動設計では、レイヤ化アーキテクチャが紹介されているのか。
方法は問わないと言っているのに紹介しているなら、その理由は簡単です。
ただの例です。
いきなりドメインを隔離しろと言われても、具体例がないとなかなか理解できないものです。
つまり、ドメインを隔離できそうな設計としてこういうのがあるよ、という例としてレイヤ化アーキテクチャが紹介されている、ということなんです。
あくまで目的はドメインを隔離することです。それができればどのアプローチでもかまわない。
ただし、方法については次のようにも書かれています。
「アーキテクチャがドメインに関連するコードを隔離して、凝縮度が高いドメインの設計が、システムの他の部分と疎結合できるようにしているなら、そのアーキテクチャはおそらくドメイン駆動設計を支えられるだろう」

単に置き場所を分ければいいというわけではなく、他の部分と疎結合できるようになっている必要がある、というわけです。
まとめると、システムの他の部分と疎結合できるような方法でドメインを隔離する、ということです。
Androidアプリでもドメインを隔離したくなってきましたね。
前回お話しましたが、我々はUIにドメインにある概念や知識、ビジネスロジックを詰め込みがちです。
利口な UI から脱却するにはぜひともドメインを隔離したいところ。
そうなると問題はどう隔離するか?です。
やりたいことは、
UIはドメインを知っているが、ドメインはUIを知らないようにしたいということ、
ドメインと他の部分を疎結合にしたいということ、
そして、それを強制させたいということです。

単純にパッケージを分けるだけだと、ドメインからUIのクラスを見ようと思ったら見えてしまいます。
見ないように気を付けようだと形骸化します。メンバーが変わった時だけでなく、リリースに間に合わせるために今回だけ特別、みたいなことが容易に想像できます。
そこで、ドメイン用に別モジュールを用意することにしました。
このモジュールは gradle のモジュールのことです。
このような構成にすると、ドメインがUIを知らないように強制できますし、純粋なロジック部分だけになるのでテストが書きやすいです。
また、UI を変更してもドメイン部分に変更がないのであれば、その部分の動作を担保できますし、ドメインモジュールのリビルドが走らないのでちょっとだけビルドが速くなります。
さぁ、ドメインモデルの置き場所が決まりました。
あとは、ここにドメインモデルをガンガン入れていくのですが、
何を入れたらいいかわからん...ってなりませんか?
いや、入れるのはドメインモデルってのはわかってる。

わからないのは、自分のコードの中でどれがドメインモデルであるべきなのか、どこから見つけていけばいいのか...

そこで役に立つのが次の戦術的設計のパターン、「値オブジェクト」です。
まずは、ドメイン駆動設計の本で値オブジェクトについてなんと言っているのか見てみましょう。
「あるオブジェクトが、ドメインにおける記述的な側面を表現し、概念的な同一性を持たない場合、そういうオブジェクトは、値オブジェクトと呼ばれる。」

概念的な同一性を持たない、というのは、一意に識別する必要がない、区別する必要がないということです。
例えば色は値オブジェクトの一例です。
同じ色を持つ Color オブジェクトを区別する必要はありませんよね。

ドメインモデルの中には、このような「何であるかだけが問題となり、誰であるかやどれであるかは問われないような要素」がでてきます。
これらを値オブジェクトとして実装することで、ドメインモデルを表現した実装になります。

値オブジェクトの他の面も見てみましょう。
ドメイン駆動設計では、値オブジェクトを不変にすることを勧めています。
不変であれば自由にコピーや共有ができ、複数のスレッドで安全に利用できます。また、引数や戻り値として別のオブジェクトに渡しても、その先で変更されないので設計の簡素化につながります。
例えば Color オブジェクトであれば、このように内部で保持する色の値を可変にするのはおすすめできません。
このように Color オブジェクトは不変にし、色を変更するときは Color オブジェクト自体を入れ替えるようにします。

この話を聞いて、なんか聞いたことあるなと思った方いらっしゃると思います。このイミュータビリティの話は Effective Java でも紹介されています。

さらに別の面も見てみましょう。
「値オブジェクトを構成する属性は、概念的な統一体を形成すべきである」
概念的な統一体...ちょっと難しいですね。
例えばユーザーに紐づく情報として、IDや名前の他に郵便番号、都道府県、残りのアドレスがあるとします。

このとき、郵便番号、都道府県、残りのアドレスは、ユーザーの別々の属性ではなく、住所という統一体を形成する属性です。

よって、この3つで形成される住所という値オブジェクトを用意し、ユーザーには住所を持たせるようにします。
値オブジェクトはドメイン駆動設計において、ドメインモデルを構成する基本的な要素です。エンティティやドメインサービスなど他の戦術的設計パターンではみな値オブジェクトを利用します。

つまり、値オブジェクトとなるドメインモデルを見つけるところから始めるのが最適ということです。

いやいや、そうは言っても、Color とか Android フレームワークにあるし、値オブジェクトにするもの見つかるかな?

そんなあなたのために、今回は Android アプリの開発で値オブジェクトとしてモデリングした例をたくさん用意しました。
例を紹介する前に、Android アプリで値オブジェクトをどう実装するかですが、
実装要件である
状態を不変に保つ、つまりイミュータブルにできる
値が等しいかどうか比較できる
全体を完全に置き換えられる
に注目すると、
Kotlin の data class が最適です。イミュータブルにしたいのでプロパティは val ですね。

Java ならフィールドを全て final にして equals() と hashCode() を override するか、AutoValue などのライブラリを使うという手もあります。ですが、これを機に Kotlin を導入してみてはいかがでしょうか。
では Android アプリで値オブジェクトを見つけていきましょう。
UserId, ItemId, ProductId, OrderId などなど、アプリの中で一つぐらい ID というものを扱っていると思います。
このID、文字列で取り回していませんか?
この ID を値オブジェクトにするとどうなるか、最初の例を見てみましょう。
ユーザーID からフォロワーの一覧を取得するAPIです。

フォロワー一覧画面を作るとき、このAPIを呼び出す側も呼び出される側も同じ人が実装すると、何を渡せばいいのかわかっているので String であっても特に困ることなく実装できます。

後になって、別の人が他の場所からこのAPIを呼ぶことになったり、機能を変更することになったら、次のような問題が出てきます。
userId ってどこにある id だっけ?
Profile クラスの持ってる id ってここに渡していいやつだっけ?
空文字ってだめだよね?呼び出す前にチェック必要?
userId が空文字になるわけないんだけど、でも Profile が持ってる id に空文字が入らないなんて他の部分も読まないと保証できないし...
そこで UserId を値オブジェクトにしましょう。

UserId のインスタンスがあるなら、それが保持している文字は絶対に空文字ではないと保証できる状態にします。

こうすれば、利用側は安心して getFollower() API を呼び出すことができます。
Profile クラスが持っている id が UserId 型なら、この API に渡してよいいIDであることが明白です。
さらに String から UserId 型にすることで、どこで利用されているかを静的に解析でき、リファクタリング時の影響範囲も調べやすくなります。
値オブジェクトとして ID を表現するということは、ユビキタス言語の成長にも繋がります。

例えばアプリの中に categoryId というのがあったので、値オブジェクトにしようと思って利用箇所などを調べていると
genreId というのもあることに気づいてしまいました。よくよく話を聞くと
この2つは同じものでした。

この ID 用の値オブジェクトを用意するにはクラスの名前が必要です。この名前はユビキタス言語になるものです。
そこで、どちらをユビキタス言語にするか話し合って GenreId に統一しようということになりました。
GenreId を値オブジェクトとして用意するとこうなります。
GenreId の変数が categoryId なのすごく違和感ありますよね。

ID を値オブジェクトにできないか考えたことで、言語の問題に気づけ、チームとして新しいユビキタス言語を見つけることができました。まぎらわしい変数名がつけられるのを防ぐこともできます。
次は ID が複数の情報を含んでいる場合の例です。
商品 ID が、ブランドの ID とブランド内でのコードから構成されています。

商品画面からブランド一覧画面に遷移するために、商品 ID の文字列を処理してブランド ID 部分を取り出すようになっていました。

id が空文字だったり : を含まない文字列だったら、意図しない値で BrandActivity を開くことになる、という問題もありますが、
ここでの一番の問題は、UI が知るべきではない商品 ID のフォーマットが漏れてしまっているということです。
もうわかりますね、
値オブジェクトとして ProductId を用意しましょう。
正しいフォーマットから ProductId が構成されることを保証できますし、UI 側は商品 ID の文字列表現がどうなっているかを知らなくてすみます。
値オブジェクトの見つけ方わかってきたでしょうか?

では次の例に行きます。
ユーザー登録画面の作成を依頼されました。
性別と生年月日を渡す必要があります。
作り始めて気がつきました。

性別ってどんな文字列送ればいいの?

というか、そもそも扱いたいのは性別そのものであって、性別を表現した文字列じゃないよね。ということは性別はモデルでは...

同じ性別なら、性別として区別する必要はないのでこれも値オブジェクトです。
このようなとりうる状態が限定されている値オブジェクトは enum で表現できますね。
性別の文字列について UI が知らなくて済むようになりました。
しかし、まだ問題があります。
誕生日の方もどういう文字列を送ったらいいかわかりませんね。

こちらも考えてみましょう。

同じ日を指している日付は区別する必要がないので、値オブジェクトとして表現できます。

誕生日など特定の意味をもった日付はドメインモデルです。
DateOfBirth クラスとして実装するとこのようになるでしょう。
どういうフォーマットで文字列にするのかを UI から隠蔽できます。
引数の型として DateOfBirth を使うことで、何を渡せばいいのか明白になりました。
次の例に行きましょう。
画像の縦横サイズとURLを持つ Image というオブジェクトがあります。画像のアクペクト比が 4 : 3 より横長、例えば 16 : 9 とかだったら、 wide 用の placeholder を使う、という仕様です。
このコードの問題は、wide かどうかの判定処理が UI に書かれているところですね。ImageView に画像をロードするあちこちの処理で同じコードが書かれていそうです。
UI に書くのはよくないよね、ということで次にありがちなのが、ユーティリティクラスです。

判定処理の重複はなくなりましたが、ドメイン駆動設計としては問題があります。
Image クラスがただのデータモデルになっているという点です。このような状態をドメインモデル貧血症といいます。
wide かどうかの判定はドメインモデル、つまり Image に属するロジックなので、Image に持たせましょう。


ロジックが誰の責務なのか考えるの難しいですよね。私も迷うことがよくあります。この例にしても、wide かどうかの情報は placeholder を使い分けるために使っているから、UI のロジックであってドメインのロジックではないのでは?と思うかもしれません。

この例では Image に置きましたが、表現するドメインモデルが異なれば異なる実装になるでしょう。画一的は判断基準はなく、頭の中のドメインモデルにとって自然かどうかだけです。 wide かどうかという属性は Image というドメインモデルにとって自然かどうか。

最初からしっくりくる実装ができないことはよくあります。大事なのは、その実装で固定化せず、よりよい方法が思いついたときにリファクタリングする、できるようにしておくことです。

次の例にいきましょう。
サーバーのAPIを叩いて JSON などのレスポンスをもらい、それをオブジェクトにマッピングするという処理は、多くの Android アプリで行われています。

このとき、JSONやXMLの構成をそのまま反映しただけのクラスにしていませんか?
例えば商品の情報を取得するAPIからこのようなレスポンスが返ってくるとします。
この images の配列には、同じ画像の異なるサイズへのURLが入っていて、アプリでは表示する領域の大きさに応じて、この中から適切なものを使う仕様になっています。
この JSON をそのままマッピングすると、このようなクラスになります。
アプリでは表示する領域の大きさに応じて、この中から適切なものを選んで使う仕様になっているので、どの Image を使うか判定するロジックが必要になります。

さぁ、このロジックをどこに置きましょうか?
複数箇所で同じことをやるからとか、UI にロジックを置くのはよくないからなどの理由で、このようなユーティリティクラスに置かれることがあります。
この場合、Item クラスは値を持っているだけで、そのデータを使った判断や加工処理は別のところで行われることになります。
このような状態は、先ほどと同じドメインモデル貧血症ですね。
Item が貧血になっているので、Item にこのロジックを置いてみました。

ところが問題が起こります。
Item の他に Category というのがあり、こちらでも Image を選択する処理が必要でした。
Item と Category に同じロジックが重複することになってしまい、よくありません。
じゃあどうするか。

JSON の形式を一旦忘れて、「表示する領域の大きさに応じて適切な Image を選択するロジック」は誰の責務なのかを考えましょう。
本当に Item の責務なのかな?
Item や Category の責務ではなく、Image の集合という別のドメインモデルの責務なんじゃないかな。
Image の集合を表現するドメインモデルとして Images クラスを導入してみたらどうだろう。
適切な Image を選択するロジックの置き場所として自然な感じがしませんか?
サーバーのレスポンス形式にとらわれず、別のドメインモデルがあるのではないか、ドメインモデルを適切に表現する値オブジェクトがあるのではないか考えてみてください。
次の例にいきましょう。
今度はお知らせの一覧を取得したときのレスポンスです。
各お知らせにはバナーが含まれている場合があり、バナーには表示期間の開始日時と終了日時があります。
これをそのままマッピングすると、このようなクラスになります。

これを使うとしたらどうでしょう。
bannerImageUrl は null じゃないけど、bannerStartDate が null のときはどうするの?って思いませんか?
この banner に関する3つの属性は、それぞれがお知らせに属するのではなく、この3つで概念的な統一体を構成しています。
つまり、この3つの属性をもった値オブジェクトが必要ということです。それを Banner クラスとして用意すると、このようになります。

banner のインスタンスがあれば url も日付も揃っているということを保証できます。
さらに、開始日時が終了日時より前であることを事前条件として保証できますし、今日が表示期間内か判定するロジックの置き場所としても自然です。
次の例に行きましょう。
このアプリにはユーザーが使える電子アイテムがあります。
アイテムにはログインしていなくても使えるものや、ログインしていれば使えるもの、有料会員になっていれば使えるものなどがあり、サーバーからは複数のフラグが返されます。

昔は2種類しかなかったなど、歴史的経緯とか拡張とかで、こういうレスポンスになっています。
今までの話の流れから想像つくと思いますが、こういうクラスにマッピングされていました。
UI 側で if 文を駆使した処理が行われていることは容易に想像できます。

ここでも一旦レスポンス形式のことを忘れましょう。

アイテムをドメインモデルとして考えると、アイテムの属性として自然なのは個々の Boolean よりも、どういうアイテムなのかという種類です。
そうなると次はどういう種類があるのか、ということになりますが、ここで何も考えずに Boolean 4つの組み合わせなので、2の4乗で16種類あるね、としてはいけません。

次にすることは、ドメインエキスパートに話を聞きに行くことです
どういう種類のアイテムがあると認識しているのか聞いてみると、結局は5種類しかないことがわかりました。
この5種類を enum にすればよいですね。

Boolean の組み合わせは16パターンあるのに、アイテムは5種類しかないということは、ありえない組み合わせや縮退している組み合わせがあるということです。

判明した種類を元に、サーバーチームにどういうときにどういう値が返ってくるのか確認しましょう。
例えば、

isMemberUsable は都度課金用のアイテムができたときに isPurchaseRequired と一緒に追加したので、isPurchaseRequired が true のときしか意味ないです。

とか、
isPurchaseRequired が true の時は isLoginNeeded と isMembershipRequired は意味ないので無視してください。

などの情報が得られました。
あとは得られた情報から enum に変換しましょう。
どこで変換するかって?
腐敗防止層ですよ。前回出てきましたね。
同じ属性値を持つなら区別が必要ない、というモデルがあり、それを値オブジェクトで表現する例をいろいろ紹介してきました。

しかし、モデルの中には同じ属性値であっても区別が必要なものがあります。例えば、AさんとBさんが同じ山田太郎という名前であっても、別の人として区別が必要です。

このようなモデルを表現するオブジェクトとしてドメイン駆動設計にはエンティティという戦術的設計パターンがあります。

本ではこのように書かれています。
「主として同一性によって定義されるオブジェクトはエンティティと呼ばれる。」

人間には同一性があります。例えば年齢や見た目が変わってもAさんが同じ人だと認識しますよね。
「ライフサイクルにおいて、エンティティの形態と内容は根本的に変わることがあるが、連続性のつながりは維持されなければならない。」

エンティティにはライフサイクルがあります。人がその一生の間に変化していくように、ライフサイクルの間にエンティティの内容は変わることがあります。そのとき、新しく作られるのではなく、以前の内容が変わったという連続性が維持される必要があります。
この連続性と同一性をソフトウェアで表現するときに問題になるのが、連続性を維持するためにどう永続化するかと、同一性を持たせるための識別子をどう発行するかです。

これらは技術的な制約と関係することもあり、エリック・エヴァンスのドメイン駆動設計でも実践ドメイン駆動設計でも具体的な実装を出して議論されています。

Android アプリでも、スタンドアローンで、マスターデータをアプリ内のデータベースに保存するような場合には、これらをどう実装するかが問題になります。

しかし、マスターデータがサーバーに保存され、識別子もサーバーで発行されるクライアントアプリでは、これらをアプリ側で行うことがありません。

現時点で Android アプリでのエンティティについて私が言えることは、識別子を使ってオブジェクトを比較するように実装すべきということだけです。

UserId で一意に識別すべき User なら、UserId で識別するよう equals() と hashCode() を override すべきでしょう。


Android アプリでのエンティティの扱いについてはまだまだ試行錯誤中で、例えば SharedPreferences に保存している、初回起動かどうかのフラグや通知を受け取るかどうかの設定はエンティティなのか、など悩みはつきません。

この話は機会があれば次回できるといいなと思っています。

では、まとめです。
ドメインを隔離するという目的に対し、gradle のモジュールで分ける方法を紹介しました。モジュールに分けることで依存方向を強制でき、ドメインが UI などのその他の部分を知らない状態に隔離できます。

同じ属性値をもっているなら区別する必要がないモデルは値オブジェクトとして表現できるということを紹介しました。
属性は不変にし、値オブジェクト自体を差し替えることで変更に対応しましょう。
モデルに属するべきロジックを持たせましょう。モデルの表現として、条件を満たしているときだけインスタンス化しましょう。
文字列で取り回している要素が値オブジェクトではないか考えましょう。ID、種類、日付、サイズなどは値オブジェクトを導入するよいスタート地点です。

サーバーのレスポンス形式は時としてモデルをうまく表現できていないことがあります。それに引きづられてドメインモデル貧血症になっていませんか?一旦レスポンス形式のことは忘れて、どういうモデルがあるべきかを考えましょう。
以上です。ご静聴ありがとうございました。