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)
                }
            }
        })
    }
}

有很多方法可以做到这一点。

一种方法是使用相同的 ObserverMutableLiveData(您这样做)

另一种方法是使用接口:

基础视图模型:

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,这就是导航的完成方式。任何其他方法都希望听到。