19

I am trying to build the following view structure/ navigation using ViewPager2 + FragmentStateAdapter + navigation component.

Preconditions: Single activity architecture, with one navigation graph

1. Fragment A holds a view pager. View pager uses FragmentStateAdapter.

2. Fragment B is instantiated via FragmentStateAdapter ("lives" in view pager).

3. Fragment C - should be navigated to from Fragment B. --> This is where the problem is.


Approach 1 : ViewPager2 + FragmentStateAdapter + navigation declared from Fragment B

    <fragment
        android:id="@+id/fragmentA"
        android:name="com.abc.FragmentA"
        android:label="FragmentA" />

    <fragment
        android:id="@+id/fragmentB"
        android:name="com.abc.FragmentB"
        android:label="FragmentB">
        <action
            android:id="@+id/to_fragmentC"
            app:destination="@id/fragmentC" />
    </fragment>

    <fragment
        android:id="@+id/fragmentC"
        android:name="com.abc.FragmentC"
        android:label="FragmentC" />

FragmentB executes:

 FragmentBDirections
            .toFragmentC()
            .let { findNavController().navigate(it) }

Result :

App crash
java.lang.IllegalArgumentException: navigation destination com.abc:id/to_fragmentC is unknown to this NavController

Approach 2 : ViewPager2 + FragmentStateAdapter + navigation declared from Fragment A

    <fragment
        android:id="@+id/fragmentA"
        android:name="com.abc.FragmentA"
        android:label="FragmentA" >
        <action
            android:id="@+id/to_fragmentC"
            app:destination="@id/fragmentC" />
    </fragment>

    <fragment
        android:id="@+id/fragmentB"
        android:name="com.abc.FragmentB"
        android:label="FragmentB">
    </fragment>

    <fragment
        android:id="@+id/fragmentC"
        android:name="com.abc.FragmentC"
        android:label="FragmentC" />

FragmentB executes:

 FragmentADirections
            .toFragmentC()
            .let { findNavController().navigate(it) }

Result:

App navigates to FragmentC, but when i hit the back button , it crashes with :
java.lang.IllegalArgumentException
        at androidx.core.util.Preconditions.checkArgument(Preconditions.java:36)
        at androidx.viewpager2.adapter.FragmentStateAdapter.onAttachedToRecyclerView(FragmentStateAdapter.java:132)
        at androidx.recyclerview.widget.RecyclerView.setAdapterInternal(RecyclerView.java:1209)
        at androidx.recyclerview.widget.RecyclerView.setAdapter(RecyclerView.java:1161)
        at androidx.viewpager2.widget.ViewPager2.setAdapter(ViewPager2.java:461)
        at com.abc.FragmentA.viewCreated(FragmentA.kt:69)


Approach 3 : ViewPager + FragmentStatePagerAdapter (deprecated) + navigation declared from Fragment B

The same result as approach 1.


Approach 4 : ViewPager + FragmentStatePagerAdapter (deprecated) + navigation declared from Fragment A

This one actually works. Also, the navigation back works fine.

The problem here is that:

  • Navigation has to be defined for every parent fragment of FragmentB -> not so scaleable
  • Usage of the adapter that is deprecated

If anybody knows some elegant solution to this problem, I would be very glad for any hints.

Thank you

Matin Petrulak
  • 1,087
  • 15
  • 23
  • I have the same problem where i have NavGraph -> Fragment A which has ( View pager ) i have to navigate to fragment B from View pager ( Fragment 1 | Fragment 2 ) do post the solution if you get any i would do the same. – Malik Saifullah May 06 '20 at 21:55
  • Approach 2 is working perfectly in my case. The issue might be with viewpager not with navigation – Nabeel May 11 '20 at 09:37
  • How you initialized FragmentStateAdapter? Are you getting FragmentStateAdapter instance from Dagger? The problem exists when you initialize with dagger – Sai's Stack May 20 '20 at 18:31
  • @nabeel I have the same crash log with approach 2. Any idea how to fix it? – Karthik Jul 12 '20 at 06:27
  • @Karthik please post code – Nabeel Jul 12 '20 at 07:35
  • @nabeel NVM, I fixed it! Lazy initialisation of adapter caused the issue. Not sure how lazy initialisation was the culprit. – Karthik Jul 12 '20 at 13:20
  • it was also my case, had to remove the lazy{} instantiation of the adapter. So: `val adapter by lazy { MyAdapter() }` to `var adapter: MyAdapter? = null`. And instantiate it in onCreate or onViewCreated, i dont remember the method name by hearth :) – Matin Petrulak Jul 15 '20 at 15:09
  • My fragment extends a base fragment. i don't have OnCreateView in the fragment. – user1106888 May 04 '21 at 23:35

3 Answers3

2

You don't need to declare Fragment B as a destination in your graph, because you never use NavController to navigate to it. Instead of using an action, you can just use a destination id to implement navigation in Fragment B, like findNavController().navigate(R.id.fragmentC). The findNavController method will find the parent fragment's navController to execute the navigation.

Cimm
  • 4,653
  • 8
  • 40
  • 66
