Android 使用 MVVM 模式时从一个片段导航到另一个片段
Navigate from one fragment to another when using MVVM pattern for Android
我正在使用 MVVM 创建一个应用程序 pattern.I 我正在使用 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
内部导航意味着您需要一个违反 MVVM 概念的视图实例。相反,使用 LiveData
向您的片段指示它需要导航到下一个目的地。您可以使用以下 Event
class(来自 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
})
更新答案(感谢 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)
})
}
}
我正在使用 MVVM 创建一个应用程序 pattern.I 我正在使用 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
内部导航意味着您需要一个违反 MVVM 概念的视图实例。相反,使用 LiveData
向您的片段指示它需要导航到下一个目的地。您可以使用以下 Event
class(来自 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
})
更新答案(感谢 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)
})
}
}