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.
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.