【问题标题】:Navigate from one fragment to another when using MVVM pattern for Android使用 Android 的 MVVM 模式时从一个片段导航到另一个片段
【发布时间】:2020-03-10 17:11:18
【问题描述】:
  • 我正在使用 MVVM 模式创建一个应用程序。我正在使用 Navigation Graph 来管理我的应用程序中的片段,根据推荐的方法,我们不必将 UI 逻辑放入其中 Activity/Fragments,但在 Viewmodel 中。

  • 所以我的问题是如何从一个片段导航到另一个片段。我知道这可以使用navController.navigate(R.id.action_here) 直接在片段内部完成,但是我将如何处理按钮按下时来自 ViewModel 的导航?

我的代码:

IntroViewModel.kt

class IntroViewModel : ViewModel() {

    fun onBtn1Pressed(view: View) {
        Log.d(IntroViewModel::class.java.simpleName, ": onBtn1Pressed")
    }

    fun onBtn2Pressed(view: View) {
        Log.d(IntroViewModel::class.java.simpleName, ": onBtn2Pressed ")
    }
}

IntroFragment.kt:

class IntroFragment : Fragment() {

    private lateinit var viewModel: IntroViewModel
    private lateinit var navController: NavController
    lateinit var introBinding: IntroFragmentBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        introBinding = DataBindingUtil.inflate(inflater, R.layout.intro_fragment, container, false)
        viewModel = ViewModelProviders.of(this).get(IntroViewModel::class.java)
        introBinding.introModel = viewModel
        return introBinding.root;
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        navController = Navigation.findNavController(view)

    }
}

intro_fragment.xml:

<data>
    <variable
        name="introModel"
        type="example.com.viewmodel.IntroViewModel" />
</data>

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:padding="@dimen/padding_16dp"
    tools:context=".fragments.IntroFragment">

    <TextView
        android:id="@+id/txt_"
        style="@style/TextAppearance.MaterialComponents.Headline5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="Choose one " />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/txt_"
        android:onClick="@{introModel::onBtn1Pressed}"
        android:layout_marginTop="@dimen/margin_8dp"
        android:text="Btn1" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_2"
        style="@style/Widget.MaterialComponents.Button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="@{introModel::onBtn2Pressed}"
        android:layout_below="@id/btn_1"
        android:layout_alignStart="@id/btn_1"
        android:layout_alignEnd="@id/btn_1"
        android:layout_marginTop="@dimen/margin_8dp"
        android:text="Btn2" />

</RelativeLayout>

【问题讨论】:

  • 在 ViewModel 中放置一个 NavController 实例并在 onViewCreated() 中初始化它以便 ViewModel 可以处理导航会违反您的设计选择规则吗?

标签: android kotlin android-jetpack android-mvvm


【解决方案1】:

ViewModel 内部导航意味着您需要一个违反 MVVM 概念的视图实例。相反,使用LiveData 向您的片段指示它需要导航到下一个目的地。您可以使用以下 Event 类(来自 Google 的 architecture-samples 之一)来确保导航只触发一次。

open class Event<out T>(private val content: T) {

    @Suppress("MemberVisibilityCanBePrivate")
    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

Observer 一起使用:

/**
 * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has
 * already been handled.
 *
 * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled.
 */
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
    override fun onChanged(event: Event<T>?) {
        event?.getContentIfNotHandled()?.let {
            onEventUnhandledContent(it)
        }
    }
}

这是你的LiveData

private val _openTaskEvent = MutableLiveData<Event<String>>()
val openTaskEvent: LiveData<Event<String>> = _openTaskEvent

最后你可以这样观察它:

viewModel.openTaskEvent.observe(this, EventObserver {
    //Do your navigation here
})

【讨论】:

    【解决方案2】:

    更新答案(感谢 Mohamed Mohsin):

    IntroViewModel.kt:

    class IntroViewModel : ViewModel() {
    
      private val _navigateScreen = MutableLiveData<Event<Any>>()
      val navigateScreen: LiveData<Event<Any>> = _navigateScreen
    
        fun onBtn1Pressed(view: View) {
           _navigateScreen.value = Event(R.id.action_here)
        }
    
        fun onBtn2Pressed(view: View) {
            _navigateScreen.value = Event(R.id.action_here)
        }
    }
    

    Event.kt:

        open class Event<out T>(private val content: T) {
    
        var hasBeenHandled = false
            private set // Allow external read but not write
    
        /**
         * Returns the content and prevents its use again.
         */
    
        fun getContentIfNotHandled(): T? {
            return if (hasBeenHandled) {
                null
            } else {
                hasBeenHandled = true
                content
            }
        }
    
        /**
         * Returns the content, even if it's already been handled.
         */
        fun peekContent(): T = content
    }
    
    class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
        override fun onChanged(event: Event<T>?) {
            event?.getContentIfNotHandled()?.let {
                onEventUnhandledContent(it)
            }
        }
    }
    

    IntroFragment.kt:

    class IntroFragment : Fragment() {
    
        private lateinit var viewModel: IntroViewModel
        private lateinit var navController: NavController
        private lateinit var introBinding: IntroFragmentBinding
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            introBinding = DataBindingUtil.inflate(inflater, R.layout.intro_fragment, container, false)
            viewModel = ViewModelProviders.of(this).get(IntroViewModel::class.java)
            introBinding.introModel = viewModel
            return introBinding.root
        }
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            navController = Navigation.findNavController(view)
            viewModel.navigateScreen.observe(activity!!, EventObserver {
                navController.navigate(it)
            })
        }
    }
    

    【讨论】:

    • 使用navController.navigate()时需要传递下一个目的地的id。您可以在 nav_graph.xml 中找到您的 ID。例如:navController.navigate(R.id.detailFragment).
    • 那我怎么知道按下了哪个按钮!
    • 好的,在这种情况下,将Event&lt;Any&gt; 更改为Event&lt;Int&gt;,因为您传递的是资源ID。并确保传递的 id(如 R.id.action_here)是 nav_graph 中目的地的实际 id
    • navController.navigate(viewModel.navigateScreen.value) 更改为navController.navigate(it)
    猜你喜欢
    • 2021-05-02
    • 1970-01-01
    • 2013-12-19
    • 1970-01-01
    • 1970-01-01
    • 2013-05-12
    • 2015-01-10
    • 1970-01-01
    • 2020-09-03
    相关资源
    最近更新 更多