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. Dagger2
    を活用して
    Android SDK
    の依存関係をクリーンにする
    DroidKaigi 2018 Day2 14:00
    ~ Room1



    り(
    @kr9ly

    View Slide

  2. 自己紹介



    り(
    @kr9ly

    Ice Cream Sandwitch(4.0)く






    らの
    Androider
    です
    dely

    いう
    五反田の会社で働

    てます
    クラシル のアプリつ

    ってます

    View Slide

  3. DI
    使ってます


    View Slide

  4. Android
    での
    DI
    ライブラリ
    RoboGuice

    deprecated

    Dagger

    deprecated

    Dagger2
    Kodein
    などなど

    View Slide

  5. Android
    での
    DI
    ライブラリ
    RoboGuice

    deprecated

    Dagger

    deprecated

    Dagger2
    Kodein
    などなど

    View Slide

  6. DI
    を使

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

    View Slide




  7. Android
    アプリの場合

    View Slide

  8. Android SDK
    への依存
    がい
    っぱ

    Context
    依存
    SDK
    由来の機能(画面遷移
    、SystemSer vice、GPS



    FragmentManager
    Activity/Fragment
    のコールバック(ライフサイクルイベント



    View Slide

  9. DI
    コンテナの外の世界に依存し


    Activity/Fragment
    へ直接
    Injection
    神に近づ


    いくActivity/Fragment
    アーキテクチャ(
    MVP, MVVM...
    )を貫徹で





    の部分の
    コードは何のアーキテクチャ?)
    Robolectricが






    テストで



    ったり
    結局今までと変わらな

    のでは?

    View Slide

  10. DI




    )使ってます


    View Slide

  11. Dagger2
    活用ベタープラクティス集
    目的
    ロジック部分


    Context
    への依存を完全に排除する
    コードのほとんどの部分を
    DI
    コンテナの中で解決させる
    Robolectric
    なしで
    Presenter, ViewModel
    などなどをテストす


    テストにつ

    ては今日は話しません
    サンプルリポジトリは

    ちら
    https://github.com/kr9ly/dagger2-sampleapp

    View Slide

  12. 注意
    ViewModel

    MVVM

    ViewModel


    とです(
    Android
    Architecture Components
    の話ではな


    なるべ
    くい


    ろなやり方のメリット
    /
    デメリットを紹介して
    ます

    あく
    まで一つのサンプルです
    個人的な好み

    反映されて

    る部分

    多分に

    ります

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  16. @Scope
    を活用すると
    @Singleton
    以外のスコープ管理

    可能に
    Activity/Fragmentご
    とにユニークなインスタンスを
    Provide



    Activity/Fragmentご
    とにユニークなインスタンス

    管理で

    ると

    れし
    いこ
    との例
    Rx

    dispose
    処理
    画面遷移
    FragmentManager
    のコントロール
    Activity/Fragment
    のコールバックメソッドの呼び出し

    View Slide

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

    View Slide

  18. Component
    定義例
    @ViewScope
    @Subcomponent(modules = {FragmentManagerDelegatedModule.class})
    public interface ViewScopeComponent {
    ListViewModel listViewModel();
    DetailViewModel detailViewModel():
    }
    自前
    Scope
    用の
    Component
    を定義する(
    ここか
    ら参
    照される
    Module

    @Provides
    に自前
    Scope
    のアノテ
    ーションを付与する)

    View Slide

  19. Component
    定義例
    @Singleton
    @Component
    public interface ApplicationComponent {
    ViewScopeComponent viewScopeComponent(
    FragmentManagerDelegatedModule fragmentManagerModule
    );
    }
    Singleton ->
    自前
    Scope

    Component
    生成は
    Subcomponentが

    View Slide

  20. 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;
    }
    }

    View Slide

  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);
    }

    View Slide

  22. Subcomponent
    の切り方




    Activity/Fragment
    は別の
    Component
    Activity/Fragment
    で同一の
    Component
    ...

    View Slide

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


    デメリット
    Component
    を二つ定義する必要
    があ
    って面倒

    View Slide

  24. Activity/Fragment
    で同一の
    Component
    メリット
    Component
    定義

    一つで済む
    デメリット
    依存
    Module
    を切り
    かえ
    られな

    View Slide

  25. Component




    する?
    依存対象クラスは
    Activity

    Fragment


    まり変わらな

    個人的には
    Component
    定義

    一つで済むほ
    うが
    よさそ

    ただし
    、Component
    定義を一つで済ます場合は

    依存対象の
    Module
    内で処理を切り分

    る必要
    があ

    View Slide

  26. Module
    内で処理を切り分

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

    View Slide

  27. Module
    内で処理を切り分

    る方法
    Delegate Pattern
    を適用する
    instanceof
    で分岐する

    View Slide

  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();
    }
    }

    View Slide

  29. Delegate
    用の
    interface
    public interface FragmentManagerDelegate {
    FragmentManager provide();
    }
    単に
    Provide
    対象のインスタンスを返すだ

    ここか

    Activity
    用と
    Fragment
    用で実装を分


    View Slide

  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
    への参照を持つ

    View Slide

  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
    への参照を持つ

    View Slide

  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();
    }
    }
    どちら

    来ても
    いい


    に動的に切り分


    View Slide

  33. どっちでも目的は達成で

    ます
    静的に解決
    or
    動的に解決
    個人的には
    Delegate Pattern
    で実装したほ
    うが
    安心(記述量は

    えが
    ち)

    View Slide

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

    View Slide

  35. Context
    に依存し


    Android
    はなんでも
    Context






    いContext
    をフィールドに



    いContext
    を引数に
    Context
    に密結合
    切り離して
    いき
    ましょ

    View Slide

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

    View Slide

  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
    にアクセスするクラス

    View Slide

  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);
    }
    }

    View Slide

  39. Module
    のコンストラクタに
    Context
    の参照を渡すパターン
    メリット
    Context
    に直接依存すべ

    でな

    レイヤー

    らの
    Context
    依存
    を避

    られる(
    Context
    インスタンス自体を隠ぺ



    る)
    デメリット
    Context
    依存

    発生するたびに
    Module
    を定義する必要
    があ


    若干面倒

    View Slide

  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
    を定義して
    おく

    View Slide

  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

    View Slide

  42. Context
    自体を
    Provide
    するパターン
    メリット
    依存関係

    簡単なので

    ればコンストラクタインジェクション
    のみで依存関係を定義で


    デメリット
    Context
    に直接依存すべ

    でな

    レイヤー

    らも
    Context
    依存

    記述で

    てしま


    Context
    インスタンス自体を隠ぺ








    View Slide

  43. 良し悪し

    るのでプロジェクトに


    たやり方を選びましょ

    View Slide

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

    View Slide

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

    メソッド



    (特に
    startActivityForResult

    View Slide

  46. 画面遷移用の
    interface
    を定義する
    public interface TransitionHandler {
    void startActivity(IntentBuilder intentBuilder);
    }
    Intent
    につ

    ても生成用の
    interface
    を定義する
    public interface IntentBuilder {
    Intent build(Context context);
    }

    View Slide

  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));
    }
    }

    View Slide

  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
    内で切り分

    られます

    View Slide

  49. Intent
    生成に
    Contextが
    必要な問題を
    解決する
    public interface IntentBuilder {
    Intent build(Context context);
    }
    Intent
    生成を
    interface
    で表現

    引数に
    Context
    依存を記述する
    Builder
    パターンは依存すべ

    でな

    レイヤー

    らの参照(

    の場合は
    ViewModelか

    Context
    )を切り離せるのに結構使


    View Slide

  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
    に依存せずに済む

    View Slide

  51. Intent
    にパラメータを設定した

    場合
    Intent#putExtra(String key, String value)
    Intent#putExtra(String key, int value)
    Intent#putExtra(String key,
    ㈭‹
    oat value)


    とに呼び出すメソッド


    う、いい
    具合にパラメ
    ータを引

    回しに
    くい
    場合の対応方法(
    Bundle


    もそ

    ですね)

    View Slide



  52. とに
    entry
    を抽象化
    public interface ExtraEntry {
    void setExtra(Intent intent);
    }




    んな
    interface
    を定義

    View Slide



  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);
    }
    }


    とにそれぞれ実装

    View Slide

  54. 使

    側は

    んな感じに
    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
    も同じよ

    な要領で

    View Slide

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

    View Slide

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

    Activity/Fragmentか

    DI
    コンテナの中の世界にアク
    セスする必要



    出て


    @Override
    public void onStart() {
    // DIコンテナの外の世界に依存している
    viewModel.onStart();
    }
    結果ロジック

    外に漏れ

    ち(





    で済めば

    いけ
    ども)

    View Slide

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

    自前でコールバックを定義する

    View Slide

  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
    を定義する

    View Slide

  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

    View Slide

  60. AAC Lifecycle
    を使

    メリット
    標準の安心感
    使

    のは簡単
    デメリット
    別に全部網羅してるわ

    じゃな

    (ライフサイクル関連のみ)
    イベント

    追加で



    View Slide

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

    View Slide

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

    View Slide

  63. コールバック管理クラスを作る
    @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();
    }
    }

    View Slide

  64. コールバック管理クラスを
    Activity/Fragment

    Componentか
    ら生成
    @ViewScope
    @Subcomponent(
    modules = {
    FragmentManagerDelegatedModule.class,
    AacLifecycleModule.class
    }
    )
    public interface LifecycleComponent {
    LifecycleCallbackController lifecycleCallbackController();
    }

    View Slide

  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();
    }

    View Slide

  66. 自前のコールバッククラスの使


    public class DetailViewModel implements OnStartCallback {
    @Inject
    public DetailViewModel(
    LifecycleCallbackController lifecycleCallbackController
    ) {
    lifecycleCallbackController.register(this);
    }
    @Override
    public void onStart() {
    // なにかやる
    }
    }
    各イベントに対して一つずつし

    コールバック定義で



    (十分だとは思
    うけ
    ども


    View Slide

  67. 自前のコールバッククラスの使


    public class DetailViewModel {
    @Inject
    public DetailViewModel(
    LifecycleCallbackController lifecycleCallbackController
    ) {
    lifecycleCallbackController.register(new OnStartCallback() {
    @Override
    public void onStart() {
    // なにかやる
    }
    });
    }
    }
    ネスト深

    なる
    &
    余計な
    inner class
    生成される

    View Slide

  68. 自前でコールバックを定義する
    メリット
    自分でイベントを増やすの

    容易(例

    ばキーイベントをコン
    トロールした

    なった場合と


    デメリット
    メソッドにアノテーション定義するのに比べるとコード

    奇麗
    じゃな


    Annotation Processor
    自前で書


    いう
    手も

    る?)
    Android Architecture Components
    の普及

    改善次第では負
    債になりそ

    View Slide

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

    View Slide

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



    てます
    AutoDispose
    を組み込む方法
    Intent /Bundle
    に依存せずにデータを取り出す方法
    Android Data Binding

    RecyclerView
    の組み合わせ
    ...

    興味

    ればど

    ぞ見て

    ださ

    View Slide

  71. あく
    までベタープラクティスです
    より良

    方法

    ったら是非共有して

    ださ

    もっと
    DI
    まみれになろ



    清聴




    うご


    ました

    View Slide