MVVM android 中的路由(最佳实践)
Routing in MVVM android (Best practice)
我使用 MVVM 模式。我想知道其他程序员如何在屏幕之间进行路由。
可以这样做:
class MyViewModel : ViewModel() {
val routeState = MutableLiveData<String>()
init {
//more fun
//...
//...
routeState.value = "Home"
}
}
class MyActivity : Activity() {
private lateinit var viewModel: MyViewModel
onCreate() {
//viewModel init
viewModel.routeState.observe(viewLifecycleOwner, Observer {
when(it) {
"Home" -> {
toHome()
//finish()
}
}
})
}
}
我知道这种方法不好。那我想问一下你是怎么做到的?
路由不是视图模型的责任,它是 android 中 Intent 的责任,通常开发者会制作一个路由器 class,它是意图在屏幕之间进行导航的包装器,
您可以在 ViewModel 中存储一个逻辑,它决定屏幕是否应该导航到不同的位置。
示例:splashScreenViewModel 可以具有 isAuthenticated 标志的逻辑,当 true 将屏幕路由到主页时,否则转到登录屏幕。
因此,根据您的原因,它不必要地从 viewmodel 跳转到 activity,然后迁移到不同的屏幕,而且它容易出错,因为每当 routeState.value
更改导航到主页时都会触发。流量不理想
自从 Jetpack 以来,我真的更喜欢通过导航组件进行导航:
https://developer.android.com/guide/navigation/navigation-getting-started
但是,例如,如果我单击登录按钮,等待登录响应,成功后我想导航到主屏幕,我会得到类似这样的东西(示例中使用的 ViewModel + Coroutines):
class LoginViewModel(
private val repository: Repository
): ViewModel() {
val liveData = MutableLiveData<LoginPayload>()
fun login(username: String, password: String) = viewModelScope.launch {
liveData.postValue(LoginPayload.StartLoginAction)
try {
val response = repository.login(username, password)
if(response is Success) {
liveData.postValue(LoginPayload.LoginSuccess)
} else {
liveData.postValue(LoginPayload.LoginError)
}
} catch(e: Exception) {
liveData.postValue(LoginPayload.LoginError)
}
}
}
sealed class LoginPayload {
object StartLoginAction: LoginPayload()
object LoginSuccess: LoginPayload()
object LoginError: LoginPayload()
}
class MyActivity : Activity() {
private lateinit var viewModel: LoginViewModel
onCreate() {
//viewModel init
viewModel.liveData.observe(viewLifecycleOwner, Observer {
when(it) {
LoginPayload.StartLoginAction -> //show progress bar, hide login button
LoginPayload.LoginError -> //hide progress bar, show error dialog, show login button
LoginPayload.LoginSuccess -> {
hideProgressBar()
findNavController.navigate(R.id.action_login_to_home)
}
}
})
}
}
有很多方法可以做到这一点。
一种方法是使用相同的 Observer
或 MutableLiveData
(您这样做)
另一种方法是使用接口:
基础视图模型:
abstract class BaseViewModel<N> : ViewModel() {
private lateinit var mNavigator: WeakReference<N>
fun getNavigator(): N {
return mNavigator.get()!!
}
fun setNavigator(navigator: N) {
this.mNavigator = WeakReference(navigator)
}
}
视图模型
class MyViewModel : BaseViewModel<MyInterFace>() {
val routeState = MutableLiveData<String>()
init {
// Wherever you need, you can call your functions :
getNavigator().test()
}
}
我的界面:
interface MyInterFace{
fun test()
}
Activity :
class MyActivity : Activity(),MyInterFace {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// init viewModel
// .
// .
// .
// then set navigator
viewmodel.setNavigator(this)
}
override fun test(){
// do somthing....
}
如果您希望路由明确,则需要执行以下几个步骤:
1.) 永远不要在任务堆栈上同时有 2 个 Activity,应用程序更喜欢有 1 个 Activity,并在 Activity.
内部管理路由
2.) 您需要考虑应用程序何时进入后台、被 Android 杀死并恢复。开箱即用的片段是根据它们当前的“添加”状态重新创建的,但是你的 private val currentRoute: MutableLiveData<String>
会在进程死亡时丢失,除非它是从 savedStateHandle.getLiveData("route")
.
初始化的
3.) 你需要考虑到屏幕有参数,这些参数有时可能比单纯的字符串更复杂,除非你开始将这些“路由”序列化为 JSON 对象,或者代替你使用的字符串是 Parcelable class.
4.) 你需要考虑到在 onStop
之后开始导航通常是无效的,所以你想要忽略命令(就像 Jetpack Navigation 所做的那样),或者将它们排入队列直到 onResume 之后。
5.) 您需要考虑导航可以是异步的(不是即时的),尽管当您使用 Fragments 时通常不需要担心这一点。例如,Jetpack Navigation 并不关心这个(DynamicNavHostFragment
除外)。
6.) 您需要考虑如果导航是异步的,那么您可以在导航进行时获取导航操作。您可能想要防止这种情况发生,或者将它们排入队列。或者在前进时将它们排入队列,但在返回后忽略它们(以消除某些无效状态)。
如果你把这 6 个都考虑进去,你最终会得到我写的库:https://github.com/Zhuinden/simple-stack
现在您的导航就像
一样简单
class MyViewModel(private val backstack: Backstack) : CustomViewModel() {
fun toOtherScreen() {
backstack.goTo(OtherScreen())
}
}
class MyActivity: AppCompatActivity(), SimpleStateChanger.NavigationHandler {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.my_activity)
// ...
Navigator.configure()
.setStateChanger(SimpleStateChanger(this))
.install(this, container, History.of(HomeScreen())
}
override fun onNavigationEvent(stateChange: StateChange) {
val screen = stateChange.topNewKey<Any>()
when {
screen is HomeScreen -> {
...
}
screen is OtherScreen -> {
...
}
}
}
}
@Parcelize object HomeScreen: Parcelable // i prefer data classes for a stable `toString()`
@Parcelize object OtherScreen: Parcelable // i prefer data classes for a stable `toString()`
差不多就这些了,除了您可能想使用 Fragment 之外,请参阅自述文件以了解如何使用 DefaultFragmentStateChanger
而不是内置的东西。
好吧,假设您出于某种原因没有购买我的图书馆。那么现在的人一般都是用Jetpack Navigation
在这种情况下,您需要将 Activity 范围的 ViewModel 传递到您的 ViewModel,其中 Activity 范围的 ViewModel 包含 LiveData<Event<NavigationCommand>>
(假设您没有t 购买我的其他库 EventEmitter 并改用事件包装器),Activity 观察到它以处理从 ViewModel 触发的导航,但导航状态仍由 Jetpack 的 NavController 管理导航。
class MyViewModel(private val navigationDispatcher: NavigationDispatcher) : ViewModel() {
fun toOtherScreen() {
navigationDispatcher.emit { navController ->
navController.navigate(HomeDirections.toOtherScreen())
}
}
}
class MyActivity : Activity() {
private val navigationDispatcher by viewModels<NavigationDispatcher>()
private val viewModel by viewModels {
object: ViewModelProvider.Factory {
override fun <T: ViewModel?> create(clazz: Class<T>): T = MyViewModel(navigationDispatcher)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.my_activity)
navigationDispatcher.navigationCommands.observeEvent(this) { command ->
command.invoke(Navigation.findNavController(this, R.id.nav_host))
}
}
}
typealias NavigationCommand = (NavController) -> Unit
class NavigationDispatcher: ViewModel() {
private val navigationEmitter: MutableLiveData<Event<NavigationCommand>> = MutableLiveData()
val navigationCommands: LiveData<Event<NavigationCommand>> = navigationEmitter
fun emit(navigationCommand: NavigationCommand) {
navigationEmitter.value = Event(navigationCommand)
}
}
class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
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)
}
}
}
inline fun <T> LiveData<Event<T>>.observeEvent(
lifecycleOwner: LifecycleOwner,
crossinline observer: (T) -> Unit
): Observer<Event<T>> = EventObserver<T> { t -> observer(t) }.also {
this.observe(lifecycleOwner, it)
}
说得对。路由不是视图模型的责任。这就是框架的作用。事实上,我的处理方式是将导航作为由框架实现的服务(android)。它是可以将此委托给导航器服务的 ViewModel,这就是导航的完成方式。任何其他方法都希望听到。
我使用 MVVM 模式。我想知道其他程序员如何在屏幕之间进行路由。
可以这样做:
class MyViewModel : ViewModel() {
val routeState = MutableLiveData<String>()
init {
//more fun
//...
//...
routeState.value = "Home"
}
}
class MyActivity : Activity() {
private lateinit var viewModel: MyViewModel
onCreate() {
//viewModel init
viewModel.routeState.observe(viewLifecycleOwner, Observer {
when(it) {
"Home" -> {
toHome()
//finish()
}
}
})
}
}
我知道这种方法不好。那我想问一下你是怎么做到的?
路由不是视图模型的责任,它是 android 中 Intent 的责任,通常开发者会制作一个路由器 class,它是意图在屏幕之间进行导航的包装器,
您可以在 ViewModel 中存储一个逻辑,它决定屏幕是否应该导航到不同的位置。
示例:splashScreenViewModel 可以具有 isAuthenticated 标志的逻辑,当 true 将屏幕路由到主页时,否则转到登录屏幕。
因此,根据您的原因,它不必要地从 viewmodel 跳转到 activity,然后迁移到不同的屏幕,而且它容易出错,因为每当 routeState.value
更改导航到主页时都会触发。流量不理想
自从 Jetpack 以来,我真的更喜欢通过导航组件进行导航:
https://developer.android.com/guide/navigation/navigation-getting-started
但是,例如,如果我单击登录按钮,等待登录响应,成功后我想导航到主屏幕,我会得到类似这样的东西(示例中使用的 ViewModel + Coroutines):
class LoginViewModel(
private val repository: Repository
): ViewModel() {
val liveData = MutableLiveData<LoginPayload>()
fun login(username: String, password: String) = viewModelScope.launch {
liveData.postValue(LoginPayload.StartLoginAction)
try {
val response = repository.login(username, password)
if(response is Success) {
liveData.postValue(LoginPayload.LoginSuccess)
} else {
liveData.postValue(LoginPayload.LoginError)
}
} catch(e: Exception) {
liveData.postValue(LoginPayload.LoginError)
}
}
}
sealed class LoginPayload {
object StartLoginAction: LoginPayload()
object LoginSuccess: LoginPayload()
object LoginError: LoginPayload()
}
class MyActivity : Activity() {
private lateinit var viewModel: LoginViewModel
onCreate() {
//viewModel init
viewModel.liveData.observe(viewLifecycleOwner, Observer {
when(it) {
LoginPayload.StartLoginAction -> //show progress bar, hide login button
LoginPayload.LoginError -> //hide progress bar, show error dialog, show login button
LoginPayload.LoginSuccess -> {
hideProgressBar()
findNavController.navigate(R.id.action_login_to_home)
}
}
})
}
}
有很多方法可以做到这一点。
一种方法是使用相同的 Observer
或 MutableLiveData
(您这样做)
另一种方法是使用接口:
基础视图模型:
abstract class BaseViewModel<N> : ViewModel() {
private lateinit var mNavigator: WeakReference<N>
fun getNavigator(): N {
return mNavigator.get()!!
}
fun setNavigator(navigator: N) {
this.mNavigator = WeakReference(navigator)
}
}
视图模型
class MyViewModel : BaseViewModel<MyInterFace>() {
val routeState = MutableLiveData<String>()
init {
// Wherever you need, you can call your functions :
getNavigator().test()
}
}
我的界面:
interface MyInterFace{
fun test()
}
Activity :
class MyActivity : Activity(),MyInterFace {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// init viewModel
// .
// .
// .
// then set navigator
viewmodel.setNavigator(this)
}
override fun test(){
// do somthing....
}
如果您希望路由明确,则需要执行以下几个步骤:
1.) 永远不要在任务堆栈上同时有 2 个 Activity,应用程序更喜欢有 1 个 Activity,并在 Activity.
内部管理路由2.) 您需要考虑应用程序何时进入后台、被 Android 杀死并恢复。开箱即用的片段是根据它们当前的“添加”状态重新创建的,但是你的 private val currentRoute: MutableLiveData<String>
会在进程死亡时丢失,除非它是从 savedStateHandle.getLiveData("route")
.
3.) 你需要考虑到屏幕有参数,这些参数有时可能比单纯的字符串更复杂,除非你开始将这些“路由”序列化为 JSON 对象,或者代替你使用的字符串是 Parcelable class.
4.) 你需要考虑到在 onStop
之后开始导航通常是无效的,所以你想要忽略命令(就像 Jetpack Navigation 所做的那样),或者将它们排入队列直到 onResume 之后。
5.) 您需要考虑导航可以是异步的(不是即时的),尽管当您使用 Fragments 时通常不需要担心这一点。例如,Jetpack Navigation 并不关心这个(DynamicNavHostFragment
除外)。
6.) 您需要考虑如果导航是异步的,那么您可以在导航进行时获取导航操作。您可能想要防止这种情况发生,或者将它们排入队列。或者在前进时将它们排入队列,但在返回后忽略它们(以消除某些无效状态)。
如果你把这 6 个都考虑进去,你最终会得到我写的库:https://github.com/Zhuinden/simple-stack
现在您的导航就像
一样简单class MyViewModel(private val backstack: Backstack) : CustomViewModel() {
fun toOtherScreen() {
backstack.goTo(OtherScreen())
}
}
class MyActivity: AppCompatActivity(), SimpleStateChanger.NavigationHandler {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.my_activity)
// ...
Navigator.configure()
.setStateChanger(SimpleStateChanger(this))
.install(this, container, History.of(HomeScreen())
}
override fun onNavigationEvent(stateChange: StateChange) {
val screen = stateChange.topNewKey<Any>()
when {
screen is HomeScreen -> {
...
}
screen is OtherScreen -> {
...
}
}
}
}
@Parcelize object HomeScreen: Parcelable // i prefer data classes for a stable `toString()`
@Parcelize object OtherScreen: Parcelable // i prefer data classes for a stable `toString()`
差不多就这些了,除了您可能想使用 Fragment 之外,请参阅自述文件以了解如何使用 DefaultFragmentStateChanger
而不是内置的东西。
好吧,假设您出于某种原因没有购买我的图书馆。那么现在的人一般都是用Jetpack Navigation
在这种情况下,您需要将 Activity 范围的 ViewModel 传递到您的 ViewModel,其中 Activity 范围的 ViewModel 包含 LiveData<Event<NavigationCommand>>
(假设您没有t 购买我的其他库 EventEmitter 并改用事件包装器),Activity 观察到它以处理从 ViewModel 触发的导航,但导航状态仍由 Jetpack 的 NavController 管理导航。
class MyViewModel(private val navigationDispatcher: NavigationDispatcher) : ViewModel() {
fun toOtherScreen() {
navigationDispatcher.emit { navController ->
navController.navigate(HomeDirections.toOtherScreen())
}
}
}
class MyActivity : Activity() {
private val navigationDispatcher by viewModels<NavigationDispatcher>()
private val viewModel by viewModels {
object: ViewModelProvider.Factory {
override fun <T: ViewModel?> create(clazz: Class<T>): T = MyViewModel(navigationDispatcher)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.my_activity)
navigationDispatcher.navigationCommands.observeEvent(this) { command ->
command.invoke(Navigation.findNavController(this, R.id.nav_host))
}
}
}
typealias NavigationCommand = (NavController) -> Unit
class NavigationDispatcher: ViewModel() {
private val navigationEmitter: MutableLiveData<Event<NavigationCommand>> = MutableLiveData()
val navigationCommands: LiveData<Event<NavigationCommand>> = navigationEmitter
fun emit(navigationCommand: NavigationCommand) {
navigationEmitter.value = Event(navigationCommand)
}
}
class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
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)
}
}
}
inline fun <T> LiveData<Event<T>>.observeEvent(
lifecycleOwner: LifecycleOwner,
crossinline observer: (T) -> Unit
): Observer<Event<T>> = EventObserver<T> { t -> observer(t) }.also {
this.observe(lifecycleOwner, it)
}
说得对。路由不是视图模型的责任。这就是框架的作用。事实上,我的处理方式是将导航作为由框架实现的服务(android)。它是可以将此委托给导航器服务的 ViewModel,这就是导航的完成方式。任何其他方法都希望听到。