How to Inject NavController to an activity using Hilt

I'm new to Hilt. Trying to convert from Dagger to Hilt. But I'm stuck on injecting NavController to the Activity.

So to create NavController instance I need to have access to the activity and the host fragment Id. In Dagger this was my approach to create that instance.

First, create ActivityModule

@Module
class ActivityModule {

    @ActivityScope
    @Provides
    fun navigation(activity: MainActivity, hosFragment: Int) =
        Navigation.findNavController(activity,hosFragment)
}

In the second step ActivitySubComponent has the ability to take activity and host fragment id as parameters.

@ActivityScope
@Subcomponent(modules = [ActivityModule::class])
interface ActivitySubComponent {

    @Subcomponent.Factory
    interface Factory{
        fun create(@BindsInstance activity: MainActivity,
                   @BindsInstance hostFragment: Int): ActivitySubComponent
    }

    fun inject(MainActivity: MainActivity)
}

After adding this subcomponent with other subcomponents to the ApplicationComponent from you can get the instance of NavController to the fragment like below.

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var navigationController: NavController
    lateinit var viewBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        injector.activityComponent().create(this, R.id.hostFragment).inject(this)

    }
}

injector coming from the following class.

interface InjectorProvider {
  val component: ApplicationComponent
}

val Activity.injector get() = (application as InjectorProvider).component
val Fragment.injector get() = (requireActivity().application as InjectorProvider).component

When I was trying to transition into Hilt, the first issue I faced was how to provide parameters to the module. Not like in Dagger I could not find an alternative for Subcomponent Factory. Is it even possible to provide parameters to a module on Hilt?

Anyways, I decided to provide the FragmentContainerView id to the NavController manually since it's not changing. With that, I came to the below solution in Hilt.

@Module
@InstallIn(ActivityComponent::class)
object NavigationModule {

    @Provides
    fun navigationController(activity: Activity) =
        activity.findNavController(R.id.hostFragment)
}

And tried to inject it into my MainActvity like below.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var navigationController: NavController
    lateinit var viewBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        initialization()

    }

    private fun initialization() {

        setSupportActionBar(viewBinding.toolbar)
        NavigationUI.setupActionBarWithNavController(this, navigationController)

    }

}

Application built successfully but now I'm getting a run time error saying "ID does not reference a View inside this Activity" but my activity layout is constructed like below.

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/purple_700"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/hostFragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/main_navigation" />

</LinearLayout>

Error pointing at super.onCreate(savedInstanceState) line on the MainActivity. Please let me know what am I missing here. Thanks.

Upvotes: 1

Views: 2069

Answers (1)

I was able to figure it out. The above issue must be caused by the fact the UI is not available at the time it tries to inject the NavController instance. So Hilt has a way of defining an interface and Injecting that interface into the Activity. And we can define the classes by that implementing that interface. This gives us a chance to provide the parameters necessary to build our instance when they are available.

First, create the interface like below.

interface AppNavigator {
    
    fun getNaveHostFragment(hostFragmentId: Int): NavController
    
}

Secondly, create an implementation class.

class AppNavigatorImplementation @Inject constructor(private val activity: FragmentActivity): AppNavigator {

    override fun getNaveHostFragment(hostFragmentId: Int): NavController {
        val navHostFragment = activity.supportFragmentManager.findFragmentById(R.id.hostFragment) as NavHostFragment
        return navHostFragment.navController
    }

}

After that define NavigationModule like below.

@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {

    @Binds
    abstract fun bindNavigator(impl: AppNavigatorImplementation): AppNavigator

}

Finally, you can inject that instance to the activity like below.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var navigator: AppNavigator
    lateinit var viewBinding: ActivityMainBinding
    private lateinit var navigationController: NavController

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        initialization()

    }

    private fun initialization() {

        navigationController = navigator.getNaveHostFragment(R.id.hostFragment)

        setSupportActionBar(viewBinding.toolbar)
        NavigationUI.setupActionBarWithNavController(this, navigationController)

    }
}

Upvotes: 1

Related Questions