对Fragment进行依赖注入的时机和方式

  在使用 Dagger2 对 Android 应用进行依赖注入时,有时需要先注入 Activity ,然后 Fragment 从 Activity 中获取被注入的 Component 实例,再对自己(Fragment)进行注入。Fragment 的生命周期和 Activity 不同,注入时需要考虑注入的时机。

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