Slide 1

Slide 1 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 俺が今までやらかした失敗事例 やらかしそうになった ヒヤリハット事例を紹介する 大前良介 (OHMAE Ryosuke) DroidKaigi 2020

Slide 2

Slide 2 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 自己紹介 •大前良介(OHMAE Ryosuke) • https://github.com/ohmae • twitter: ryo_mm2d • qiita: ryo_mm2d •ヤフー株式会社 @グランフロント大阪 • Androidアプリエンジニア • Yahoo!天気アプリ担当

Slide 3

Slide 3 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 過去のDroidKaigi •DroidKaigi 2018 • タッチイベントを捕まえよう •DroidKaigi 2019 • Chrome Custom Tabsの仕組みから学ぶプロセス間通信 •DroidKaigi 2020 • 俺が今までやらかした失敗事例、やらかしそうになったヒヤリハット事例を紹介する イマココ

Slide 4

Slide 4 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 本発表には以下の成分が含まれます • 脈絡のない失敗事例の列挙 • Android開発で発生する問題あるある • 合計○○円分の損失を出しました、などの内容は含まれません • 突如始まるエモい話

Slide 5

Slide 5 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 予防線 • 本発表は個人の見解であって、所属組織を代表するもので はありません • あくまであるある事例の紹介 • やらかしたのか、ヒヤリハットですんだのかも秘密 察してください(重要)

Slide 6

Slide 6 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. startActivity でクラッシュ

Slide 7

Slide 7 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. android.content.ActivityNotFoundException: No Activity found to handle Intent { xxxx }

Slide 8

Slide 8 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. ActivityNotFoundException •Intentを受け取れるActivityが見つからない • 外部特定アプリを起動するIntentで仕様がかわった • 暗黙的Intentを投げたが通常あるであろうアプリがインストール されていない • システム設定を呼び出したが、特定メーカーのカスタム設定画面 は呼び出せなかった •ほとんどの環境、テスト環境でも問題無い

Slide 9

Slide 9 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 対策 •try/catchしましょう • 特に外部アプリを起動するときは必ずcatchするクセを fun Context.startActivitySafely(intent: Intent) { runCatching { startActivity(intent) } }

Slide 10

Slide 10 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. android.os.TransactionTooLargeException at android.os.BinderProxy.transactNative(Native Method) at android.os.BinderProxy.transact(Binder.java:496)

Slide 11

Slide 11 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. TransactionTooLargeException •トランザクションデータが大きすぎる • プロセス内のトランザクションバッファ1MB • プロセス内の合計が超えるとアウト • IntentのExtra/onSaveInstanceStateのBundle •正直制限が厳しい • 誰だよこんなデータ突っ込んだの (レガシーコードあるある)

Slide 12

Slide 12 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 対策 •IntentやsavedStateに大きなデータを置かない、 キーとなるパラメータだけを置く • Activity内 :ViewModel • Activity間 :キャッシュ・永続化データ・再取得 • プロセス間 :AIDL

Slide 13

Slide 13 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 消えた ウィジェット

Slide 14

Slide 14 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. アップデートとともに消える ウィジェット • ウィジェットが消えた! • 表示できませんに変わった! ※OSやホームアプリによって症状が違う

Slide 15

Slide 15 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 原因 •リファクタリングでWidgetProviderの ComponentNameが変わった • WidgetProviderのComponentNameに変化があると 設置済みのウィジェットと紐付けができなくなる

Slide 16

Slide 16 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 原因 •リファクタリングでWidgetProviderの ComponentNameが変わった • WidgetProviderのComponentNameに変化があると 設置済みのウィジェットと紐付けができなくなる 消えるのは必然

Slide 17

Slide 17 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 起動できない ショートカット

Slide 18

Slide 18 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. ホーム画面に作成したショートカットが! •消えた! •タップしてもアプリが起動しない! ※追従してくれる場合もある

Slide 19

Slide 19 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 原因 •リファクタリングでActivityの ComponentNameが変わった • アプリドロワーはIntentFilterから列挙 • ショートカットはComponentNameで管理 •ComponentNameが変わると紐付けが外れる

