Part 3:The dagger.android Missing Documentation, Fragments

2020-11-22 21:26:33

参考地址 The dagger.android Missing Documentation, Part 3: Fragments


Welcome to Part 3 of this series on dagger.android. If you're just joining us, you can check out the first two parts here:

  1. Part 1 — Basic Setup

  2. Part 2 — ViewModels and View Model Factories

  3. This part

Here in Part 3, we'll learn about injecting Fragments, injecting retained Fragments (where getRetainInstance() returns true), sharing objects owned by the host Activity, and a gotcha involving ViewModels that have custom ViewModelProvider.Factorys.

Injecting a Fragment

As we all know, it would be Very Bad to attempt to perform constructor injection on a Fragment.

// NEVER DO THISclass VeryBadFragment(private val someInt: Int) : Fragment

There's a reason Android Studio's new-fragment wizard always adds a default (no-arg) constructor, and that reason is that the framework may sometimes have to recreate your fragment later, and if it does so, it will call the no-arg constructor, leaving your app in a bad state or even causing a crash if you don't have such a constructor available. This is why code such as this is so common:

class GoodFragment : Fragment {
  companion object {
    fun newInstance(someInt: Int): GoodFragment {
      // Gross, looks like Java
      val args = Bundle()
      args.putInt("SOME_INT", someInt)
      val fragment = GoodFragment()
      fragment.arguments = args
      return fragment
    }
  }

  // You cannot have two companion objects in a single class; this is just a demonstration
  companion object {
    fun newInstance(someInt: Int): GoodFragment {
      // So much nicer! Uses `T.apply()` from Kotlin stdlib and
      // `bundleOf(vararg pairs: Pair<String, Any?>)` from android-ktx
      return GoodFragment().apply {
        arguments = bundleOf("SOME_INT" to someInt)
      }
    }
  }}

We do this because the framework can re-use the Bundle of arguments later on, but it can't remember that you called a non-default constructor. This begs the question, though: how do we inject complex objects?

Step 1: Implement HasSupportFragmentInjector

The first thing we have to do is implement the HasSupportFragmentInjector interface. The most logical place to do this is on the Fragment's host Activity, but you can also have your custom Application class do this (you'll see an example of this later on).

class SimpleFragmentActivity : AppCompatActivity(), HasSupportFragmentInjector {

  @Inject lateinit var fragmentInjector: DispatchingAndroidInjector<Fragment>
  override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentInjector

  override fun onCreate(savedInstanceState: Bundle?) {
    AndroidInjection.inject(this)
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_simple_fragment)
  }}

You will recognize this pattern from Part 1, when our MainApplication implemented the HasActivityInjector interface and was injected with an instance of DispatchingAndroidInjector<Activity>.

Step 2: Dagger Magic

Implementing that interface isn't quite enough. We also need to let Dagger know a few things.

@Module abstract class SimpleFragmentActivityModule {
  @ContributesAndroidInjector abstract fun simpleFragment(): SimpleFragment}

Again, this is very similar to the code we saw in Part 1 for generating subcomponent definitions for activities.

Now that we have this module, we need to install it somewhere.

@Moduleabstract class ScreenBindingModule {
  @ActivityScoped
  @ContributesAndroidInjector(modules = [
    // Right here.
    SimpleFragmentActivityModule::class
  ]) 
  abstract fun simpleFragmentActivity(): SimpleFragmentActivity}

And, as you may recall, ScreenBindingModule itself is installed in our root, @Singleton-scoped component.

Step 3: Have Something Worth Injecting

Let's edit our activity class a bit.

class SimpleFragmentActivity : AppCompatActivity(), HasSupportFragmentInjector {

  // Everything else as before

  // Yes, I'm kind of embarrassed at the class name in retrospect, but it didn't seem so 
  // bad when I had IDE auto-completion...
  @Inject lateinit var viewModelFactory: SimpleFragmentActivityViewModelFactory
  private val viewModel: SimpleFragmentActivityViewModel by lazy {
    ViewModelProviders.of(this, viewModelFactory)
        .get(SimpleFragmentActivityViewModel::class.java)
  }}

We're injecting a view model factory, but it could be anything. It turns out we also want to inject this into our fragment instance, which is the entire motivation for HasSupportFragmentInjector. Let's take a look at our sample fragment class:

class SimpleFragment : Fragment() {

