Blog
January 9, 2019Using AndroidX's FragmentFactory with Dagger for Fragment dependency injection
- Topics
- Mobile Innovation
In the newest release of AndroidX's Fragment support library (1.1.0-alpha01), there was an exciting new bit of functionality:
You can now set a FragmentFactory on any FragmentManager to control how new Fragment instances are instantiated.
Previously, Fragments required a no-arg default constructor so that an Activity would be able to re-create the Fragment after an orientation change or process death. The new FragmentFactory functionality will allow developers to fine-tune the creation (and re-creation) of their Fragment instances, opening up the possibility for constructor injection using a library like Dagger.
Before we begin, let's inspect the default implementation of a FragmentFactory to see how it creates new Fragments:
@NonNull
public Fragment instantiate(@NonNull ClassLoader classLoader,
@NonNull String className,
@Nullable Bundle args) {
try {
Class? extends Fragment> cls = loadFragmentClass(classLoader, className);
return cls.getConstructor().newInstance();
} catch (java.lang.InstantiationException e) {
/* ... */
} /* more exception handling */
}
The instantiate method takes a ClassLoader and a String representation of the Fragment's name and uses a static utility method called loadFragmentClass to pull the Class of the Fragment that needs to be created. It then invokes the no-arg constructor, and creates the new instance of the Fragment. This is reason Fragments previously required having only a default, no-arg constructor, so that when an Activity attempts to restore a Fragment, it can invoke the default constructor and continue without having to worry about any dependencies or otherwise being injected in non-default constructors. Because of this, injecting dependencies on Fragments with Dagger previously required using field-level injection.
By supplying a custom FragmentFactory to our FragmentManager we will be able to take advantage of constructor injection with Dagger to create our Fragments and inject the relevant dependencies - no more need for field dependency injection!
Our custom FragmentFactory will make use of the Map Multibinding functionality of Dagger to make it easy for us to map Fragment Classes to Dagger's Providers, which we will use to create and inject our Fragment instance (NOTE: This will not go through a full setup of Dagger, there is already plenty of good documentation on that available). Full code of the sample app can be found on Github.
First step, we need to add our Fragment classes to a Map using the @Binds and @IntoMap Dagger annotations, and this map will be used in our custom FragmentManager:
@Module
abstract class FragmentBindingModule {
@Binds
@IntoMap
// FragmentKey annotation used for specifying the MapKey for associating the value
// to our map
@FragmentKey(MainFragment::class)
abstract fun bindMainFragment(mainFragment: MainFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SecondFragment::class)
abstract fun bindSecondFragment(secondFragment: SecondFragment): Fragment
@Binds
abstract fun bindFragmentFactory(factory: DaggerFragmentInjectionFactory): FragmentFactory
}
Next, we can take a look at the custom DaggerFragmentInjectionFactory, that will take care of creating our Fragments and injecting their dependencies.
/**
* [FragmentFactory] class that takes a map of [Fragment] classes and related
* Dagger [Provider] instances to create new [Fragment] instances using dependency injection
*/
@PerActivity
class DaggerFragmentInjectionFactory @Inject constructor(
private val creators: Map, @JvmSuppressWildcards Provider>
) : FragmentFactory() {
override fun instantiate(
classLoader: ClassLoader,
className: String,
args: Bundle?): Fragment {
val fragmentClass = loadFragmentClass(classLoader, className)
val creator = creators[fragmentClass]
?: throw IllegalArgumentException("Unknown fragment class $fragmentClass")
try {
val fragment = creator.get()
fragment.arguments = args
return fragment
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
Our custom FragmentFactory makes use of the same loadFragmentClass method to pull the Class of the Fragment we are trying to create, then pulls the related Provider out of our map and creates the injected Fragment. We're also setting the newly created Fragment's arguments to the passed in value, although that is not handled in the default FragmentFactory (it can be left up to preference how you want to handle setting your Fragment's arguments).
Below, we can see how we setup and use the custom FragmentFactory in our MainActivity class. Note there are a few caveats - most importantly, we must set our custom factory before calling super.onCreate() in our Activity, so that our custom factory is set before we attempt to re-create our Fragments in the case of a configuration change or returning from process death. You also must use the factory when creating your Fragments to be added to your FragmentManager - while it looks a little clunky, it can be greatly improved with an extension function if you're using Kotlin (see sample code).
@Inject
lateinit var fragmentFactory: FragmentFactory
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
// must set this BEFORE super.onCreate() so that the custom fragment factory is used in
// re-creating fragments after rotation or process death
supportFragmentManager.fragmentFactory = fragmentFactory
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
// AndroidKTX fragment extensions!!
supportFragmentManager.transaction {
replace(R.id.fragment_container,
// use the attached fragment factory to instantiate our new fragment
supportFragmentManager.fragmentFactory.instantiate(
MainFragment::class.java.classLoader!!,
MainFragment::class.java.name,
null)
)
}
}
}
Now we can setup our Fragments using constructor injection. In this case we are injecting an App-level dependency with a reference to our SharedPreferences and an Activity-level dependency with a ToolbarProvider to get references to our Activity's Toolbar (a bit contrived, but there are tons of possibilities here).
// suppress or disable this lint error - Android studio has not caught up yet!
@SuppressLint("ValidFragment")
class MainFragment @Inject constructor(
private val sharedPreferences: SharedPreferences,
private val toolbarCallbacks: ToolbarCallbacks
) : Fragment() {
/* fragment implementation */
}
But why is this important? Why should we care about being able to use constructor injection for Fragments? The answer, as it often is, is testability. Google is making a push for making Fragments more testable in isolation - releasing their FragmentScenario in the same AndroidX alpha release they introduce the FragmentFactory. Having constructor dependency injection will allow you to easily include mocked dependencies for testing your Fragments, and will also clear up your code from field-level injection and needing to pull references during Fragment lifecycle operations (note that this does not mean you can ignore the Fragment lifecycle when accessing your dependencies).
This new functionality for creating Fragment instances is a welcome addition to the Android ecosystem for sure, and it (hopefully) is just another step in Google making Android development more flexible and robust.