android navigation pass arguments to fragment constructor - android

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()
}
}

Related

Test Android Jetpack Navigation findNavController in onCreate method

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)
}
}

How to safeargs in viewModel Kotlin Android?

I have a problem with my MVVM structure. I create apps and pass data between fragments. Now it works fine, but I need to add this logic to my ViewModel.
This is my NotesClickFragment:
#AndroidEntryPoint
class NotesClickFragment : Fragment(R.layout.fragment_click_notes) {
private val args by navArgs<NotesClickFragmentArgs>()
private val viewModel: NotesClickViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentClickNotesBinding.bind(view)
binding.apply {
textViewTitleClick.setText(args.notesClickArgs.titleNotes)
textViewContentNotesClick.setText(args.notesClickArgs.contentNotes)
textViewHistoryClick.setText(args.notesClickArgs.createdNotesDateFormat)
}
}
}
This is my NotesClickViewModel:
class NotesClickViewModel #ViewModelInject constructor(
private val notesDao: NotesDao
) : ViewModel() {
}
I'm trying to add a private argument val navArgs: NotesClickFragmentArgs and create the other functions to set data from fragments but it doesn't work. What is good practice? Thanks in advance for your tips.
NotesDao has no place in ViewModel and instead should be inside Repository.
For more information on MVVM, please read the following: Guide to app architecture
As far as I know, ViewModel cannot be used here. Navigation should be handled inside Fragment itself.
In order to use SafeArgs you first need to declare parameters inside NavGraph and then you can use Directions to navigate to a fragment where you can pass your necessary data.
For example. Let's say I have list of movies in RecyclerView. When I click on any of the movies I want to see id of the movie I clicked in a new fragment.
Step 1: Declare information in NavGraph
This is how Fragment is declared in my NavGraph. The tag argument means that this Fragment accepts Int(id).
<fragment
android:id="#+id/movieDetailsFragment"
android:name="my.test.MovieDetailsFragment"
android:label="fragment_movie_details"
tools:layout="#layout/fragment_movie_details">
<argument
android:name="id"
app:argType="integer" />
</fragment>
Then, in any other Fragment connected to MovieDetailsFragment I simply use Directions and pass id that I need.
val direction = MoviesPopularFragmentDirections.actionPopularFragmentToMovieDetailsFragment(id = movie.id)
this.findNavController().navigate(direction)
And that should be it.
Again, more information in official documentation here Navigation Safe Args

How should i use ViewModel in two fragments?

I have an app with one activity and two fragments, in the first fragment, I should be able to insert data to the database, in the second I should be able to see the added items in a recyclerView.
So I've made the Database, my RecyclerView Adapter, and the ViewModel,
the issue is now how should I manage all that?
Should I initialize the ViewModel in the activity and call it in some way from the fragment to use the insert?
Should I initialize the viewmodel twice in both fragments?
My code looks like this:
Let's assume i initialize the viewholder in my Activity:
class MainActivity : AppCompatActivity() {
private val articoliViewModel: ArticoliViewModel by viewModels {
ArticoliViewModelFactory((application as ArticoliApplication).repository)
}
}
Then my FirstFragments method where i should add the data to database using the viewModel looks like this:
class FirstFragment : Fragment() {
private val articoliViewModel: ArticoliViewModel by activityViewModels()
private fun addArticolo(barcode: String, qta: Int) { // function which add should add items on click
// here i should be able to do something like this
articoliViewModel.insert(Articolo(barcode, qta))
}
}
And my SecondFragment
class SecondFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private val articoliViewModel: ArticoliViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView = view.findViewById(R.id.recyclerView)
val adapter = ArticoliListAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(activity)
// HERE I SHOULD BE ABLE DO THIS
articoliViewModel.allWords.observe(viewLifecycleOwner) { articolo->
articolo.let { adapter.submitList(it) }
}
}
}
EDIT:
My ViewModel looks like this:
class ArticoliViewModel(private val repository: ArticoliRepository): ViewModel() {
val articoli: LiveData<List<Articolo>> = repository.articoli.asLiveData()
fun insert(articolo: Articolo) = viewModelScope.launch {
repository.insert(articolo)
}
}
class ArticoliViewModelFactory(private val repository: ArticoliRepository): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ArticoliViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return ArticoliViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Whether multiple fragments should share a ViewModel depends on whether they are showing the same data. If they show the same data, I think it usually makes sense to share a ViewModel so the data doesn't have to be pulled from the repository when you switch between them, so the transition is faster. If either of them also has significant amount of unique data, you might consider breaking that out into a separate ViewModel so it doesn't take up memory when it doesn't need to.
Assuming you are using a shared ViewModel, you can do it one of at least two different ways, depending on what code style you prefer. There's kind of a minor trade-off between encapsulation and code duplication, although it's not really encapsulated anyway since they are looking at the same instance. So personally, I prefer the second way of doing it.
Each ViewModel directly creates the ViewModel. If you use by activityViewModels(), then the ViewModel will be scoped to the Activity, so they will both receive the same instance. But since your ViewModel requires a custom factory, you have to specify it in both Fragments, so there is a little bit of code duplication:
// In each Fragment:
private val articoliViewModel: ArticoliViewModel by activityViewModels {
ArticoliViewModelFactory((application as ArticoliApplication).repository)
}
Specify the ViewModel once in the MainActivity and access it in the Fragments by casting the activity.
// In Activity: The same view model code you already showed in your Activity, but not private
// In Fragments:
private val articoliViewModel: ArticoliViewModel
get() = (activity as MainActivity).articoliViewModel
Or to avoid code duplication, you can create an extension property for your Fragments so they don't have to have this code duplication:
val Fragment.articoliViewModel: ArticoliViewModel
get() = (activity as MainActivity).articoliViewModel