Slide 20

Slide 20 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 外された デフォルトアプリ

Slide 21

Slide 21 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. デフォルトアプリ •セレクターを経由しないで起動する • ブラウザーやホームアプリにとって重要 • アプリ側からデフォルトにすることは不可 •せっかく設定してもらえたのに はずれた!?

Slide 22

Slide 22 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 原因 •リファクタリングでActivityの CompnentNameが(ry • デフォルトアプリもCompnentNameで識別 •ComponentNameが変わると紐付けが外れる

Slide 23

Slide 23 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. AndroidManifest •アプリの外部仕様の宣言 • データを扱うのはアプリ外 • 削除と追加はできてもマイグレーションは不可能 •Manifestへの変更はプロトコルの変更である • 過去・未来・外部アプリからの影響を考慮する

Slide 24

Slide 24 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 対策 •Manifestの変更を極力回避する • 変更しなくてよいように新規作成、追加時に検討する •リファクタリング・アーキテクチャ変更 • Activity:activity-alias • その他:ロジックの乗らない踏み台となる層を残す class OldService: NewService() class OldReceiver: NewReceiver()

Slide 25

Slide 25 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. Android 4.x

Slide 26

Slide 26 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. Android 4.3以下で ClassNotFoundException •Objects

Slide 27

Slide 27 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. Android 4.3以下で ClassNotFoundException •Objects • Java7で追加されたクラス Objects.requireNonNull(hoge)

Slide 28

Slide 28 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. Android 4.3以下で ClassNotFoundException •Objects • Java7で追加されたクラス • デバッグビルドだと動く Objects.requireNonNull(hoge)

Slide 29

Slide 29 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. Android 4.3以下で ClassNotFoundException •Objects • Java7で追加されたクラス • デバッグビルドだと動く • リリースビルドするとClassNotFoundException Objects.requireNonNull(hoge)

Slide 30

Slide 30 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. Android 4.3以下で ClassNotFoundException •Objects • Java7で追加されたクラス • デバッグビルドだと動く • リリースビルドするとClassNotFoundException なんでや! Objects.requireNonNull(hoge)

Slide 31

Slide 31 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. Android 4.3以下でClassCastException Caused by: java.lang.ClassCastException: android.content.res.XmlBlock$Parser cannot be cast to java.lang.AutoCloseable context.resources.getXml(resId).use { loadFromResource(context, it, target) }

Slide 32

Slide 32 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. Android 4.3以下でClassCastException Caused by: java.lang.ClassCastException: android.content.res.XmlBlock$Parser cannot be cast to java.lang.AutoCloseable context.resources.getXml(resId).use { loadFromResource(context, it, target) }

Slide 33

Slide 33 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable { String getAttributeNamespace (int index); public void close(); } API 19

Slide 34

Slide 34 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable { String getAttributeNamespace (int index); public void close(); } public interface XmlResourceParser extends XmlPullParser, AttributeSet { public void close(); } API 18 API 19

Slide 35

Slide 35 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. Kotlinの罠 •jdk7の意味 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

Slide 36

Slide 36 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. Kotlinの罠 •jdk7の意味 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" public inline fun T.use(block: (T) -> R): R { var exception: Throwable? = null try { return block(this) } catch (e: Throwable) { exception = e throw e } finally { this.closeFinally(exception) } } https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/jdk7/src/kotlin/AutoClo seable.kt

Slide 37

Slide 37 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 対策 •Android 4.xのサポートを切る • サポートを維持するコストとリスクを把握しましょう •Java7/8の機能を使うときは要注意 • 使えないクラスを使っても警告が出ない場合がある compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 }

Slide 38

Slide 38 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 消えた9-patch

Slide 39

Slide 39 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 9-patchで作った境界線 見えなくなる端末がある?

Slide 40

Slide 40 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 9-patchとは 拡大時に引き延ばす領域 コンテンツ領域

Slide 41

Slide 41 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 9-patchとは 拡大時に引き延ばす領域 コンテンツ領域 1px以上あるのは無駄じゃん?

Slide 42

Slide 42 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 一切無駄のない9-patch ふつくしい……

