Slide 1

Slide 1 text

Dagger2 を活用して Android SDK の依存関係をクリーンにする DroidKaigi 2018 Day2 14:00 ~ Room1 か ら く り( @kr9ly )

Slide 2

Slide 2 text

自己紹介 か ら く り( @kr9ly ) Ice Cream Sandwitch(4.0)く ら い の こ ろ か らの Androider です dely と いう 五反田の会社で働 い てます クラシル のアプリつ く ってます

Slide 3

Slide 3 text

DI 使ってます か ?

Slide 4

Slide 4 text

Android での DI ライブラリ RoboGuice ( deprecated ) Dagger ( deprecated ) Dagger2 Kodein などなど

Slide 5

Slide 5 text

Android での DI ライブラリ RoboGuice ( deprecated ) Dagger ( deprecated ) Dagger2 Kodein などなど

Slide 6

Slide 6 text

DI を使 う 目的 疎結合化 依存関係を整理 テスタビリティの向上 インスタンスのライフタイム管理 などなど

Slide 7

Slide 7 text

し か し Android アプリの場合

Slide 8

Slide 8 text

Android SDK への依存 がい っぱ い Context 依存 SDK 由来の機能(画面遷移 、SystemSer vice、GPS 等 々 ) FragmentManager Activity/Fragment のコールバック(ライフサイクルイベント と か )

Slide 9

Slide 9 text

DI コンテナの外の世界に依存し が ち Activity/Fragment へ直接 Injection 神に近づ い て いくActivity/Fragment アーキテクチャ( MVP, MVVM... )を貫徹で き な い ( こ の部分の コードは何のアーキテクチャ?) Robolectricが な い と う ま く テストで き な か ったり 結局今までと変わらな い のでは?

Slide 10

Slide 10 text

DI ( う ま く )使ってます か ?

Slide 11

Slide 11 text

Dagger2 活用ベタープラクティス集 目的 ロジック部分 か ら Context への依存を完全に排除する コードのほとんどの部分を DI コンテナの中で解決させる Robolectric なしで Presenter, ViewModel などなどをテストす る ※ テストにつ い ては今日は話しません サンプルリポジトリは こ ちら https://github.com/kr9ly/dagger2-sampleapp

Slide 12

Slide 12 text

注意 ViewModel は MVVM の ViewModel の こ とです( Android Architecture Components の話ではな い ) なるべ くい ろ い ろなやり方のメリット / デメリットを紹介して ます が あく まで一つのサンプルです 個人的な好み が 反映されて い る部分 が 多分に あ ります

Slide 13

Slide 13 text

目次 @Scope を活用する Context 依存を切り離す 画面遷移を整理する Activity/Fragment のコールバックイベントを整理する

Slide 14

Slide 14 text

目次 @Scope を活用する Context 依存を切り離す 画面遷移を整理する Activity/Fragment のコールバックイベントを整理する

Slide 15

Slide 15 text

@Scope を活用する @Scope @Retention(RetentionPolicy.RUNTIME) public @interface ViewScope { } 代表的なのは @Singleton

Slide 16

Slide 16 text

@Scope を活用すると @Singleton 以外のスコープ管理 が 可能に Activity/Fragmentご とにユニークなインスタンスを Provide で き る Activity/Fragmentご とにユニークなインスタンス が 管理で き ると う れし いこ との例 Rx の dispose 処理 画面遷移 FragmentManager のコントロール Activity/Fragment のコールバックメソッドの呼び出し

Slide 17

Slide 17 text

@Scope を使ってアノテーションを定 義する @Scope @Retention(RetentionPolicy.RUNTIME) public @interface ViewScope { }

Slide 18

Slide 18 text

Component 定義例 @ViewScope @Subcomponent(modules = {FragmentManagerDelegatedModule.class}) public interface ViewScopeComponent { ListViewModel listViewModel(); DetailViewModel detailViewModel(): } 自前 Scope 用の Component を定義する( ここか ら参 照される Module の @Provides に自前 Scope のアノテ ーションを付与する)

Slide 19

Slide 19 text

Component 定義例 @Singleton @Component public interface ApplicationComponent { ViewScopeComponent viewScopeComponent( FragmentManagerDelegatedModule fragmentManagerModule ); } Singleton -> 自前 Scope の Component 生成は Subcomponentが 楽

