我正在尝试使用ViewPager2 + FragmentStateAdapter +导航组件构建以下视图结构/导航。
先决条件:具有一个导航图的单一活动架构
1。片段A 包含一个视图寻呼机。查看分页器使用FragmentStateAdapter。
2。片段B 通过FragmentStateAdapter实例化(视图分页器中的“活动”)。
3。片段C -应该从片段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
<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)
与方法1相同的结果。
这实际上有效。另外,导航返回也可以正常工作。
这里的问题是:
如果有人知道解决这个问题的优雅方法,我将很高兴收到任何提示。
谢谢
答案 0 :(得分:0)
我不知道您是否找到解决问题的方法,但这就是我在应用程序中所做的事情(这与您对解决方案4的描述类似)
我有一个具有主片段(HomeFragment)的应用程序,该片段包含一个底部的标签栏,其中显示了四个不同的片段。
在使用该应用程序的任何时候,用户都可以登录/注销(其中包含4个片段的流程)回答问卷(2个片段),显示设置(1个片段)或拍照(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,后者又将消息发送到HomeFragment,它将调用类似的内容
findNavController().navigate(HomeFragmentDirections.logIn())
因此您可以看到Fragment1 / 2/3/4在整个导航过程之外,而HomeFragment包含将要执行的所有操作
如果有人有任何疑问或需要澄清的地方,我希望我能正确解释
答案 1 :(得分:0)
您可以在 ViewModel 中定义一个 Channel
来处理 UI 事件。
让我们看看如何实现这一目标:
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()
}
}
activityViewModels
的初始 viewModel 并侦听包含 Channel
视图的 Fragment 中的 viewPager2
:@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)
)
}
}
}
}
}
Fragment
绑定到 viewPager
初始 viewModel 并调用 activityViewModels
函数的 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)
}
}
}