Dagger
Dagger は Android/Java で動作する依存性注入(Dependency Injection)ライブラリです。
結構 Dagger を使っているプロジェクトはあるのですが、設計やその利用方法に疑問点や改善点が散見されるので、まとめておこうと思います。
Dagger 2がリリースされていますが、@Module
Annotation 内に記述していた、依存関係が@Component
に移譲された認識ですので、考え方は同じはずです。
Dagger は画面(or 機能)単位の依存関係を記述するべき
例えばログイン画面、リスト画面、詳細画面の場合の記述
Bad Pattern
[root: ApplicationModule] >[injects: Application.java]
└──<[includes: AwesomeModule]
└──>[injects: LoginActivity.java]
└──>[injects: ListActivity.java]
└──>[injects: DetailActivity.java]
@Module(
includes = AwesomeModule.class,
library = true
)
public class ApplicationModule {
...
}
@Module(
injects = {
LoginActivity.class,
ListActivity.class,
DetailActivity.class
},
library = true
)
public class AwesomeModule {
...
}
これでは子の依存関係を作成した場合に不要な @Inject
を行ってしまいます。
Good Pattern
[root: ApplicationModule] >[injects: Application.java]
├──<[addsTo: LoginModule]
│ └──>[injects: LoginActivity.java]
├──<[addsTo: ListModule]
│ └──>[injects: ListActivity.java]
└──<[addsTo: DetailModule]
└──>[injects: DetailActivity.java]
@Module(
includes = AwesomeModule.class,
library = true
)
public class ApplicationModule {
...
}
@Module(
injects = LoginActivity.class,
addsTo = ApplicationModule.class,
library = true
)
public class LoginModule {
...
}
@Module(
injects = ListActivity.class,
addsTo = ApplicationModule.class,
library = true
)
public class ListModule {
...
}
@Module(
injects = DetailActivity.class,
addsTo = ApplicationModule.class,
library = true
)
public class DetailModule {
...
}
このように addsTo
で子の依存関係として追加することで画面(or 機能)毎の注入を選択できるようになります。
Dagger は @Inject
指定された場合、インスタンス化が可能なものはインスタンス化する事を知っておくべき
例えば以下の様な構成の場合
// Retrofit interface
interface YourService {
getVideo(int id);
getUser(int id);
}
public class final VideoDao {
final YourService mService;
public VideoDao(YourService service) { mService = service; }
public Observable<VideoModel> get(int id) {
return service.getVideo(id).first();
}
}
public class final UserDao {
final YourService mService;
public UserDao(YourService service) { mService = service; }
public Observable<VideoModel> get(int id) {
return service.getUser(id).first();
}
}
@Module(complete = false, library = true)
public class ApiModule.java {
@Provides @Singleton
public VideoDao provideYourService(RestAdapter adapter) { ... }
}
public class AwesomeActivity extends Activity {
@Inject
VideoDao mVideoDao;
@Inject
UserDao mUserDao;
...
}
Bad Pattern
@Module(
injects = AwesomeActivity.class,
includes = ApiModule.class
)
public class DaoModule {
@Provides @Singleton
public VideoDao provideVideoDao(YourService service) { ... }
@Provides @Singleton
public VideoDao provideUserDao(YourService service) { ... }
}
この場合 YourService
が @Provides
されていれば DAO を@Provides
する必要ありません。
Good Pattern
+ @Singleton
public class final VideoDao {
final YourService mService;
+ @Inject
public VideoDao(YourService service) { mService = service; }
public Observable<VideoModel> get(int id) {
return service.getVideo(id).first();
}
}
+ @Singleton
public class final UserDao {
final YourService mService;
+ @Inject
public UserDao(YourService service) { mService = service; }
public Observable<UserModel> get(int id) {
return service.getUser(id).first();
}
}
// Application Module
@Module(
includes = ApiModule.class,
injects = YourApplication.class
)
public class ApplicationModule {
...
}
// Awesome domain Module
@Module(
injects = AwesomeActivity.class,
addsTo = ApplicationModule.class,
library = true
)
public class AwesomeModule {
}
上記のように Annotation と依存関係を追加すれば、DAO を @Provides
する必要がありません。
Dagger で複数インスタンス作成される場合は Lazy<T>
を利用すべき
Injection の種類
- SINGLETON
- 唯一のインスタンスです。多くの他のオブジェクトから参照されます
- LAZY
- 一度生成されると破棄されるまで利用されるインスタンス。
- PROVIDER
- 都度生成されるインスタンス。
Provider<T>
は便利ですが、自分でインスタンスの管理を行わなければならず混乱を招きます。
これが必要な場合、設計の見直しが必要かもしれません。
Bad Pattern
class MainPresenter {
@Inject Provider<ChildPresenter> mChildProvider;
private final List<ChildPresenter> mChilds = new ArrayList<>();
public void OnViewInflated() {
// Has 5 childs
for (int i=0; i < childCount; i++) {
final ChildPresenter presenter = mChildProvider.get();
((ChildView) getView().getChildAt(i)).setPresenter(presenter);
mChilds.add(presenter);
}
}
public void fireChildEvent(int id) {
mChilds.get(id).fire();
}
}
class ChildPresenter {
public void fire() { ... }
}
class ChildView extend View {
private ChildPresenter mPresenter;
public void setPresenter(ChildPresenter presenter) { mPresenter = presenter; }
}
複数のネストした View
が存在する場合は Provider<T>
でインスタンスを作成するべきではありません。
Good Pattern
class MainPresenter {
public void fireChildEvent(int id) {
((ChildView) getView().getChildAt(id)).getPresenter().fire();
}
}
class ChildPresenter {
public void fire() { ... }
}
class ChildView extend View {
@Inject
Lazy<ChildPresenter> mPresenter;
public ChildPresenter getPresenter() { return mPresenter.get(); }
}
Lazy<T>
を使うことで、インスタンスの管理から解放されます。
Dagger でネストした CustomView へ ObjectGraph
を渡すときは LayoutInflator.Factory
を使うべき
Fragment 等から CustomView へ依存関係を引き渡す必要がある場合があると思いますが、その場合は Factory
を作りましょう。
Jake もいってる https://github.com/square/dagger/issues/382
Mortar を使えば考えなくていいんですがね…。
Bad Pattern
public class AwesomeFragment extends Fragment {
final ObjectGraph mObjectGraph;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mObjectGraph = MainApplication.get(getActivity()).getObjectGraph()
.plus(getDaggerModule());
mObjectGraph.inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.awesome_fragment,
container, false);
mObjectGraph.inject(view.findViewById(R.layout.awesome_view));
return view;
}
...
}
これだと Fragment
と CustomView が ObjectGraph
の状態によって密結合してしまうので、避けたほうがいいでしょう。
Good Pattern
public class AwesomeView extends FrameLayout {
static public class Factory implements LayoutInflater.Factory {
final ObjectGraph mObjectGraph;
public Factory(ObjectGraph objectGraph) { mObjectGraph = objectGraph; }
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
// null を返すと標準のファクトリを使うので、型名で判定する。
if (!name.equalsIgnoreCase(AwesomeView.class.getName())) {
return null;
}
return new AwesomeView(context, attrs, mObjectGraph);
}
public AwesomeView(Context context, AttributeSet attrs, ObjectGraph objectGraph) {
objectGraph.inject(this);
}
...
}
public class AwesomeFragment extends Fragment {
final ObjectGraph mObjectGraph;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mObjectGraph = MainApplication.get(getActivity()).getObjectGraph()
.plus(getDaggerModule());
mObjectGraph.inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final LayoutInflater daggerInflater = inflater.cloneInContext(getActivity);
daggerInflater.setFactory(new AwesomeView.Factory(mObjectGraph));
final View view = inflater.inflate(R.layout.awesome_fragment,
container, false);
return view;
}
...
}
CustomView 内で inject
操作を行ったほうが関係性を疎にできます。
参考
https://github.com/square/dagger
https://github.com/JakeWharton/u2020
https://github.com/LiveTyping/u2020-mvp