Slide 43

Slide 43 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved.

Slide 44

Slide 44 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. あっ

Slide 45

Slide 45 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved.

Slide 46

Slide 46 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. なにが起こっていたか •9-patchを無駄なく1pxの画像にした

Slide 47

Slide 47 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. なにが起こっていたか •9-patchを無駄なく1pxの画像にした •xxhdpiしか用意していなかった • 縮小方向なら品質的な問題も起こりにくい

Slide 48

Slide 48 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. なにが起こっていたか •9-patchを無駄なく1pxの画像にした •xxhdpiしか用意していなかった • 縮小方向なら品質的な問題も起こりにくい •1pxの画像を縮小すると? \(^o^)/

Slide 49

Slide 49 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 対策 •解像度ごとのリソースを用意する •mdpiに縮小しても問題無い画像を利用する

Slide 50

Slide 50 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. アップデートで クラッシュ その1

Slide 51

Slide 51 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. getStringExtraでClassNotFoundException? Caused by: java.lang.RuntimeException: Parcelable encountered ClassNotFoundException reading a Serializable object (name = xxx) at android.os.Parcel.readSerializable(Parcel.java:2378) at android.os.Parcel.readValue(Parcel.java:2197) at android.os.Parcel.readArrayMapInternal(Parcel.java:2479) at android.os.BaseBundle.unparcel(BaseBundle.java:221) at android.os.BaseBundle.getString(BaseBundle.java:918) at android.content.Intent.getStringExtra(Intent.java:4816) at ...

Slide 52

Slide 52 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. getStringExtraでClassNotFoundException? Caused by: java.lang.RuntimeException: Parcelable encountered ClassNotFoundException reading a Serializable object (name = xxx) at android.os.Parcel.readSerializable(Parcel.java:2378) at android.os.Parcel.readValue(Parcel.java:2197) at android.os.Parcel.readArrayMapInternal(Parcel.java:2479) at android.os.BaseBundle.unparcel(BaseBundle.java:221) at android.os.BaseBundle.getString(BaseBundle.java:918) at android.content.Intent.getStringExtra(Intent.java:4816) at ... Enum?

Slide 53

Slide 53 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. val intent = Intent(ACTION_HOGE) intent.putExtra(EXTRA_KEY_FUGA, VALUE_FUGA) val pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0) val value = intent.getStringExtra(EXTRA_KEY_FUGA)

Slide 54

Slide 54 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. val intent = Intent(ACTION_HOGE) intent.putExtra(EXTRA_KEY_FUGA, VALUE_FUGA) val pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0) val value = intent.getStringExtra(EXTRA_KEY_FUGA) Stringを設定

Slide 55

Slide 55 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. val intent = Intent(ACTION_HOGE) intent.putExtra(EXTRA_KEY_FUGA, VALUE_FUGA) val pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0) val value = intent.getStringExtra(EXTRA_KEY_FUGA) Stringを設定 Stringを読み出し

Slide 56

Slide 56 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. どこに問題が?

Slide 57

Slide 57 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. val intent = Intent(ACTION_HOGE) intent.putExtra(EXTRA_KEY_HOGE, HogeEnum.HOGE) val pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0) val value = intent.getSerializableExtra(EXTRA_KEY_HOGE) 前バージョン

Slide 58

Slide 58 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. val intent = Intent(ACTION_HOGE) intent.putExtra(EXTRA_KEY_HOGE, HogeEnum.HOGE) val pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0) val value = intent.getSerializableExtra(EXTRA_KEY_HOGE) 前バージョン Enumを設定

Slide 59

Slide 59 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. val intent = Intent(ACTION_HOGE) intent.putExtra(EXTRA_KEY_HOGE, HogeEnum.HOGE) val pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0) val value = intent.getSerializableExtra(EXTRA_KEY_HOGE) 前バージョン Enumを設定 Serializableとして読み出し

Slide 60

Slide 60 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. val intent = Intent(ACTION_HOGE) intent.putExtra(EXTRA_KEY_HOGE, HogeEnum.HOGE) val pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0) val value = intent.getSerializableExtra(EXTRA_KEY_HOGE) 前バージョン Enumを設定 Serializableとして読み出し これが見つからない?

