Dagger2を活用してAndroid SDKの依存関係をクリーンにする

63e891c4f79dbdeb70aa8ebb8104c8d5?s=47 kr9ly
February 09, 2018

Dagger2を活用してAndroid SDKの依存関係をクリーンにする

DroidKaigi 2018 Day2 14:00~ Room1

63e891c4f79dbdeb70aa8ebb8104c8d5?s=128

kr9ly

February 09, 2018
Tweet

Transcript

  1. Dagger2 を活用して Android SDK の依存関係をクリーンにする DroidKaigi 2018 Day2 14:00 ~

    Room1 か ら く り( @kr9ly )
  2. 自己紹介 か ら く り( @kr9ly ) Ice Cream Sandwitch(4.0)く

    ら い の こ ろ か らの Androider です dely と いう 五反田の会社で働 い てます クラシル のアプリつ く ってます
  3. DI 使ってます か ?

  4. Android での DI ライブラリ RoboGuice ( deprecated ) Dagger (

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

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

  7. し か し Android アプリの場合

  8. Android SDK への依存 がい っぱ い Context 依存 SDK 由来の機能(画面遷移

    、SystemSer vice、GPS 等 々 ) FragmentManager Activity/Fragment のコールバック(ライフサイクルイベント と か )
  9. DI コンテナの外の世界に依存し が ち Activity/Fragment へ直接 Injection 神に近づ い て

    いくActivity/Fragment アーキテクチャ( MVP, MVVM... )を貫徹で き な い ( こ の部分の コードは何のアーキテクチャ?) Robolectricが な い と う ま く テストで き な か ったり 結局今までと変わらな い のでは?
  10. DI ( う ま く )使ってます か ?

  11. Dagger2 活用ベタープラクティス集 目的 ロジック部分 か ら Context への依存を完全に排除する コードのほとんどの部分を DI

    コンテナの中で解決させる Robolectric なしで Presenter, ViewModel などなどをテストす る ※ テストにつ い ては今日は話しません サンプルリポジトリは こ ちら https://github.com/kr9ly/dagger2-sampleapp
  12. 注意 ViewModel は MVVM の ViewModel の こ とです( Android

    Architecture Components の話ではな い ) なるべ くい ろ い ろなやり方のメリット / デメリットを紹介して ます が あく まで一つのサンプルです 個人的な好み が 反映されて い る部分 が 多分に あ ります
  13. 目次 @Scope を活用する Context 依存を切り離す 画面遷移を整理する Activity/Fragment のコールバックイベントを整理する

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

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

    @Singleton
  16. @Scope を活用すると @Singleton 以外のスコープ管理 が 可能に Activity/Fragmentご とにユニークなインスタンスを Provide で

    き る Activity/Fragmentご とにユニークなインスタンス が 管理で き ると う れし いこ との例 Rx の dispose 処理 画面遷移 FragmentManager のコントロール Activity/Fragment のコールバックメソッドの呼び出し
  17. @Scope を使ってアノテーションを定 義する @Scope @Retention(RetentionPolicy.RUNTIME) public @interface ViewScope { }

  18. Component 定義例 @ViewScope @Subcomponent(modules = {FragmentManagerDelegatedModule.class}) public interface ViewScopeComponent {

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

    FragmentManagerDelegatedModule fragmentManagerModule ); } Singleton -> 自前 Scope の Component 生成は Subcomponentが 楽
  20. Subcomponent を作る方法の例 public class ApplicationComponentManager { private static final WeakHashMap<Context,

    ApplicationComponent> 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; } }
  21. 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); }
  22. Subcomponent の切り方 い ろ い ろ Activity/Fragment は別の Component Activity/Fragment

    で同一の Component ...
  23. Activity/Fragment は別の Component メリット 依存 Module をそれぞれで別のものにで き る デメリット

    Component を二つ定義する必要 があ って面倒
  24. Activity/Fragment で同一の Component メリット Component 定義 が 一つで済む デメリット 依存

    Module を切り かえ られな い
  25. Component 分 け ど う する? 依存対象クラスは Activity と Fragment

    で あ まり変わらな い 個人的には Component 定義 が 一つで済むほ うが よさそ う ただし 、Component 定義を一つで済ます場合は 、 依存対象の Module 内で処理を切り分 け る必要 があ る
  26. Module 内で処理を切り分 け る方法 @Module public class FragmentManagerModule { @ViewScope

    @Provides FragmentManager provideFragmentManager() { // ActivityもFragmentもこのメソッドが呼ばれる return null; } } まずは Module を定義する
  27. Module 内で処理を切り分 け る方法 Delegate Pattern を適用する instanceof で分岐する

  28. 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(); } }
  29. Delegate 用の interface public interface FragmentManagerDelegate { FragmentManager provide(); }

    単に Provide 対象のインスタンスを返すだ け ここか ら Activity 用と Fragment 用で実装を分 け る
  30. 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 への参照を持つ
  31. 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 への参照を持つ
  32. 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(); } } どちら が 来ても いい よ う に動的に切り分 け る
  33. どっちでも目的は達成で き ます 静的に解決 or 動的に解決 個人的には Delegate Pattern で実装したほ

    うが 安心(記述量は 増 えが ち)
  34. 目次 @Scope を活用する Context 依存を切り離す 画面遷移を整理する Activity/Fragment のコールバックイベントを整理する

  35. Context に依存し が ち Android はなんでも Context に あ る

    つ い つ いContext をフィールドに つ い つ いContext を引数に Context に密結合 切り離して いき ましょ う
  36. 二通りのやり方 Module のコンストラクタに Context の参照を渡すパターン Context 自体を Provide するパターン

  37. コード例 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 にアクセスするクラス
  38. 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); } }
  39. Module のコンストラクタに Context の参照を渡すパターン メリット Context に直接依存すべ き でな い

    レイヤー か らの Context 依存 を避 け られる( Context インスタンス自体を隠ぺ い で き る) デメリット Context 依存 が 発生するたびに Module を定義する必要 があ り 、 若干面倒
  40. 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 を定義して おく
  41. 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
  42. Context 自体を Provide するパターン メリット 依存関係 が 簡単なので あ ればコンストラクタインジェクション

    のみで依存関係を定義で き る デメリット Context に直接依存すべ き でな い レイヤー か らも Context 依存 が 記述で き てしま う ( Context インスタンス自体を隠ぺ い で き て い な い )
  43. 良し悪し あ るのでプロジェクトに あ っ たやり方を選びましょ う

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

  45. Android の画面遷移 Intent Intent の生成に Contextが 必要 Activity#startActivity, Fragment#startActivity と場所によっ

    て呼び出すべ き メソッド が 違 う (特に startActivityForResult )
  46. 画面遷移用の interface を定義する public interface TransitionHandler { void startActivity(IntentBuilder intentBuilder);

    } Intent につ い ても生成用の interface を定義する public interface IntentBuilder { Intent build(Context context); }
  47. 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)); } }
  48. 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 内で切り分 け られます
  49. Intent 生成に Contextが 必要な問題を 解決する public interface IntentBuilder { Intent

    build(Context context); } Intent 生成を interface で表現 、 引数に Context 依存を記述する Builder パターンは依存すべ き でな い レイヤー か らの参照( こ の場合は ViewModelか ら Context )を切り離せるのに結構使 え る
  50. 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 に依存せずに済む
  51. Intent にパラメータを設定した い 場合 Intent#putExtra(String key, String value) Intent#putExtra(String key,

    int value) Intent#putExtra(String key, ㈭‹ oat value) 型 ご とに呼び出すメソッド が 違 う、いい 具合にパラメ ータを引 き 回しに くい 場合の対応方法( Bundle と か もそ う ですね)
  52. 型 ご とに entry を抽象化 public interface ExtraEntry { void

    setExtra(Intent intent); } 例 え ば こ んな interface を定義
  53. 型 ご とに 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); } } 型 ご とにそれぞれ実装
  54. 使 う 側は こ んな感じに public class SimpleIntentBuilder implements IntentBuilder

    { private final List<ExtraEntry> 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 も同じよ う な要領で
  55. 目次 @Scope を活用する Context 依存を切り離す 画面遷移を整理する Activity/Fragment のコールバックイベントを整理する

  56. Activity/Fragment のコールバックイ ベントのハンドリングを DI コンテナの 中でやりた い Activity/Fragmentか ら DI

    コンテナの中の世界にアク セスする必要 が よ く 出て く る @Override public void onStart() { // DIコンテナの外の世界に依存している viewModel.onStart(); } 結果ロジック が 外に漏れ が ち( こ の く ら い で済めば い いけ ども)
  57. Activity/Fragment のコールバックイ ベントを整理するやり方二点 AAC(Android Architecture Components) Lifecycle を使 う 自前でコールバックを定義する

  58. 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 を定義する
  59. 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
  60. AAC Lifecycle を使 う メリット 標準の安心感 使 う のは簡単 デメリット

    別に全部網羅してるわ け じゃな い (ライフサイクル関連のみ) イベント が 追加で き な い
  61. 自前でコールバックを定義する コールバック用の基底 interface を定義 コールバックの interface を定義 自前コールバックの呼び出しコードを追加

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

    public interface OnStartCallback extends LifecycleCallback { void onStart(); }
  63. コールバック管理クラスを作る @ViewScope public class LifecycleCallbackController { private final List<OnStartCallback> 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(); } }
  64. コールバック管理クラスを Activity/Fragment の Componentか ら生成 @ViewScope @Subcomponent( modules = {

    FragmentManagerDelegatedModule.class, AacLifecycleModule.class } ) public interface LifecycleComponent { LifecycleCallbackController lifecycleCallbackController(); }
  65. 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(); }
  66. 自前のコールバッククラスの使 い 方 public class DetailViewModel implements OnStartCallback { @Inject

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

    LifecycleCallbackController lifecycleCallbackController ) { lifecycleCallbackController.register(new OnStartCallback() { @Override public void onStart() { // なにかやる } }); } } ネスト深 く なる & 余計な inner class 生成される
  68. 自前でコールバックを定義する メリット 自分でイベントを増やすの が 容易(例 え ばキーイベントをコン トロールした く なった場合と

    か ) デメリット メソッドにアノテーション定義するのに比べるとコード が 奇麗 じゃな い ( Annotation Processor 自前で書 く と いう 手も あ る?) Android Architecture Components の普及 、 改善次第では負 債になりそ う
  69. 以上ベタープラクティス集でした

  70. サンプルリポジトリ https://github.com/kr9ly/dagger2-sampleapp 割愛したコードはすべて入ってます 今日 あげ た内容の他にも色 々 書 い てます

    AutoDispose を組み込む方法 Intent /Bundle に依存せずにデータを取り出す方法 Android Data Binding と RecyclerView の組み合わせ ... ご 興味 あ ればど う ぞ見て く ださ い
  71. あく までベタープラクティスです より良 い 方法 あ ったら是非共有して く ださ い

    もっと DI まみれになろ う ぜ ご 清聴 あ り が と うご ざ い ました