Slide 20

Slide 20 text

Subcomponent を作る方法の例 public class ApplicationComponentManager { private static final WeakHashMap components = new WeakHashMap<>(); public static synchronized ApplicationComponent get(Context context) { Context appContext = context.getApplicationContext(); ApplicationComponent component = components.get(appContext); if (component != null) { return component; } component = DaggerApplicationComponent.builder() .resourceProviderModule(new ResourceProviderModule(appContext)) .build(); components.put(context, component); return component; } }

Slide 21

Slide 21 text

Subcomponent を作る方法の例 public abstract class DaggerBaseActivity extends AppCompatActivity { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); ViewScopeComponent viewScopeComponent = ApplicationComponentManager .get(this) .viewScopeComponent( new FragmentManagerDelegatedModule( new FragmentManagerActivityDelegate(this) ) ); onComponentPrepared(viewScopeComponent, savedInstanceState); } protected abstract void onComponentPrepared(ViewScopeComponent component, @Nullable Bundle savedInstanceState); }

Slide 22

Slide 22 text

Subcomponent の切り方 い ろ い ろ Activity/Fragment は別の Component Activity/Fragment で同一の Component ...

Slide 23

Slide 23 text

Activity/Fragment は別の Component メリット 依存 Module をそれぞれで別のものにで き る デメリット Component を二つ定義する必要 があ って面倒

Slide 24

Slide 24 text

Activity/Fragment で同一の Component メリット Component 定義 が 一つで済む デメリット 依存 Module を切り かえ られな い

Slide 25

Slide 25 text

Component 分 け ど う する? 依存対象クラスは Activity と Fragment で あ まり変わらな い 個人的には Component 定義 が 一つで済むほ うが よさそ う ただし 、Component 定義を一つで済ます場合は 、 依存対象の Module 内で処理を切り分 け る必要 があ る

Slide 26

Slide 26 text