Slide 61

Slide 61 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. val intent = Intent(ACTION_HOGE) intent.putExtra(EXTRA_KEY_HOGE, HogeEnum.HOGE) val pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0) val value = intent.getSerializableExtra(EXTRA_KEY_HOGE) 前バージョン Enumを設定 Serializableとして読み出し これが見つからない? Keyも違うよ?

Slide 62

Slide 62 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 前バージョンのPendingIntentを 新バージョンで受け取った? …だとしても読み出ししていないよ?

Slide 63

Slide 63 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. ソースを追ってみる public @Nullable String getStringExtra(String name) { return mExtras == null ? null : mExtras.getString(name); } Intent

Slide 64

Slide 64 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. ソースを追ってみる public @Nullable String getStringExtra(String name) { return mExtras == null ? null : mExtras.getString(name); } @Nullable public String getString(@Nullable String key) { unparcel(); final Object o = mMap.get(key); try { return (String) o; } catch (ClassCastException e) { typeWarning(key, o, "String", e); return null; } } Intent Bundle

Slide 65

Slide 65 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. ソースを追ってみる void unparcel() { synchronized (this) { final Parcel source = mParcelledData; if (source != null) { initializeFromParcelLocked(source, true, mParcelledByNative); } else { if (DEBUG) { Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) + ": no parcelled data"); } } } } Bundle

Slide 66

Slide 66 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. IntentのgetXXXの挙動 •Bundle(Extras)から読み出す

Slide 67

Slide 67 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. IntentのgetXXXの挙動 •Bundle(Extras)から読み出す •Bundleは内部データがunparcel前であれば、 内部データ全体をunparcelする

Slide 68

Slide 68 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. IntentのgetXXXの挙動 •Bundle(Extras)から読み出す •Bundleは内部データがunparcel前であれば、 内部データ全体をunparcelする •Extraの中に一つでもunparcel/deserializeでき ないデータが含まれているとException

Slide 69

Slide 69 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 対策 •PendingIntentを含む、アプリ外へ出て行く Intentに独自クラスを入れることを原則禁止 •先にActionなどで分岐 •アプリ外から受け取るIntentのExtraに不用意にア クセスしない • 読み出す場合はtry/catch必須

Slide 70

Slide 70 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. try/catch付きの拡張関数を用意するとか fun Intent.getBooleanExtraSafely(key: String, default: Boolean): Boolean = runCatching { getBooleanExtra(key, default) }.getOrDefault(default) fun Intent.getIntExtraSafely(key: String, default: Int = 0): Int = runCatching { getIntExtra(key, default) }.getOrDefault(default)

Slide 71

Slide 71 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 可能性の話 •悪意あるIntentを投げることもできてしまう • 適当なSerializableをIntentにいれて投げるだけ • 対策をしていなければクラッシュ • 意図せず加害者になる可能性も

Slide 72

Slide 72 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 可能性の話 •悪意あるIntentを投げることもできてしまう • 適当なSerializableをIntentにいれて投げるだけ • 対策をしていなければクラッシュ • 意図せず加害者になる可能性も 万全の対策を!

Slide 73

Slide 73 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. アップデートで クラッシュ その2

Slide 74

Slide 74 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. SharedPreferencesで ClassCastException !? java.lang.ClassCastException: java.lang.Boolean cannot be cast to java.lang.Integer at android.app.SharedPreferencesImpl.getInt( SharedPreferencesImpl.java:302)

Slide 75

Slide 75 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 何が起こったか? sharedPreferences .getBoolean("HOGE_HOGE", false) 過去バージョン

Slide 76

Slide 76 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 何が起こったか? sharedPreferences .getBoolean("HOGE_HOGE", false) 削除 過去バージョン

Slide 77

Slide 77 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 何が起こったか? sharedPreferences .getBoolean("HOGE_HOGE", false) sharedPreferences .getInt("HOGE_HOGE", 0) 削除 過去バージョン 新バージョン

Slide 78

