MVVM架构下Navigation切换Fragment后界面无显示变化问题
问题描述
使用Kotlin结合MVVM架构开发应用时,Fragment切换功能出现异常:点击按钮首次无任何反应,再次点击直接崩溃,报错信息如下:
Navigation action/destination MAIN_to_RESULT cannot be found from the current destination RESULT
该错误表明Fragment已实际从MAIN切换至RESULT,但界面仍显示MAIN片段。
相关代码
MainFragment
class MainFragment : Fragment() { private lateinit var viewModel: MainViewModel lateinit var binding: FragmentMainBinding companion object { fun newInstance() = MainFragment() } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel = (activity as MainActivity).viewModel binding.setVariable(myViewModel, viewModel) viewModel.bnd = binding binding.button2.setOnClickListener { viewModel.chng() } binding.submitButton.setOnClickListener { val controller = NavHostFragment.findNavController(this) controller.navigate(R.id.action_mainFragment_to_resultFragment) } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false) binding.setLifecycleOwner(this) return binding.root } }
ResultFragment
class ResultFragment : Fragment() { private lateinit var viewModel: MainViewModel lateinit var binding: FragmentMainBinding companion object { fun newInstance() = ResultFragment() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false) binding.setLifecycleOwner(this) return binding.root } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel = (activity as MainActivity).viewModel binding.setVariable(BR.myViewModel, viewModel) binding.button2.setOnClickListener { viewModel.chng() } viewModel.bnd = binding } }
MainViewModel
class MainViewModel(application: Application) : AndroidViewModel(application) { val rps = Repository() lateinit var bnd : FragmentMainBinding var changing = false var SPEED_OF_CHANGING = 100L var actualCountedCards = 0 var numberOfCardsToRotate = 0 var numberOfRotatedCards = 0 lateinit var actualCard : Card var mainHandler: Handler = Handler(Looper.getMainLooper()) private val changeCard = object : Runnable { override fun run() { if (changing) { actualCard = rps.Cards.random() bnd.cardView.setImageResource(actualCard.image) actualCountedCards += actualCard.power numberOfRotatedCards += 1 Log.d("tagicek", "${numberOfRotatedCards}/${numberOfCardsToRotate}, ${actualCountedCards}") if (numberOfRotatedCards >= numberOfCardsToRotate) { chng() bnd.editTextTextPersonName.visibility = View.VISIBLE bnd.submitButton.visibility = View.VISIBLE bnd.button2.visibility = View.INVISIBLE } mainHandler.postDelayed(this, SPEED_OF_CHANGING) } } } fun chng() { if (changing == true) { mainHandler.removeCallbacks(changeCard) actualCountedCards = 0 numberOfRotatedCards = 0 bnd.cardView.setImageResource(R.drawable.bg) changing = false } else { numberOfCardsToRotate = (1..51).random() mainHandler.post(changeCard) changing = true } } }
MainActivity
class MainActivity : AppCompatActivity() { lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this)[MainViewModel::class.java] if (savedInstanceState == null) { supportFragmentManager.beginTransaction() .replace(R.id.container, MainFragment.newInstance()) .commitNow() } } }
问题根源
- Navigation组件未正确配置:MainActivity使用传统
FragmentTransaction手动添加Fragment,而非Navigation组件的NavHostFragment托管导航,导致Navigation栈状态与实际界面显示不一致。 - ResultFragment布局绑定错误:ResultFragment错误绑定了MainFragment的
FragmentMainBinding和fragment_main布局,切换后界面视觉上无变化,但Navigation栈已更新,再次点击导航会因当前目的地为RESULT而找不到MAIN到RESULT的action。 - ViewModel持有View层引用:ViewModel直接持有
FragmentMainBinding实例,违反MVVM分层原则,不仅导致内存泄漏,还会在Fragment切换后覆盖绑定实例,混淆视图状态。
修复方案
1. 正确配置Navigation组件
- 修改
activity_main.xml,添加NavHostFragment替换原容器:
<androidx.fragment.app.FragmentContainerView android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:navGraph="@navigation/nav_graph" app:defaultNavHost="true" />
- 创建
res/navigation/nav_graph.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" android:id="@+id/nav_graph" app:startDestination="@id/mainFragment"> <fragment android:id="@+id/mainFragment" android:name="com.yourpackage.MainFragment" android:label="MainFragment"> <action android:id="@+id/action_mainFragment_to_resultFragment" app:destination="@id/resultFragment" /> </fragment> <fragment android:id="@+id/resultFragment" android:name="com.yourpackage.ResultFragment" android:label="ResultFragment" /> </navigation>
- 简化MainActivity,移除手动Fragment事务:
class MainActivity : AppCompatActivity() { lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this)[MainViewModel::class.java] } }
2. 修复ResultFragment的布局绑定
ResultFragment需使用自身布局(如fragment_result.xml)和对应Binding类:
class ResultFragment : Fragment() { private lateinit var viewModel: MainViewModel lateinit var binding: FragmentResultBinding companion object { fun newInstance() = ResultFragment() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = DataBindingUtil.inflate(inflater, R.layout.fragment_result, container, false) binding.setLifecycleOwner(this) return binding.root } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel = (activity as MainActivity).viewModel binding.setVariable(BR.myViewModel, viewModel) // 示例:添加返回按钮导航回MainFragment binding.backButton?.setOnClickListener { NavHostFragment.findNavController(this).navigateUp() } } }
3. 移除ViewModel中的Binding引用,改用LiveData通知UI更新
class MainViewModel(application: Application) : AndroidViewModel(application) { val rps = Repository() var changing = false var SPEED_OF_CHANGING = 100L var actualCountedCards = 0 var numberOfCardsToRotate = 0 var numberOfRotatedCards = 0 val actualCard = MutableLiveData<Card>() val showSubmitControls = MutableLiveData<Boolean>() var mainHandler: Handler = Handler(Looper.getMainLooper()) private val changeCard = object : Runnable { override fun run() { if (changing) { val card = rps.Cards.random() actualCard.postValue(card) actualCountedCards += card.power numberOfRotatedCards += 1 Log.d("tagicek", "${numberOfRotatedCards}/${numberOfCardsToRotate}, ${actualCountedCards}") if (numberOfRotatedCards >= numberOfCardsToRotate) { chng() showSubmitControls.postValue(true) } mainHandler.postDelayed(this, SPEED_OF_CHANGING) } } } fun chng() { if (changing) { mainHandler.removeCallbacks(changeCard) actualCountedCards = 0 numberOfRotatedCards = 0 actualCard.postValue(null) changing = false showSubmitControls.postValue(false) } else { numberOfCardsToRotate = (1..51).random() mainHandler.post(changeCard) changing = true showSubmitControls.postValue(false) } } }
4. 更新MainFragment,通过LiveData响应ViewModel变化
class MainFragment : Fragment() { private lateinit var viewModel: MainViewModel lateinit var binding: FragmentMainBinding companion object { fun newInstance() = MainFragment() } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel = (activity as MainActivity).viewModel binding.setVariable(BR.myViewModel, viewModel) viewModel.actualCard.observe(viewLifecycleOwner) { card -> card?.let { binding.cardView.setImageResource(it.image) } ?: run { binding.cardView.setImageResource(R.drawable.bg) } } viewModel.showSubmitControls.observe(viewLifecycleOwner) { show -> binding.editTextTextPersonName.visibility = if (show) View.VISIBLE else View.GONE binding.submitButton.visibility = if (show) View.VISIBLE else View.GONE binding.button2.visibility = if (show) View.INVISIBLE else View.VISIBLE } binding.button2.setOnClickListener { viewModel.chng() } binding.submitButton.setOnClickListener { NavHostFragment.findNavController(this).navigate(R.id.action_mainFragment_to_resultFragment) } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false) binding.setLifecycleOwner(this) return binding.root } override fun onDestroyView() { super.onDestroyView() viewModel.mainHandler.removeCallbacks(viewModel.changeCard) } }
内容的提问来源于stack exchange,提问作者Jacob
相关产品推荐
相关产品推荐

