对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() 中进行相关初始化操作,而不必关心注入具体发生在哪个生命周期。