对Fragment进行依赖注入的时机和方式
在使用 Dagger2 对 Android 应用进行依赖注入时,有时需要先注入 Activity ,然后 Fragment 从 Activity 中获取被注入的 Component 实例,再对自己(Fragment)进行注入。Fragment 的生命周期和 Activity 不同,注入时需要考虑注入的时机。
Contents
1. 在 onCreate() 时注入
Fernando Cejas 在 Tasting Dagger 2 on Android 一文中介绍了使用 Dagger2 对 Android 应用进行依赖注入的一种方法,并给出了示例工程 Android-CleanArchitecture。在 Android-CleanArchitecture 中,Dagger2 的 Component 先是被注入到 Activity,Activity 实现了 HasComponent 接口,Fragment 通过Activity 的 HasComponent 接口获得 Component,并对自己进行注入。
Android-CleanArchitecture 的展示界面是一个 Master Detail 结构,UserListActivity.java 用于显示一个用户列表,在列表中点击某个用户后,UserDetailsActivity 会显示用户的详细信息。 UserListActivity 的主要代码如下:
public class UserListActivity extends BaseActivity implements HasComponent<UserComponent>,
UserListFragment.UserListListener {
// ...
private UserComponent userComponent;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.activity_layout);
this.initializeInjector();
if (savedInstanceState == null) {
addFragment(R.id.fragmentContainer, new UserListFragment());
}
}
private void initializeInjector() {
this.userComponent = DaggerUserComponent.builder()
.applicationComponent(getApplicationComponent())
.activityModule(getActivityModule())
.build();
}
@Override public UserComponent getComponent() {
return userComponent;
}
// ...
}
可以看到,UserListActivity 在 onCreate() 时对自己进行了注入,并向 fragmentContainer 添加了 UserListFragment。UserListActivity 实现了 HasComponent<UserComponent> ,getComponent() 返回注入的 UserComponent 实例。
UserListFragment 用于显示具体的列表,其主要代码如下:
public class UserListFragment extends BaseFragment implements UserListView {
// ...
@Inject UserListPresenter userListPresenter;
@Inject UsersAdapter usersAdapter;
// ...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.getComponent(UserComponent.class).inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View fragmentView = inflater.inflate(R.layout.fragment_user_list, container, false);
ButterKnife.bind(this, fragmentView);
setupRecyclerView();
return fragmentView;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
this.userListPresenter.setView(this);
if (savedInstanceState == null) {
this.loadUserList();
}
}
// ...
}
UserListFragment 在 onCreate() 时通过 this.getComponent() 获得 UserListActivity 中注入的 UserComponent 实例,然后对自己进行注入。 这里的 this.getComponent() 是 BaseFragment 的方法:
@SuppressWarnings("unchecked")
protected <C> C getComponent(Class<C> componentType) {
return componentType.cast(((HasComponent<C>) getActivity()).getComponent());
}
2. 在 onCreate() 时注入的问题
Android-CleanArchitecture 的 #124 和 #144 这两个问题揭示了在 onCreate() 时对 Fragment 进行注入的问题:当 Activity 发生重建时,Fragment 在其 onCreate() 中通过 getComponent() 获取的 Comonent 实例是 null。复现这一问题的方法,从 UserListActivity 进入 UserDetailsActivity 后等待一会儿,等系统回收了 UserListActivity 后(可以在 UserListActivity 的 onDestroy() 中加 log 判断),返回 UserListActivity ,就会出现 FC。另一种更简单的复现方法是在开发者选项中打开“不保留活动(Don’t keep activities)”的选项,这样在从 UserListActivity 进入 UserDetailsActivity 后,UserListActivity 会被立即回收,返回 UserListActivity 就会 FC。
再来看 UserListActivity 的 onCreate():
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.activity_layout);
this.initializeInjector();
if (savedInstanceState == null) {
addFragment(R.id.fragmentContainer, new UserListFragment());
}
}
当 savedInstanceState 不为 null 时,不会创建 UserListFragment 的新实例,此时 UserListFragment 在 onCreate() 的时候,UserListActivity 的 onCreate() 还没有结束,UserListActivity 还没有被注入,所以 UserListFragment 在 onCreate() 中获取的 UserComponent 实例是 null。
需要指出的是,如果 UserListActivity 每次都重新创建 UserListFragment 的新实例,就不会有上面的问题,但这是一种浪费。
3. 在 onActivityCreated() 时注入
为了避免上面的问题,一个直接的解决方法就是在 Fragment 的 onActivityCreated() 中进行注入,这样就可以确保 Fragment 总能在 Activity 的 onCreate() 完成后再进行注入。
4. 在 onActivityCreated() 时注入的问题
在 onActivityCreated() 时对Fragment进行注入的问题是,每次 Activity 重建都会重新注入 Fragment,导致之前注入的实例被丢弃,其中保存的状态也就随之丢失。比如使用了 MVP,为 Fragment 注入了 Presenter,当发生屏幕方向旋转时,Activity 重建,Presenter 中的状态全部丢失,显然不是一个友好的用户体验。又如注入的实例中保存了从服务器获取的大量数据,Activity 重建导致数据丢失,不得不重新获取,浪费时间和带宽。
5. 一种有效的注入方式
Efficient and bug-free fragment injection in Android MVP applications 一文给出了一种有效的注入方式,Android-CleanArchitecture 的 PR #134 也使用了类似的方法。简单来说,就是在 Fragment 进行 onCreate() 时先尝试进行注入,如果注入失败,则再在 onActivityCreated() 时注入。使用的 BaseFragment 如下:
/**
* Base {@link android.app.Fragment} class for every fragment in this application.
*/
public abstract class BaseFragment extends Fragment {
private boolean mIsInjected = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
try {
mIsInjected = onInjectView();
} catch (IllegalStateException e) {
Log.e(e.getClass().getSimpleName(), e.getMessage());
mIsInjected = false;
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (mIsInjected) onViewInjected(savedInstanceState);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (!mIsInjected) {
mIsInjected = onInjectView();
if (mIsInjected) onViewInjected(savedInstanceState);
}
}
/**
* Gets a component for dependency injection by its type.
*
* @throws IllegalStateException if component has not been initialized yet.
*/
@SuppressWarnings("unchecked")
protected <C> C getComponent(Class<C> componentType) throws IllegalStateException {
C component = componentType.cast(((HasComponent<C>) getActivity()).getComponent());
if (component == null) {
throw new IllegalStateException(componentType.getSimpleName()
+ " has not been initialized yet.");
}
return component;
}
/**
* Called to do an optional injection. This will be called on {@link #onCreate(Bundle)} and if
* an exception is thrown or false returned, on {@link #onActivityCreated(Bundle)} again.
* Within this method get the injection component and inject the view. Based on returned value
* {@link #onViewInjected(Bundle)} will be called. Check {@link #onViewInjected(Bundle)}
* documentation for more info.
*
* @return True, if injection was successful, false otherwise. Returns false by default.
* @throws IllegalStateException If there is a failure in getting injection component or
* injection process itself. This can occur if activity holding
* component instance has been killed by the system and has not
* been initialized yet.
*/
protected boolean onInjectView() throws IllegalStateException {
// Return false by default.
return false;
}
/**
* Called when the fragment has been injected and the field injected can be initialized. This
* will be called on {@link #onViewCreated(View, Bundle)} if {@link #onInjectView()} returned
* true when executed on {@link #onCreate(Bundle)}, otherwise it will be called on
* {@link #onActivityCreated(Bundle)} if {@link #onInjectView()} returned true right before.
*
* @param savedInstanceState If non-null, this fragment is being re-constructed
* from a previous saved state as given here.
*/
@CallSuper
protected void onViewInjected(Bundle savedInstanceState) {
// Intentionally left empty.
}
}
使用方法为:
public class SampleFragment extends BaseFragment implements SampleView {
@Inject
SamplePresenter mSamplePresenter;
@Override
protected boolean onInjectView() throws IllegalStateException {
getComponent(SampleComponent.class).inject(this);
return true;
}
@Override
protected void onViewInjected(Bundle savedInstanceState) {
super.onViewInjected(savedInstanceState);
this.mSamplePresenter.setView(this);
}
}
BaseFragment 在 onCreate() 时先尝试通过 onInjectView() 进行注入,并使用 mIsInjected 记录此次注入是否成功。onInjectView() 由具体的 Fragment 负责实现,如上面的 SampleFragment。SampleFragment 在其 onInjectView() 中通过 BaseFragment 的 getComponent() 尝试获取 Activity 中注入的 SampleComponent 实例。在 BaseFragment 的 getComponent() 中,如果获取 Component 失败,会抛出 IllegalStateException,被 onCreate() 捕获并记录 mIsInjected 为false。
BaseFragment 会在 onViewCreated() 时检测 mIsInjected ,判断之前在 onCreate() 中的注入是否成功,如果成功,则调用 onViewInjected() 进行相关初始化,否则不进行初始化。BaseFragment 会在 onActivityCreated() 时检测 mIsInjected,如果之前在 onCreate() 注入失败,则再次注入,并调用 onViewInjected() 进行相关初始化。作为具体的Fragment,如SampleFragment ,只需要在 onViewInjected() 中进行相关初始化操作,而不必关心注入具体发生在哪个生命周期。