Slide 78 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 何が起こったか? sharedPreferences .getBoolean("HOGE_HOGE", false) sharedPreferences .getInt("HOGE_HOGE", 0) 削除 同じKey! 過去バージョン 新バージョン

Slide 79

Slide 79 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved.

Slide 80

Slide 80 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 要するに •同バージョン内では整合性がとれている • ユニットテストなどでは問題なし

Slide 81

Slide 81 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 要するに •同バージョン内では整合性がとれている • ユニットテストなどでは問題なし •前バージョンには存在しないキー • アップデートを含むシナリオテストでも問題なし

Slide 82

Slide 82 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 要するに •同バージョン内では整合性がとれている • ユニットテストなどでは問題なし •前バージョンには存在しないキー • アップデートを含むシナリオテストでも問題なし •一般ユーザー:削除前からのユーザー多数 \(^o^)/

Slide 83

Slide 83 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. SharedPreferences •自由度が高すぎ、型安全でない private Map mMap; public String getString(String key, @Nullable String defValue) { synchronized (mLock) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } }

Slide 84

Slide 84 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. SharedPreferences •Keyを適切に管理する責任はアプリ側にある •内容はアプリをアップデートしても残り続ける •無法地帯化しやすい

Slide 85

Slide 85 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 対策

Slide 86

