Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

kr9ly
February 09, 2018

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

DroidKaigi 2018 Day2 14:00~ Room1

kr9ly

February 09, 2018
Tweet

More Decks by kr9ly

Other Decks in Programming

Transcript

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

    ら い の こ ろ か らの Androider です dely と いう 五反田の会社で働 い てます クラシル のアプリつ く ってます
  2. Android SDK への依存 がい っぱ い Context 依存 SDK 由来の機能(画面遷移

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

    いくActivity/Fragment アーキテクチャ( MVP, MVVM... )を貫徹で き な い ( こ の部分の コードは何のアーキテクチャ?) Robolectricが な い と う ま く テストで き な か ったり 結局今までと変わらな い のでは?
  4. Dagger2 活用ベタープラクティス集 目的 ロジック部分 か ら Context への依存を完全に排除する コードのほとんどの部分を DI

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

    Architecture Components の話ではな い ) なるべ くい ろ い ろなやり方のメリット / デメリットを紹介して ます が あく まで一つのサンプルです 個人的な好み が 反映されて い る部分 が 多分に あ ります
  6. @Scope を活用すると @Singleton 以外のスコープ管理 が 可能に Activity/Fragmentご とにユニークなインスタンスを Provide で

    き る Activity/Fragmentご とにユニークなインスタンス が 管理で き ると う れし いこ との例 Rx の dispose 処理 画面遷移 FragmentManager のコントロール Activity/Fragment のコールバックメソッドの呼び出し
  7. Component 定義例 @ViewScope @Subcomponent(modules = {FragmentManagerDelegatedModule.class}) public interface ViewScopeComponent {

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

    FragmentManagerDelegatedModule fragmentManagerModule ); } Singleton -> 自前 Scope の Component 生成は Subcomponentが 楽
  9. 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; } }
  10. 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); }
  11. Component 分 け ど う する? 依存対象クラスは Activity と Fragment

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

    @Provides FragmentManager provideFragmentManager() { // ActivityもFragmentもこのメソッドが呼ばれる return null; } } まずは Module を定義する
  13. 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(); } }
  14. Delegate 用の interface public interface FragmentManagerDelegate { FragmentManager provide(); }

    単に Provide 対象のインスタンスを返すだ け ここか ら Activity 用と Fragment 用で実装を分 け る
  15. 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 への参照を持つ
  16. 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 への参照を持つ
  17. 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(); } } どちら が 来ても いい よ う に動的に切り分 け る
  18. Context に依存し が ち Android はなんでも Context に あ る

    つ い つ いContext をフィールドに つ い つ いContext を引数に Context に密結合 切り離して いき ましょ う
  19. コード例 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 にアクセスするクラス
  20. 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); } }
  21. Module のコンストラクタに Context の参照を渡すパターン メリット Context に直接依存すべ き でな い

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

    のみで依存関係を定義で き る デメリット Context に直接依存すべ き でな い レイヤー か らも Context 依存 が 記述で き てしま う ( Context インスタンス自体を隠ぺ い で き て い な い )
  25. 画面遷移用の interface を定義する public interface TransitionHandler { void startActivity(IntentBuilder intentBuilder);

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

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

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

    setExtra(Intent intent); } 例 え ば こ んな interface を定義
  32. 型 ご とに 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); } } 型 ご とにそれぞれ実装
  33. 使 う 側は こ んな感じに 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 も同じよ う な要領で
  34. Activity/Fragment のコールバックイ ベントのハンドリングを DI コンテナの 中でやりた い Activity/Fragmentか ら DI

    コンテナの中の世界にアク セスする必要 が よ く 出て く る @Override public void onStart() { // DIコンテナの外の世界に依存している viewModel.onStart(); } 結果ロジック が 外に漏れ が ち( こ の く ら い で済めば い いけ ども)
  35. 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 を定義する
  36. 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
  37. AAC Lifecycle を使 う メリット 標準の安心感 使 う のは簡単 デメリット

    別に全部網羅してるわ け じゃな い (ライフサイクル関連のみ) イベント が 追加で き な い
  38. コールバック管理クラスを作る @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(); } }
  39. コールバック管理クラスを Activity/Fragment の Componentか ら生成 @ViewScope @Subcomponent( modules = {

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

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

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

    か ) デメリット メソッドにアノテーション定義するのに比べるとコード が 奇麗 じゃな い ( Annotation Processor 自前で書 く と いう 手も あ る?) Android Architecture Components の普及 、 改善次第では負 債になりそ う
  44. サンプルリポジトリ https://github.com/kr9ly/dagger2-sampleapp 割愛したコードはすべて入ってます 今日 あげ た内容の他にも色 々 書 い てます

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

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