  @Inject lateinit var viewModelFactory: SimpleFragmentActivityViewModelFactory
  private val viewModel: SimpleFragmentActivityViewModel by lazy {
    ViewModelProviders.of(activity!!, viewModelFactory)
        .get(SimpleFragmentActivityViewModel::class.java)
  }

  override fun onAttach(context: Context) {
    // This static method ends up calling 
    // `((activity as HasSupportFragmentInjector).inject(this)`, which is where the 
    // interface comes into play
    AndroidSupportInjection.inject(this)
    super.onAttach(context)
  }}

You may have noticed that we inject activities in onCreate(Bundle?) before the call to super.onCreate(Bundle?). In fragments, we wish to inject sooner (docs), and so we use onAttach(Context); again, we do so before the call to super.onAttach(Context).

That's actually it. You now know how to inject a Fragment in Android, and this will serve for 90-95% of cases. There are a few lingering questions, though....

Why Inject a View Model Factory into a Fragment?

After all, wouldn't the following work?

class SimpleFragment : Fragment() {

  private val viewModel: SimpleFragmentActivityViewModel by lazy {
    // Don't do this. Really. You'll regret it.
    ViewModelProviders.of(activity!!).get(SimpleFragmentActivityViewModel::class.java)
  }

  override fun onAttach(context: Context) {
    AndroidSupportInjection.inject(this)
    super.onAttach(context)
  }}

I mean, it sure seems to work. I get my ViewModel as expected, the app doesn't crash....

Confused

Hint: remember what I said earlier about the framework calling our fragment's default constructor? One time it does that is during back navigation. If we navigate back to this fragment sometime later, our app will crash. This is because the Android framework actually instantiates and attaches the fragment before finishing the onCreate of our activity! The above code works the first time because the view model has already been created and is keyed to the activity — meaning we're just getting an existing view model from a Map<Activity, ViewModel>. However, during back navigation, when the fragment is created first, that map.get(activity) call returns null and we're out of luck. Therefore, we need the view model factory for the back-navigation case when we do need to create a new view model.

Sigh. Android. #programmingishard

Injecting a Retained Fragment, or, Never Inject Something Twice

First we should get this out of the way: you should probably avoid using retained fragments. While they may seem to solve an important problem (retaining state across rotations), they do so at the cost of your sanity.

Now that that's out of the way, let's just assume we already have a retained fragment, and we're not going to refactor it away anytime soon -- so how do we inject it, and why is it any different than injecting a normal fragment?

Here's our first, naive attempt:

// RetainedFragment.ktclass RetainedFragment : Fragment() {

  @Inject lateinit var thing: Thing

  override fun onAttach(context: Context) {
    AndroidSupportInjection.inject(this)
    super.onAttach(context)
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    retainInstance = true // yup, a retained fragment
  }}@Moduleabstract class RetainedFragmentModule {
  @Binds @FragmentScoped abstract fun bindThing(impl: ThingImpl): Thing}// ScreenBindingModule.kt@Moduleabstract class ScreenBindingModule {
  @FragmentScoped
  @ContributesAndroidInjector(modules = [RetainedFragmentModule::class]) 
  abstract fun upgradeRetainedFragment(): RetainedFragment}// MainApplication.kt// Implements HasSupportFragmentInjector because our fragment is scoped just below // `@Singleton`class MainApplication : Application(), HasActivityInjector, HasSupportFragmentInjector

Now an interesting thing will happen. Let's assume that the whole reason we want a retained fragment is so that thing will be immune to device rotation. Our host activity can get destroyed and recreated all it likes, but our retained fragment will only get created once.

Well, it turns out that, every time you call AndroidSupportInjection.inject(this), you're going to end up with a new thing. Even though you scoped your binding code, it doesn't matter. To understand this, we need to know a bit more about the AndroidInjection and AndroidSupportInjection classes, as well as the "magic" annotation @ContributesAndroidInjector.

@ContributesAndroidInjector

This is really a bit of "syntactic sugar" (in the dagger sense, so it makes you bleed), and it replaces something like the following:

ContributesAndroidInjector

You can see why the annotation is preferable. But the point is that what you're doing is defining a Subcomponent.Builder, not the Subcomponent itself! Boiling it all down to its essence, you're creating a Map<HasSupportFragmentInjector, Subcomponent.Builder>. So, whenever you call Android[Support]Injection.inject(this), the Android[Support]Injection class grabs the subcomponent builder from the map, creates a new subcomponent, and then uses that subcomponent to inject your object. And this is why thing is new each time you inject your retained fragment. The dagger framework is not retaining a reference to your subcomponent, even though you scoped it, so it has nowhere to store those references.