suncunxing
  • 21
  • 3
  • This is great, but how can it done while passing the arguments? – Siele Kim Jun 04 '22 at 11:05
  • 1
    Passing the arguments by Bundle. For example: `Bundle bundle = new Bundle(); bundle.putString("key", "value"); findNavController().navigate(R.id.fragmentC, bundle);` – suncunxing Jun 07 '22 at 08:51
0

General idea

You can define a Channel in your ViewModel to handle the UI events.

Let's see how we can achieve this:

  1. define your viewModel:
@HiltViewModel
class OrderViewModel @Inject constructor(
    private val repository: Repository
) : ViewModel() {
    
    private val orderEventChannel = Channel<OrderEvent>()
    val orderEvent = orderEventChannel.receiveAsFlow()

    ...

    fun onInvoiceClicked(invoice: Invoice) = viewModelScope.launch {
        orderEventChannel.send(OrderEvent.NavigateToOrderDetailsFragment(invoice))
    }

    sealed class OrderEvent{
        data class NavigateToOrderDetailsFragment(val invoice: Invoice) : OrderEvent()
    }
}
  1. Initial viewModel by activityViewModels and listen to the Channel in Fragment that contains viewPager2 view:
@AndroidEntryPoint
class OrdersFragment : Fragment(R.layout.fragment_orders) {
    private val viewModel: OrderViewModel by activityViewModels()
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        ...
        
        viewLifecycleOwner.lifecycleScope.launchWhenStarted {
            viewModel.orderEvent.collect { event ->
                when(event){
                    is OrderViewModel.OrderEvent.NavigateToOrderDetailsFragment -> {
                        findNavController().navigate(
                            OrdersFragmentDirections.actionOrdersFragmentToOrderDetailsFragment(event.invoice)
                        )
                    }
                }
            }
        }
    }
  1. Now in all Fragment that bind in viewPager initial viewModel by activityViewModels and call onInvoiceClicked() function:
class InProgressViewPagerFragment : Fragment(R.layout.fragment_in_progress_view_pager){
    private val viewModel: OrderViewModel by activityViewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        ...
        binding.invoice.setOnClickListener{
           viewModel.onInvoiceClicked(invoice)
        }
    }
}
Kaaveh Mohamedi
  • 1,399
  • 1
  • 15
  • 36
-1

I don't know if you found a solution to your problem but this is how I did it in my app (this is similar to what you describe for solution 4)

I have an application that has a main fragment (HomeFragment) which contains a bottom tab bar which shows four different fragments.

At any point while using the application the user can log in/log out (this is a flow that contains 4 fragments in it) answer a questionnaire (2 fragments) show settings (one fragment) or take a picture (1 fragment)

I have declared my home view model like so:

class HomeViewModel(app:Application):AndroidViewModel(app), HomeParent {
}


intefrace HomeParent { //this contains all the actions that can be executed, like showLogin, showQuestions etc etc
    fun ShowLogin()
 
}

Inside the HomeFragment I initialize the pager like so:

class HomeFragment: Fragment(), KoinComponent {
    private val vm by sharedViewModel<HomeViewModel>()

    private val pager by lazy { binding.pager }  //I just use view bindings you can do it with findviewbyid
    private val adapter by lazy { HomePageAdapter(this) }

    
    override fun initView(state:Bundle?) { //this is run before oncreate finishes
         pager.setPageTransformer(this::pageAnimation) //this is just to animate transitions
    }

    override fun setUpListeners() {  //this is run after oncreate finishes
        bottomNav.setOnNavigationItemSelectedListener(this::navigationItemSelected) //this does stuff and sends it to vm
        pager.registerOnPageChangeCallback(pagerChangeCallback)
    }

    override fun onResume() {
        super.onResume()
        pager.adapter = adapter
        (vm.state.value as? HomeState.Default)?.let { //I'm using mvi, this basically holds the current position in the pager
            pager.setCurrentItem(it.position.position, false)
        }
    }

    override fun onPause() {
        super.onPause()
        pager.adapter = null //I don't remember why I did this, I gues
    }

    override fun onDestroy() {
        super.onDestroy()
        pager.unregisterOnPageChangeCallback(pagerChangeCallback)
    }

}

the actual adapter looks like this:

class HomePageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
    override fun getItemCount() = 4

    override fun createFragment(position: Int): Fragment {
        return when (position) {
            0 -> Fragment1()
            1 -> Fragment2()
            2 -> Fragment3()
            3 -> Fragment4()
            else -> throw Throwable("Invalid position $position")
        }
    }
}

each of those fragments is created like so:

class Fragment1 :Fragment(),
    private val home : HomeParent by sharedViewModel<HomeViewModel>()
    override val vm: Fragment1ViewModel by viewModel { parametersOf(home) }

as you can see I pass the home view model as a parameter to each of the tabs inside home

then each fragment can do something like

home.ShowLogin()

this will send the message to HomeViewModel which will in turn send the message to HomeFragment and it will call something like

findNavController().navigate(HomeFragmentDirections.logIn())

so as you can see Fragment1/2/3/4 are outside of the whole navigation process, and HomeFragment contains all the actions that will be performed

I hope I explained it correctly, if anyone has any questions or need clarifications let me know

Cruces
  • 3,029
  • 1
  • 26
  • 55