Slide 86 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. class Preferences( context: Context, kClass: KClass ) where K : Enum<*>, K : Key { private val sharedPreferences: SharedPreferences = context.getSharedPreferences(kClass.simpleName, MODE_PRIVATE) fun writeBoolean(key: K, value: Boolean) { if (BuildConfig.DEBUG) key.checkSuffix(value) sharedPreferences.edit().putBoolean(key.name, value).apply() } fun readBoolean(key: K, defaultValue: Boolean): Boolean = ... fun writeInt(key: K, value: Int): Unit = ... fun readInt(key: K, defaultValue: Int) = ... 1. KeyをEnumで管理

Slide 87

Slide 87 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. interface Key { enum class Main : Key { PREFERENCES_VERSION_INT, KEY_BOOLEAN, KEY_INT, KEY_LONG, KEY_FLOAT, @Deprecated("removed:v1.1.1") KEY_STRING, ... } enum class Temp : Key { PREFERENCES_VERSION_INT, ...

Slide 88

Slide 88 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. interface Key { enum class Main : Key { PREFERENCES_VERSION_INT, KEY_BOOLEAN, KEY_INT, KEY_LONG, KEY_FLOAT, @Deprecated("removed:v1.1.1") KEY_STRING, ... } enum class Temp : Key { PREFERENCES_VERSION_INT, ... 2. Key名に型名をつける カッコ悪い……

Slide 89

Slide 89 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. interface Key { enum class Main : Key { PREFERENCES_VERSION_INT, KEY_BOOLEAN, KEY_INT, KEY_LONG, KEY_FLOAT, @Deprecated("removed:v1.1.1") KEY_STRING, ... } enum class Temp : Key { PREFERENCES_VERSION_INT, ... 2. Key名に型名をつける 3. 使わなくなったキーを削除 しないことをルール化 カッコ悪い……

Slide 90

Slide 90 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. interface Key { enum class Main : Key { PREFERENCES_VERSION_INT, KEY_BOOLEAN, KEY_INT, KEY_LONG, KEY_FLOAT, @Deprecated("removed:v1.1.1") KEY_STRING, ... } enum class Temp : Key { PREFERENCES_VERSION_INT, ... 2. Key名に型名をつける 4. 過去を清算しやすく • バージョンをつける • 一時的設定値を分離 3. 使わなくなったキーを削除 しないことをルール化 カッコ悪い……

Slide 91

Slide 91 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. class PreferenceService( private val main: Preferences, private val temp: Preferences ) { fun getHoge(): Int = main.readInt(Main.KEY_INT, 0) fun setHoge(value: Int) = main.writeInt(Main.KEY_INT, value) 5. アプリ全体からはKeyを隠蔽

Slide 92

Slide 92 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. internal fun Enum<*>.checkSuffix(value: Any) { when (value) { is Boolean -> require(name.endsWith(SUFFIX_BOOLEAN)) is Int -> require(name.endsWith(SUFFIX_INT)) is Long -> require(name.endsWith(SUFFIX_LONG)) is Float -> require(name.endsWith(SUFFIX_FLOAT)) is String -> require(name.endsWith(SUFFIX_STRING)) } } fun writeBoolean(key: K, value: Boolean) { if (BuildConfig.DEBUG) key.checkSuffix(value) sharedPreferences.edit().putBoolean(key.name, value).apply() } 6. 名前ルールは実行時チェック

Slide 93

Slide 93 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. マイグレーションミス

Slide 94

Slide 94 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 永続化データのマイグレーションミス •忘れがち • バージョンは一つ一つ上がるわけではない • 1→2、2→3のマイグレーションを想定していても1→3が正しく 動作しているか? •過去の清算も計画的に • マイグレーションを実装するときに、いつまでマイグレーションをサ ポートするかを決めておく • 一定以上古い場合は全クリアした方が安全

Slide 95

Slide 95 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 消えたリソース

Slide 96

Slide 96 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. shrinkResource •リソースをID参照していない • getIdentifier • パス参照 resources.getIdentifier( "ic_launcher", "drawable", packageName) "file:///android_res/drawable/ic_launcher.png"

Slide 97

Slide 97 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. getIdentifierの罠 •連番のついたリソース • どのリソースが使われているのか分からない! (1..9).map { resources.getIdentifier( "ic_$it", "drawable", packageName) }

Slide 98

Slide 98 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 対策 •getIdentifierを原則禁止 • ローカルリソースは必ずIDで参照する • 連番リソースもarrayやmapなどで保持する •パス参照 • ID参照にする • assetsに移動

Slide 99

Slide 99 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. Gravityの罠

Slide 100

Slide 100 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. SlideでInvalid slide direction? java.lang.IllegalArgumentException: Invalid slide direction at android.transition.Slide.setSlideEdge(Slide.java:165) at android.transition.Slide.(Slide.java:112) at ...

Slide 101

Slide 101 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 何が起こっている? if (animate) { val transition = Slide(Gravity.END) .setDuration(150L) .setInterpolator(DecelerateInterpolator()) fragment.enterTransition = transition } activity.supportFragmentManager.beginTransaction() .replace(R.id.server_detail_container, fragment) .commitAllowingStateLoss()

Slide 102

Slide 102 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 何が起こっている? if (animate) { val transition = Slide(Gravity.END) .setDuration(150L) .setInterpolator(DecelerateInterpolator()) fragment.enterTransition = transition } activity.supportFragmentManager.beginTransaction() .replace(R.id.server_detail_container, fragment) .commitAllowingStateLoss() ここ?

Slide 103

Slide 103 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. public void setSlideEdge(@GravityFlag int slideEdge) { switch (slideEdge) { case Gravity.LEFT: mSlideCalculator = sCalculateLeft; break; case Gravity.TOP: mSlideCalculator = sCalculateTop; break; case Gravity.RIGHT: mSlideCalculator = sCalculateRight; break; case Gravity.BOTTOM: mSlideCalculator = sCalculateBottom; break; case Gravity.START: mSlideCalculator = sCalculateStart; break; case Gravity.END: mSlideCalculator = sCalculateEnd; break; default: throw new IllegalArgumentException("Invalid slide direction"); public Slide(@GravityFlag int slideEdge) { setSlideEdge(slideEdge); }

Slide 104

Slide 104 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. public void setSlideEdge(@GravityFlag int slideEdge) { switch (slideEdge) { case Gravity.LEFT: mSlideCalculator = sCalculateLeft; break; case Gravity.TOP: mSlideCalculator = sCalculateTop; break; case Gravity.RIGHT: mSlideCalculator = sCalculateRight; break; case Gravity.BOTTOM: mSlideCalculator = sCalculateBottom; break; case Gravity.START: mSlideCalculator = sCalculateStart; break; case Gravity.END: mSlideCalculator = sCalculateEnd; break; default: throw new IllegalArgumentException("Invalid slide direction"); public Slide(@GravityFlag int slideEdge) { setSlideEdge(slideEdge); } 問題無い?

Slide 105

Slide 105 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. public void setSlideEdge(@GravityFlag int slideEdge) { switch (slideEdge) { case Gravity.LEFT: mSlideCalculator = sCalculateLeft; break; case Gravity.TOP: mSlideCalculator = sCalculateTop; break; case Gravity.RIGHT: mSlideCalculator = sCalculateRight; break; case Gravity.BOTTOM: mSlideCalculator = sCalculateBottom; break; case Gravity.START: mSlideCalculator = sCalculateStart; break; case Gravity.END: mSlideCalculator = sCalculateEnd; break; default: throw new IllegalArgumentException("Invalid slide direction"); public Slide(@GravityFlag int slideEdge) { setSlideEdge(slideEdge); } 問題無い? API Level?

Slide 106

Slide 106 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. public void setSlideEdge(int slideEdge) { switch (slideEdge) { case Gravity.LEFT: mSlideCalculator = sCalculateLeft; break; case Gravity.TOP: mSlideCalculator = sCalculateTop; break; case Gravity.RIGHT: mSlideCalculator = sCalculateRight; break; case Gravity.BOTTOM: mSlideCalculator = sCalculateBottom; break; default: throw new IllegalArgumentException("Invalid slide direction"); }

Slide 107

Slide 107 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. public void setSlideEdge(int slideEdge) { switch (slideEdge) { case Gravity.LEFT: mSlideCalculator = sCalculateLeft; break; case Gravity.TOP: mSlideCalculator = sCalculateTop; break; case Gravity.RIGHT: mSlideCalculator = sCalculateRight; break; case Gravity.BOTTOM: mSlideCalculator = sCalculateBottom; break; default: throw new IllegalArgumentException("Invalid slide direction"); } API Level 21

Slide 108

Slide 108 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 時系列 • GravityにSTART/ENDが追加された :API 14 • LayoutでStart/End指定が追加された:API 17 • android.transion.Slideが追加された:API 21 • SlideがSTART/ENDに対応した :API 22

Slide 109

Slide 109 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. 対策 •Jetpackを使いましょう •SDKにも実装ミスがありうることを頭の片隅に

Slide 110

Slide 110 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. おまけ:start/endとleft/rightは違うよ •当たり前だけどね • コード上から操作するときも、start/endとleft/rightの使い分 けきちんとできてる? • setPaddingRelative • setPadding • setCompoundDrawablesRelative • setCompoundDrawables

Slide 111

Slide 111 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. エモい話

Slide 112

Slide 112 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. エモい話 •やらかさない人はいない • 人間はミスを犯すもの • 開発はルーチンワークではなく、チャレンジの連続 •開発は必ずしも万全の状態で行えない • 時間が無い・人員が足りない • スキルが足りない・秘伝のタレ •失敗を過度に恐れない、失敗から学べばよい • どんな凄腕エンジニアもはじめは初心者

Slide 113

Slide 113 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. エモいまとめ •慎重になるべき部分を見極める • 問題が出やすい部分 • 問題が出たときに取り返しがつかない部分 •特に注意するべき部分 • 外部とのプロトコル • 永続化データ

Slide 114

Slide 114 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. エモいまとめ •コードを改善すればミスも減る • コードの状態は伝搬する • 悪いコードは悪いコードを増やす • よいコードはよいコードを増やす • リファクタリングはエンジニアの当然の義務 •過去を清算できる仕組みを仕込む • いつサポートを切るか?

Slide 115

Slide 115 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. エモいまとめ •失敗してしまったら • 何が問題だったか、現実的な再発防止策を考える • 「再発防止策を行わない」という結論も重要 •失敗を乗り越えたエンジニアは強い! • 人の失敗事例は他山の石とする

Slide 116

Slide 116 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. キサマ等がやらかしそうに なったミスは 既に私が1年前に 通過したミスだッッッ!

Slide 117

Slide 117 text

Copyright© 2020 Yahoo Japan Corporation. All Rights Reserved. ありがとうございました