Don't Inject Twice

There are actually a lot of ways to avoid this problem. The hard part, for me anyway, was understanding why it was a problem in the first place. Let me share my solution.

class RetainedFragment : Fragment(), HasViewInjector {

    @Inject lateinit var thing: Thing

    override fun onAttach(context: Context) {
        if (!::thing.isInitialized) {
            AndroidSupportInjection.inject(this)
        }
        super.onAttach(context)
    }}

A little inelegant, perhaps, but effective! Since Kotlin 1.2, it is now possible to check if a lateinit var has been initialized, so I use that feature to decide whether or not to inject my fragment. I might also have declared my property @Inject var thing: Thing? and used a null-check, but then I would have to use the safe access operator (?.) everywhere. Another possibility would be to inject in onCreate instead, since I know that only ever gets called once for a retained fragment. I didn't like that approach because it was directly contrary to the advice in the dagger.android documentation, and my own personal experience dealing with fragments and their frankly bizarre lifecycles.

Bottom line: never inject twice.


  • 2017-09-11 16:35:22

    ngx_http_realip_module使用详解

    网络上关于ngx_http_realip_module的文章千篇一律,全是在说怎么安装,最多贴一个示例配置,却没有说怎么用,为什么这么用,官网文档写得也十分简略,于是就自己探索了一下。

  • 2017-09-11 16:39:43

    基于Nginx dyups模块的站点动态上下线

    在分布式服务下,我们会用nginx做负载均衡, 业务站点访问某服务站点的时候, 统一走nginx, 然后nginx根据一定的轮询策略,将请求路由到后端一台指定的服务器上。

  • 2017-09-13 13:49:21

    Web性能测试:工具之Siege详解

    Siege是一款开源的压力测试工具,设计用于评估WEB应用在压力下的承受能力。可以根据配置对一个WEB站点进行多用户的并发访问,记录每个用户所有请求过程的相应时间,并在一定数量的并发访问下重复进行。siege可以从您选择的预置列表中请求随机的URL。所以siege可用于仿真用户请求负载,而ab则不能。但不要使用siege来执行最高性能基准调校测试,这方面ab就准确很多

  • 2017-09-14 10:18:25

    15分钟成为Git专家

    不管是以前使用过 Git 还是刚开始使用这个神奇的版本控制工具的开发者,阅读了本文以后都会收获颇丰。如果你是应一名有经验的 GIT 使用者,你会更好的理解 checkout -> modify -> commit 这个过程。如果你刚开始使用 Git,本文将给你一个很好的开端。

  • 2017-09-28 16:42:57

    Linux vmstat命令实战详解

    vmstat命令是最常见的Linux/Unix监控工具,可以展现给定时间间隔的服务器的状态值,包括服务器的CPU使用率,内存使用,虚拟内存交换情况,IO读写情况。这个命令是我查看Linux/Unix最喜爱的命令,一个是Linux/Unix都支持,二是相比top,我可以看到整个机器的CPU,内存,IO的使用情况,而不是单单看到各个进程的CPU使用率和内存使用率(使用场景不一样)。

  • 2017-10-13 16:21:29

    Activity的四种launchMode

    launchMode在多个Activity跳转的过程中扮演着重要的角色,它可以决定是否生成新的Activity实例,是否重用已存在的Activity实例,是否和其他Activity实例公用一个task里。这里简单介绍一下task的概念,task是一个具有栈结构的对象,一个task可以管理多个Activity,启动一个应用,也就创建一个与之对应的task。

  • 2017-10-16 16:45:45

    Android开发技巧:Application和Instance

    在开发过程中,我们经常会需要用到一些全局的变量或者全局的“管理者”,例如QQ,需要有一个“全局的管理者“保存好友信息,各个activity即可直接通过该”管理者“来获取和修改某个好友信息,显然,这样的一个好友信息,保存到某一个具体的activity里面,然后依靠activity的intent来传递参数是不合适。我们有两种方法来实现这样一个全局的管理者,一种是使用C++/Java中常用的单例模式,另一种是利用Android的Application类,下面一一阐述。