使用ViewPager2和嵌套导航组件的正确方式

19
我正在尝试使用ViewPager2 + FragmentStateAdapter + 导航组件来构建以下视图结构/导航。
前提条件:单Activity架构,一个导航图。
1. Fragment A 拥有一个ViewPager。ViewPager使用FragmentStateAdapter.
2. 通过FragmentStateAdapter实例化 Fragment B(“存在”于ViewPager 中)。
3. 应该从Fragment B导航到Fragment C。--> 这是问题所在。
方法1:ViewPager2+FragmentStateAdapter+从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 执行:

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

结果:

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

第二种方法:使用ViewPager2 + FragmentStateAdapter + 在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 执行:

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

结果:

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)

第三种方案:ViewPager + FragmentStatePagerAdapter(已弃用)+ 从Fragment B声明导航

与方案一相同的结果。


第四种方案:ViewPager + FragmentStatePagerAdapter(已弃用)+ 从Fragment A声明导航

这个方案实际上是可行的。此外,后退导航也可以正常工作。

问题在于:

  • 必须为FragmentB的每个父片段定义导航->不太可扩展
  • 使用了已经弃用的适配器

如果有人知道解决这个问题的优雅方法,我会非常感激任何提示。

谢谢


我有同样的问题,其中我有NavGraph-> Fragment A,它具有(View pager)我必须从View pager(Fragment 1 | Fragment 2)导航到fragment B,如果您得到任何解决方案,请发布,我也会这样做。 - Malik Saifullah
方法二在我的情况下完美地运作。问题可能出在ViewPager上,而不是导航上。 - Nabeel
你是如何初始化 FragmentStateAdapter 的?你是否从 Dagger 中获取了 FragmentStateAdapter 实例?当你使用 dagger 进行初始化时,问题就出现了。 - Sai's Stack
@nabeel 我也有使用方法2时出现的相同崩溃日志。你有什么想法如何解决它吗? - Karthik
我的片段扩展了一个基础片段。该片段中没有OnCreateView方法。 - user1106888
显示剩余3条评论
3个回答

2

您不需要在导航图中将 Fragment B 声明为目的地,因为您从未使用 NavController 导航到它。您可以使用 目标 ID 来实现 Fragment B 中的导航,例如 findNavController().navigate(R.id.fragmentC)findNavController 方法将找到父片段的 NavController 来执行导航。


这很不错,但是在传递参数的同时该怎么做呢? - Siele Kim
1
通过 Bundle 传递参数。 例如: Bundle bundle = new Bundle(); bundle.putString("key", "value"); findNavController().navigate(R.id.fragmentC, bundle); - suncunxing

0

概述

您可以在ViewModel中定义一个Channel来处理UI事件。

让我们看看如何实现:

  1. 定义您的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. 通过activityViewModels初始化viewModel,并在包含viewPager2视图的Fragment中监听Channel
@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)
                        )
                    }
                }
            }
        }
    }

现在在所有绑定在viewPager中的Fragment中,通过activityViewModels初始化viewModel并调用onInvoiceClicked()函数:
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)
        }
    }
}

-1

我不知道你是否已经找到了解决方案,但这是我在我的应用程序中所做的(这类似于你描述的解决方案4)

我有一个应用程序,它有一个主要的片段(HomeFragment),其中包含一个底部选项卡栏,显示四个不同的片段。

在使用应用程序的任何时候,用户都可以登录/注销(这是一个包含4个片段的流程),回答问卷调查(2个片段),显示设置(一个片段)或拍照(1个片段)

我已经声明了我的主页视图模型:

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

在HomeFragment中,我这样初始化分页器:

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

}

实际的适配器看起来像这样:

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

每个片段都是这样创建的:

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

正如您所看到的,我将主页视图模型作为参数传递给主页内的每个选项卡

然后每个片段可以执行类似以下操作

home.ShowLogin()

这将把消息发送到HomeViewModel,然后HomeViewModel会将消息发送到HomeFragment,它将调用类似于以下内容的东西

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

正如您所看到的,Fragment1/2/3/4在整个导航过程之外,而HomeFragment包含将执行的所有操作

我希望我已经正确地解释了,如果有任何问题或需要澄清,请让我知道


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接