Module 内で処理を切り分 け る方法 @Module public class FragmentManagerModule { @ViewScope @Provides FragmentManager provideFragmentManager() { // ActivityもFragmentもこのメソッドが呼ばれる return null; } } まずは Module を定義する

Slide 27

Slide 27 text

Module 内で処理を切り分 け る方法 Delegate Pattern を適用する instanceof で分岐する

Slide 28

Slide 28 text

Delegate Pattern を適用する Delegate 用の interface を定義する @Module public class FragmentManagerDelegatedModule { private final FragmentManagerDelegate delegate; public FragmentManagerDelegatedModule(FragmentManagerDelegate delegate) { this.delegate = delegate; } @ViewScope @Provides FragmentManager provideFragmentManager() { // Delegate interfaceを呼び出す return delegate.provide(); } }

Slide 29

Slide 29 text

Delegate 用の interface public interface FragmentManagerDelegate { FragmentManager provide(); } 単に Provide 対象のインスタンスを返すだ け ここか ら Activity 用と Fragment 用で実装を分 け る

Slide 30

Slide 30 text

Activity 用の interface 実装 public class FragmentManagerActivityDelegate implements FragmentManagerDelegate { private final FragmentActivity fragmentActivity; public FragmentManagerActivityDelegate(FragmentActivity fragmentActivity) { this.fragmentActivity = fragmentActivity; } @Override public FragmentManager provide() { return fragmentActivity.getSupportFragmentManager(); } } Activity への参照を持つ

Slide 31

Slide 31 text

Fragment 用の interface 実装 public class FragmentManagerFragmentDelegate implements FragmentManagerDelegate { private final Fragment fragment; public FragmentManagerFragmentDelegate(Fragment fragment) { this.fragment = fragment; } @Override public FragmentManager provide() { // うっかり親のFragmentManagerを参照してハマったりしない return fragment.getChildFragmentManager(); } } Fragment への参照を持つ

Slide 32

Slide 32 text

insntanceof で分岐する方法 public class FragmentManagerDynamicModule { private final Object scopeObject; public FragmentManagerDynamicModule(Object scopeObject) { this.scopeObject = scopeObject; } @ViewScope @Provides FragmentManager provideFragmentManager() { if (scopeObject instanceof FragmentActivity) { return ((FragmentActivity) scopeObject).getSupportFragmentManager(); } else if (scopeObject instanceof Fragment) { return ((Fragment) scopeObject).getChildFragmentManager(); } throw new IllegalStateException(); } } どちら が 来ても いい よ う に動的に切り分 け る

Slide 33

Slide 33 text

どっちでも目的は達成で き ます 静的に解決 or 動的に解決 個人的には Delegate Pattern で実装したほ うが 安心(記述量は 増 えが ち)

Slide 34

Slide 34 text

目次 @Scope を活用する Context 依存を切り離す 画面遷移を整理する Activity/Fragment のコールバックイベントを整理する

Slide 35

Slide 35 text

Context に依存し が ち Android はなんでも Context に あ る つ い つ いContext をフィールドに つ い つ いContext を引数に Context に密結合 切り離して いき ましょ う

Slide 36

Slide 36 text

二通りのやり方 Module のコンストラクタに Context の参照を渡すパターン Context 自体を Provide するパターン

Slide 37

Slide 37 text

コード例 public class ResourceProvider { private final Context context; public ResourceProvider(Context context) { this.context = context; } public String getString(@StringRes int resId) { return context.getString(resId); } public String getString(@StringRes int resId, Object... formatArgs) { return context.getString(resId, formatArgs); } } 例 え ば string 等の Resource にアクセスするクラス

Slide 38

Slide 38 text

Module のコンストラクタに Context の参照を渡すパターン @Module public class ResourceProviderModule { private final Context appContext; public ResourceProviderModule(Context appContext) { this.appContext = appContext; } @Singleton @Provides public ResourceProvider provideResourceProvider() { return new ResourceProvider(appContext); } }

Slide 39

Slide 39 text

Module のコンストラクタに Context の参照を渡すパターン メリット Context に直接依存すべ き でな い レイヤー か らの Context 依存 を避 け られる( Context インスタンス自体を隠ぺ い で き る) デメリット Context 依存 が 発生するたびに Module を定義する必要 があ り 、 若干面倒

Slide 40

Slide 40 text

Context 自体を Provide するパターン @Module public class AppContextModule { private final Context appContext; public AppContextModule(Context appContext) { this.appContext = appContext; } @Singleton @Provides public Context provide() { return appContext; } } こうい った Module を定義して おく

Slide 41

Slide 41 text

Context 自体を Provide するパターン @Singleton public class ResourceProvider { private final Context context; @Inject public ResourceProvider(Context context) { this.context = context; } public String getString(@StringRes int resId) { return context.getString(resId); } public String getString(@StringRes int resId, Object... formatArgs) { return context.getString(resId, formatArgs); } } コンストラクタに @Inject, クラス定義に @Singleton

Slide 42

Slide 42 text

Context 自体を Provide するパターン メリット 依存関係 が 簡単なので あ ればコンストラクタインジェクション のみで依存関係を定義で き る デメリット Context に直接依存すべ き でな い レイヤー か らも Context 依存 が 記述で き てしま う ( Context インスタンス自体を隠ぺ い で き て い な い )

Slide 43

Slide 43 text

良し悪し あ るのでプロジェクトに あ っ たやり方を選びましょ う

Slide 44

Slide 44 text

目次 @Scope を活用する Context 依存を切り離す 画面遷移を整理する Activity/Fragment のコールバックイベントを整理する

Slide 45

Slide 45 text

Android の画面遷移 Intent Intent の生成に Contextが 必要 Activity#startActivity, Fragment#startActivity と場所によっ て呼び出すべ き メソッド が 違 う (特に startActivityForResult )

Slide 46

Slide 46 text

画面遷移用の interface を定義する public interface TransitionHandler { void startActivity(IntentBuilder intentBuilder); } Intent につ い ても生成用の interface を定義する public interface IntentBuilder { Intent build(Context context); }

Slide 47

Slide 47 text

Activity 用の実装を定義 public class ActivityTransitionHandler implements TransitionHandler { private final FragmentActivity activity; public ActivityTransitionHandler(FragmentActivity activity) { this.activity = activity; } @Override public void startActivity(IntentBuilder intentBuilder) { activity.startActivity(intentBuilder.build(activity)); } }

Slide 48

Slide 48 text

Fragment 用の実装を定義 public class FragmentTransitionHandler implements TransitionHandler { private final Fragment fragment; public FragmentTransitionHandler(Fragment fragment) { this.fragment = fragment; } @Override public void startActivity(IntentBuilder intentBuilder) { fragment.startActivity(intentBuilder.build(fragment.getContext())); } } Delegate Pattern 使 う と簡単に Module 内で切り分 け られます

Slide 49

Slide 49 text

Intent 生成に Contextが 必要な問題を 解決する public interface IntentBuilder { Intent build(Context context); } Intent 生成を interface で表現 、 引数に Context 依存を記述する Builder パターンは依存すべ き でな い レイヤー か らの参照( こ の場合は ViewModelか ら Context )を切り離せるのに結構使 え る

Slide 50

Slide 50 text

Intent 生成に Contextが 必要な問題を 解決する public class SimpleIntentBuilder implements IntentBuilder { private final Class extends Activity> targetActivityClass; public SimpleIntentBuilder(Class extends Activity> targetActivityClass) { this.targetActivityClass = targetActivityClass; } @Override public Intent build(Context context) { return new Intent(context, targetActivityClass); } } Contextが 必要なパターンでも Intent 生成する側は Context に依存せずに済む

Slide 51

Slide 51 text

Intent にパラメータを設定した い 場合 Intent#putExtra(String key, String value) Intent#putExtra(String key, int value) Intent#putExtra(String key, ㈭‹ oat value) 型 ご とに呼び出すメソッド が 違 う、いい 具合にパラメ ータを引 き 回しに くい 場合の対応方法( Bundle と か もそ う ですね)

Slide 52

Slide 52 text

型 ご とに entry を抽象化 public interface ExtraEntry { void setExtra(Intent intent); } 例 え ば こ んな interface を定義

Slide 53

Slide 53 text

型 ご とに entry を抽象化 public class StringExtraEntry implements ExtraEntry { private final String key; private final String value; public StringExtraEntry(String key, String value) { this.key = key; this.value = value; } @Override public void setExtra(Intent intent) { intent.putExtra(key, value); } } 型 ご とにそれぞれ実装

Slide 54

Slide 54 text

使 う 側は こ んな感じに public class SimpleIntentBuilder implements IntentBuilder { private final List extras = new ArrayList<>(); private final Class extends Activity> targetActivityClass; public SimpleIntentBuilder(Class extends Activity> targetActivityClass) { this.targetActivityClass = targetActivityClass; } public void putExtra(String key, String value) { extras.add(new StringExtraEntry(key, value)); } @Override public Intent build(Context context) { Intent intent = new Intent(context, targetActivityClass); for (ExtraEntry extra : extras) { extra.setExtra(intent); } return intent; } } Fragment の Arguments 用の Bundle も同じよ う な要領で

Slide 55

Slide 55 text

目次 @Scope を活用する Context 依存を切り離す 画面遷移を整理する Activity/Fragment のコールバックイベントを整理する

Slide 56

Slide 56 text

Activity/Fragment のコールバックイ ベントのハンドリングを DI コンテナの 中でやりた い Activity/Fragmentか ら DI コンテナの中の世界にアク セスする必要 が よ く 出て く る @Override public void onStart() { // DIコンテナの外の世界に依存している viewModel.onStart(); } 結果ロジック が 外に漏れ が ち( こ の く ら い で済めば い いけ ども)

Slide 57

Slide 57 text

Activity/Fragment のコールバックイ ベントを整理するやり方二点 AAC(Android Architecture Components) Lifecycle を使 う 自前でコールバックを定義する

Slide 58

Slide 58 text

AAC Lifecycle を使 う @Module public class AacLifecycleModule { // FragmentActivity/Fragment private final LifecycleOwner lifecycleOwner; public AacLifecycleModule(LifecycleOwner lifecycleOwner) { this.lifecycleOwner = lifecycleOwner; } @ViewScope @Provides Lifecycle provideAacLifecycle() { return lifecycleOwner.getLifecycle(); } } Lifecycle を Provide する Module を定義する

Slide 59

Slide 59 text

AAC Lifecycle を使 う public class ListViewModel implements LifecycleObserver { @Inject public ListViewModel( Lifecycle lifecycle ) { lifecycle.addObserver(this); } @OnLifecycleEvent(Lifecycle.Event.ON_START) public void onStart() { // do something } } 使 う 側は こ んな感じ 詳し く は https://developer.android.com/topic/libraries/arc hitecture/lifecycle.html

Slide 60

Slide 60 text

AAC Lifecycle を使 う メリット 標準の安心感 使 う のは簡単 デメリット 別に全部網羅してるわ け じゃな い (ライフサイクル関連のみ) イベント が 追加で き な い

Slide 61

Slide 61 text

自前でコールバックを定義する コールバック用の基底 interface を定義 コールバックの interface を定義 自前コールバックの呼び出しコードを追加

Slide 62

Slide 62 text

コールバックの interface を定義 public interface LifecycleCallback { // 基底インターフェースを用意しておくことで登録メソッドに何でもは入れられないようにしておく } public interface OnStartCallback extends LifecycleCallback { void onStart(); }

Slide 63

Slide 63 text

コールバック管理クラスを作る @ViewScope public class LifecycleCallbackController { private final List onStartCallbackList = new ArrayList<>(); ... @Inject public LifecycleCallbackController() { } public void register(LifecycleCallback callback) { if (callback instanceof OnStartCallback) { onStartCallbackList.add((OnStartCallback) callback); } ... } public void onStart() { for (OnStartCallback onStartCallback : onStartCallbackList) { onStartCallback.onStart(); } }

Slide 64

Slide 64 text

コールバック管理クラスを Activity/Fragment の Componentか ら生成 @ViewScope @Subcomponent( modules = { FragmentManagerDelegatedModule.class, AacLifecycleModule.class } ) public interface LifecycleComponent { LifecycleCallbackController lifecycleCallbackController(); }

Slide 65

Slide 65 text

Activity/Fragmentか らコールバック を呼ぶ public abstract class DaggerBaseActivity extends AppCompatActivity { private LifecycleCallbackController lifecycleCallbackController; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // lifecycleComponentを初期化 lifecycleCallbackController = lifecycleComponent .lifecycleCallbackController(); } @Override protected void onStart() { super.onStart(); // コールバックを呼ぶ lifecycleCallbackController.onStart(); }

Slide 66

Slide 66 text

自前のコールバッククラスの使 い 方 public class DetailViewModel implements OnStartCallback { @Inject public DetailViewModel( LifecycleCallbackController lifecycleCallbackController ) { lifecycleCallbackController.register(this); } @Override public void onStart() { // なにかやる } } 各イベントに対して一つずつし か コールバック定義で き な い (十分だとは思 うけ ども … )

Slide 67

Slide 67 text

自前のコールバッククラスの使 い 方 public class DetailViewModel { @Inject public DetailViewModel( LifecycleCallbackController lifecycleCallbackController ) { lifecycleCallbackController.register(new OnStartCallback() { @Override public void onStart() { // なにかやる } }); } } ネスト深 く なる & 余計な inner class 生成される

Slide 68

Slide 68 text

自前でコールバックを定義する メリット 自分でイベントを増やすの が 容易(例 え ばキーイベントをコン トロールした く なった場合と か ) デメリット メソッドにアノテーション定義するのに比べるとコード が 奇麗 じゃな い ( Annotation Processor 自前で書 く と いう 手も あ る?) Android Architecture Components の普及 、 改善次第では負 債になりそ う

Slide 69

Slide 69 text

以上ベタープラクティス集でした

Slide 70

Slide 70 text

サンプルリポジトリ https://github.com/kr9ly/dagger2-sampleapp 割愛したコードはすべて入ってます 今日 あげ た内容の他にも色 々 書 い てます AutoDispose を組み込む方法 Intent /Bundle に依存せずにデータを取り出す方法 Android Data Binding と RecyclerView の組み合わせ ... ご 興味 あ ればど う ぞ見て く ださ い

Slide 71

Slide 71 text

あく までベタープラクティスです より良 い 方法 あ ったら是非共有して く ださ い もっと DI まみれになろ う ぜ ご 清聴 あ り が と うご ざ い ました