Skip to main content
Version: Next

Bundle Injection

The goal

Fragment runtime arguments must be passed via a Bundle in order for the arguments to be present if the Fragment is recreated by a FragmentManager. For those of us who don't want to rely upon Androidx Navigation, there's still quite a lot of boilerplate involved in passing these arguments and ensuring that it's compile-time safe.

Tangle removes as much of that boilerplate as possible, while using some Dagger tricks to prevent creating new instances without their arguments.

note

Use @FragmentInject instead of @Inject

import androidx.fragment.app.Fragment
import com.example.AppScope
import tangle.fragment.ContributesFragment
import tangle.fragment.FragmentInject
import tangle.fragment.FragmentInjectFactory
import tangle.fragment.arg
import tangle.inject.TangleParam

@ContributesFragment(AppScope::class)
class MyFragment @FragmentInject constructor() : Fragment() {

val name by arg<String>("name")

@FragmentInjectFactory
interface Factory {
fun create(@TangleParam("name") name: String): MyFragment
}
}

val myFragmentFactory: MyFragment.Factory = TODO("use your favorite Dagger pattern here")

val fragment = myFragmentFactory.create(name = "Bigyan")

Background

Since long before FragmentFactory and Androidx Navigation, it has long been common practice to create static newInstance functions which take the deconstructed Bundle parameters and return a Fragment instance which already has those arguments injected as a Bundle.

Here's what it may look like in Kotlin:

import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment

class MyFragment : Fragment() {

companion object {
fun newInstance(name: String): MyFragment {
val myFragment = MyFragment()

myFragment.arguments = bundleOf("name" to name)
return myFragment
}
}
}

Tangle's generated factories

For the MyFragment definition above, Tangle will generate the following:

import androidx.core.os.bundleOf
import dagger.internal.InstanceFactory
import javax.inject.Provider

public class MyFragment_Factory_Impl(
public val delegateFactory: MyFragment_Factory
) : MyFragment.Factory {
public override fun create(name: String): MyFragment {
val bundle = bundleOf(
"name" to name
)
return delegateFactory.get().apply {
this@apply.arguments = bundle
}
}

public companion object {
@JvmStatic
public fun create(delegateFactory: MyFragment_Factory): Provider<Factory> =
InstanceFactory.create(MyFragment_Factory_Impl(delegateFactory))
}
}

It will then create a Dagger binding for MyFragment_Factory_Impl to MyFragment.Factory, which allows us to use it in our code:

import javax.inject.Inject
import javax.inject.Provider

class MyNavigationImpl @Inject constructor(
// fragments without bundle arguments can be injected in a Provider
val myListFragmentProvider: Provider<MyListFragment>,
// fragments with a factory must be injected this way
val myFragmentFactory: MyFragment.Factory
) : MyNavigation {

override fun goToMyListFragment(name: String){
val fragment = myFragmentFactory.create(name)
// actual navigation logic would go here
}
override fun goToMyFragment(name: String){
val fragment = myFragmentFactory.create(name)
// actual navigation logic would go here
}
}

These factories are essentially an "entry point" to the TangleFragmentFactory. Once the factory has initialized its Fragment, the arguments are established and cached by the Android framework. If the Fragment needs to be recreated by the TangleFragmentFactory, the new instance will be created using a Provider and just invoking the constructor, without recreating the Bundle.

Limiting access

If a Fragment requires a custom factory for bundle arguments, Tangle does create a @Provides-annotated function, but it's hidden behind a qualifier:

@Provides
@TangleFragmentProviderMap
public fun provideMyFragment(): MyFragment = MyFragment_Factory.newInstance()

This means that if anyone attempts to inject it like a normal Dagger dependency:

class SomeClass @Inject constructor(
val myFragmentProvider: Provider<MyFragment>
)

...Dagger will fail the build with a very familiar error message:

[Dagger/MissingBinding] com.example.MyFragment cannot be provided without an @Inject constructor or an @Provides-annotated method.