Is it possible to use DI with the androidx.navigation.NavController

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>

Set ViewPager's adapter via Data Binding

Trying to use Android's Data Binding to adapter for a ViewPager (controls slidable Fragments).
FooPagerAdapter.kt:
class FooPagerAdapter(fm: Fragmentmanager, private val mFragments: List<BarFragment>) : FragmentStatePagerAdapter(fm) {
override fun getItem(position: int): Fragment {
return mFragments(position)
}
override fun getCount(): Int {
return mFragments.size
}
}
If done from the Activity, it would look like:
..
mFooViewPager.adapter = FooPagerAdapter(fragmentFamanager, fragmentsList)
..
Question:
Now how does one transfer adapter functionality to the binding file to update fragments ViewPager using Data Binding?
Edit:
As I understand it has to be something like this.
activity_foo.xml:
<android.support.v4.view.ViewPager
..
app:fragments"${viewModel.fragments}"/>
And then in a FooViewModel.kt:
fun getFragments(): LiveData<List<BarFragment>>? = mFragments
companion object {
#BindingAdapter("bind:fragments")
fun setAdapter(pager: ViewPager, adapter: BarPagerAdapter) {
pager.adapter = adapter
}
}
Edit2:
Decided to use a ViewModel directly (without binding) to set ViewPager's adapter.
activity_foo.xml:
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.foo.bar.viewmodels.FooViewModel"/>
</data>
..
<android.support.v4.view.ViewPager
..
app:adapter="%{viewModel.adapter}"/>
FooViewModel.kt:
class FooViewModel(application: Application) : AndroidViewModel(application) {
..
fun setAdapter(pager: ViewPager, fragments:List<PeriodFragment>) {
pager.adapter = PeriodsPagerAdapter(mFragmentManager!!, periods)
}
Getting:
Error:...layout\activity_foo.xml:39 attribute 'com.foo.bar:adapter' not found
A #BindingAdapter should be static in Java, thus be annotated with #JvmStatic. Additionally you're supposed to skip all namespaces with the binding attributes in the adapter definition. Also the second parameter needs to reference a type you want to set. In your case this is LiveData<List<BarFragment>>. Then you can create the adapter statically.
companion object {
#JvmStatic
#BindingAdapter("fragments")
fun setAdapter(pager: ViewPager, fragments: LiveData<List<BarFragment>>) {
pager.adapter = createAdapterForFragments(fragments)
}
}
But if fragments would be a PagerAdapter, a binding adapter is not necessary at all. As a default implementation the compiler looks for a given setter method for the attribute. So if you use app:adapter, the setAdapter() method will be used automatically. Therefore it should be sufficient to just put this adapter definition in the layout.
<android.support.v4.view.ViewPager
...
app:adapter"#{viewModel.fragments}"/>
I'd suggest to use the latter and setup the adapter with the viewModel not with the data binding.
A little data binding convenience for ViewPager and TabLayout you'll find with ViewPagerDataBinding.

Categories

Resources