So, the title of this reflects the question.
to obtain a link on a navigation controller (androidx.navigation.NavController) usually we use following code:
NavController navController = Navigation.findNavController(this, R.id.nav_host_frag);
Is it possible to inject a NavController using Dagger2 framework? (findNavController requires an activity or a view reference)
Maybe it's a silly question and nobody injects the androidx.navigation.NavController, but nevertheless I decided to ask this question to be certain in my assumptions. Thanks ahead
I don't see why you would want to inject the NavController when there are methods for you to find it, also I would be concerned with using dependency injection due to holding a reference to an Activity.
Given you are working with an Activity you would normally find the controller by using the following method:
private val navController: NavController by lazy { findNavController(R.id.main_container) }
Now if we take a look at the source code for the method findNavController() you will notice that it uses an extension function and Navigation.findNavController(this, viewId).
/**
* Find a [NavController] given the id of a View and its containing
* [Activity].
*
* Calling this on a View that is not a [NavHost] or within a [NavHost]
* will result in an [IllegalStateException]
*/
fun Activity.findNavController(#IdRes viewId: Int): NavController =
Navigation.findNavController(this, viewId)
The only thing I would do to complement the above is to create another extension function to facilitate navigation from a Fragment.
fun Fragment.navigate(resId: Int, bundle: Bundle? = null) {
NavHostFragment.findNavController(this).navigate(resId, bundle)
}
Then you could simply use within a Fragment:
navigate(
R.id.action_fragmentA_to_FragmentB,
bundleOf(Global.CAN_NAVIGATE_BACK to false)
)
Why shouldn't this work? You can add it like any other object to a component
through the Component.Builder via #BindsInstance or a module with an argument
by returning it from an #Provides annotated method
Using a #Provides annotated method you need to have the Activity or View available in the component as well. Depending on how you use Dagger you would usually have the specific Activity available, so you can just use that, e.g. for a MyActivityComponent with a MyActivity you could simply return it in a module
#Provides
NavController bindController(MyActivity activity) {
Navigation.findNavController(this, R.id.nav_host_frag)
}
I've answered this in https://stackoverflow.com/a/60061872/789110
In short,
Provide the NavController via usual dagger means, like:
#Provides
fun providesNavController(activity: MainActivity): NavController {
return activity.supportFragmentManager.findFragmentById(R.id.main_content).findNavController()
}
Inject the NavController from onAttach
Inject the NavController lazily to avoid race conditions between Android recreating the Activity and when the NavController can be retrieved:
#Inject
lateinit var navController: Provider<NavController>
Related
I need to use navigation, and I also need in each screen to use an instance of SharedViewModel. Here is what I tried.
class MainActivity : ComponentActivity() {
private lateinit var navController: NavHostController
private val viewModel: SharedViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
navController = rememberNavController()
NavGraph(
navController = navController,
sharedViewModel = sharedViewModel
)
}
}
}
As you can see, I pass the navController and the sharedViewModel to the NavGraph.
fun NavGraph(
navController: NavHostController,
sharedViewModel: SharedViewModel
) {
NavHost(
navController = navController,
startDestination = HomeScreen.route
) {
composable(
route = HomeScreen.route
) {
HomeScreen(
sharedViewModel = sharedViewModel
)
}
composable(
route = ProfileScreen.route
) {
ProfileScreen(
sharedViewModel = sharedViewModel
)
}
}
}
To be able to use the SharedViewModel in each screen, I pass an instance to each composable function. This operation works fine. However, I read that we can inject in each composable an instance of the view model directly using:
fun HomeScreen(
viewModel: SharedViewModel = hiltViewModel()
) {
//...
}
Which approach is better? Is it better to pass an instance of SharedViewModel to all composable functions as in the first approach? Or it is better to inject it directly as in the second?
fun HomeScreen(
viewModel: SharedViewModel = hiltViewModel()
) {
//...
}
With this approach The instance is not really shared (if you do not pass the argument from calling point since it can be omitted because you mentioned its default value) . You are using default value argument for viewModel: SharedViewModel So its optional to pass it to the Composable method . if you do not pass it and when it runs it will get initialized by Hilt In that Composable Scope Only So not a shared one.
you can check this by logging the ViewModel's instance
You can obviously pass it from the calling point but since its a default named_argument its easy to miss to pass it ..
What you can do is just remove the initialization i.e hiltViewModel() from method argument . Then it will be mandatory and you have to pass it while calling the method. Because having a optional parameter doesn't really make sense in this case.
There is an another way of doing it if you do not want to pass it ..
We can make hilt to create ViewModel with Activity's context ..
#Composable
fun mainActivity() = LocalContext.current as MainActivity
fun HomeScreen(viewModel: SharedViewModel = hiltViewModel(mainActivity())) {
}
This way also you will get same instance of VM hence a shared one . In this case this composable is kind of restricted to a Single Activity . So u gotta watch out for it if u use this in some other Activity it will crash with cast exception for MainActivity . But in Single Activity architecture it will be fine or u can just further add the checks for Activity i guess.
I need to open a Compose component with its own ViewModel and pass arguments to it, but at the same time I inject dependencies to this ViewModel. How can I achieve this? Can I combine ViewModel factory and Dependency Injection (Hilt)?
Yes. you can..
Have your component be like this:
#Composable
fun MyScreen(
viewModel: MyViewModel = hiltViewModel()
) {
...
}
and in your viewModel:
#HiltViewModel
class MyViewModel #Inject constructor(
private val repository: MyRepository,
... //If you have any other dependencies, add them here
): ViewModel() {
...
}
When you pass arguments to the ViewModel, make sure that Hilt knows where to get that dependency. If you follow the MVVM architecture, then the ViewModel should handle all the data and the composable all the ui related components. So usually, you only need the ViewModel injection into the composable and all the other data injected dependencies into the ViewModel.
The composable should only care about the data that it gets from the ViewModel. Where the ViewModel gets that data and the operations it does on that data, it does not care.
Lemme know if this is what you meant..
Check out the official website for more:
Hilt-Android
Yes, you can. This is called "Assisted Inject" and it has it's own solutions in Hilt, Dagger(since version 2.31) and other libraries like AutoFactory or square/AssistedInject.
In this article, you can find an example of providing AssistedInject in ViewModel for Composable with Hilt Entry points.
Here is some code from article in case if article would be deleted:
In the main Activity, we’ll need to declare EntryPoint interface which will provide Factory for creating ViewModel:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
#EntryPoint
#InstallIn(ActivityComponent::class)
interface ViewModelFactoryProvider {
fun noteDetailViewModelFactory(): NoteDetailViewModel.Factory
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NotyTheme {
NotyNavigation()
}
}
}
}
We get Factory from Activity and instantiating our ViewModel with that Factory and assisted some field:
#Composable
fun noteDetailViewModel(noteId: String): NoteDetailViewModel {
val factory = EntryPointAccessors.fromActivity(
LocalContext.current as Activity,
MainActivity.ViewModelFactoryProvider::class.java
).noteDetailViewModelFactory()
return viewModel(factory = NoteDetailViewModel.provideFactory(factory, noteId))
}
Now just go to your navigation components and use this method to provide ViewModel to your Composable screen as following:
NavHost(navController, startDestination = Screen.Notes.route, route = NOTY_NAV_HOST_ROUTE) {
composable(
Screen.NotesDetail.route,
arguments = listOf(navArgument(Screen.NotesDetail.ARG_NOTE_ID) { type = NavType.StringType })
) {
val noteId = it.arguments?.getString(Screen.NotesDetail.ARG_NOTE_ID)!!
NoteDetailsScreen(navController, noteDetailViewModel(noteId))
}
}
According to the latest changes in Android Jetpack Navigation component NavHostFragment creates instance of NavController in his onCreate() method before any child fragment is recreated. Consequently it allows to call findNavController() even in our own fragment's onCreate() method.
Here is the official example of testing navigation call(https://developer.android.com/guide/navigation/navigation-testing):
#RunWith(AndroidJUnit4::class)
class TitleScreenTest {
#Test
fun testNavigationToInGameScreen() {
// Create a TestNavHostController
val navController = TestNavHostController(
ApplicationProvider.getApplicationContext())
// Create a graphical FragmentScenario for the TitleScreen
val titleScenario = launchFragmentInContainer<TitleScreen>()
titleScenario.onFragment { fragment ->
// Set the graph on the TestNavHostController
navController.setGraph(R.navigation.trivia)
// Make the NavController available via the findNavController() APIs
Navigation.setViewNavController(fragment.requireView(), navController)
}
// Verify that performing a click changes the NavController’s state
onView(ViewMatchers.withId(R.id.play_btn)).perform(ViewActions.click())
assertThat(navController.currentDestination?.id).isEqualTo(R.id.in_game)
}
}
In this example NavController object is set only when the fragment's view is already available.
Is there any pure standard way to test the navigation call if we have it during fragment's onCreate() method?
e.g.
class TitleScreen : Fragment(R.layout.fragment_title_screen) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
findNavController().navigate(R.id.action_title_screen_to_in_game)
}
}
I have created Navigation Drawer Activity:
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
val navView: NavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(navController.graph, drawerLayout)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
}
And I have mobile_navigation.xml:
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="#+id/mobile_navigation"
app:startDestination="#id/databaseFragment">
<fragment
android:id="#+id/databaseFragment"
android:name="com.acmpo6ou.myaccounts.ui.DatabaseFragment"
android:label="fragment_database_list"
tools:layout="#layout/fragment_database_list" >
<action
android:id="#+id/actionCreateDatabase"
app:destination="#id/createDatabaseFragment" />
</fragment>
<fragment
android:id="#+id/createDatabaseFragment"
android:name="com.acmpo6ou.myaccounts.ui.CreateDatabaseFragment"
android:label="create_edit_database_fragment"
tools:layout="#layout/create_edit_database_fragment" />
</navigation>
The start destination is DatabaseFragment. However there is a problem, here is my DatabaseFragment:
class DatabaseFragment(
override val adapter: DatabasesAdapterInter,
val presenter: DatabasesPresenterInter
) : Fragment(), DatabaseFragmentInter {
...
companion object {
#JvmStatic
fun newInstance(
adapter: DatabasesAdapterInter,
presenter: DatabasesPresenterInter
) = DatabaseFragment(adapter, presenter)
}
}
As you can see my DatabaseFragment should receive two arguments to its constructor: adapter and presenter. This is because of dependency injection, in my tests I can instantiate DatabaseFragment passing through mocked adapter and presenter. Like this:
...
val adapter = mock<DatabasesAdapterInter>()
val presenter = mock<DatabasesPresenterInter>()
val fragment = DatabaseFragment(adapter, presenter)
...
It works with tests, but it doesn't work with android navigation. It seems that Android Navigation Components create DatabaseFragment instead of me, but they don't pass any arguments to fragment's constructor and it fails with error that is too long to post it here.
Is there a way to tell Navigation Components so that they pass appropriate arguments to my fragments when instantiating them?
Thanks!
Short answer is no, you can not pass arguments to Fragment.
All subclasses of Fragment must include a public no-argument constructor. The framework will often re-instantiate a fragment class when needed, in particular during state restore, and needs to be able to find this constructor to instantiate it. If the no-argument constructor is not available, a runtime exception will occur in some cases during state restore.
I just want to add to i30mb1 answer:
Is there really a necessity for you to pass those two arguments in the constructor?
As far as I know and as far as I have experimented with MVP, each view should have a presenter. So for example when I create a new fragment, I create a new presenter for it. Then the parent activity should have another presenter. If you need that presenter so that the fragment can make changes in the Activities view, you could implement interfaces, but that's another topic.
If you ever need to pass simple arguments using navigation like POJOS or even simplier objects like Strings etc.. you can use SafeArgs https://developer.android.com/guide/navigation/navigation-pass-data
I fixed everything pretty easily using default arguments, like this:
class DatabaseFragment(
override val adapter: DatabasesAdapterInter = DatabasesAdapter(),
val presenter: DatabasesPresenterInter = DatabasesPresenter()
) : Fragment(), DatabaseFragmentInter {
...
companion object {
#JvmStatic
fun newInstance() = DatabaseFragment()
}
}
I'm trying to share a ViewModel between my activity and my fragment. My ViewModel contains a report, which is a complex object I cannot serialize.
protected val viewModel: ReportViewModel by lazy {
val report = ...
ViewModelProviders.of(this, ReportViewModelFactory(report)).get(ReportViewModel::class.java)
}
Now I'm trying to access the viewmodel in a fragment, but I don't want to pass all the factory parameters again.
As stated by the ViewModelProvider.get documentation:
Returns an existing ViewModel or creates a new one in the scope
I want to access the ViewModel instance defined in the activity, so I tried the following but it logically crashes as the model doesn't have an empty constructor:
protected val viewModel: ReportViewModel by lazy {
ViewModelProviders.of(requireActivity()).get(ReportViewModel::class.java)
}
How one should access its "factorysed" ViewModels in a fragment? Should we pass the factory to the fragment?
Thanks!
A little late but I had this question myself. What I found is you can do the following:
In your activity override getDefaultViewModelProviderFactory() like so:
override fun getDefaultViewModelProviderFactory(): ReportViewModelFactory {
return ReportViewModelFactory(report)
}
now in your fragments you can do
requireActivity().getDefaultViewModelProviderFactory()
to get the factory.
Or simply instantiate your viewModel like:
private val viewModel: ReportViewModel by activityViewModels()