包的划分
在包目录下应包含以下package
- di(依赖注入相关的,XXXModule、XXXQualifier应该在此package下)
- contract(现在startActivityForResult方法已经被弃用,如果需要启动activity获取结果,请写一个contacts类集成ActivityResultContract)
- domain(UseCase所在的package)
- repository
-
ui(例子如下)
ui例子
app架构
mvvm架构,对应关系是
- 一个Activity/Fragment持有一个或多个ViewModel
- 一个ViewModel持有一个或多个UseCase
- 一个UseCase持有一个或多个Repository
UseCase编写规范
UseCase类代码如下
abstract class UseCase<in P, R>(private val coroutineDispatcher: CoroutineDispatcher) {
/** Executes the use case asynchronously and returns a [Result].
*
* @return a [Result].
*
* @param parameters the input parameters to run the use case with
*/
suspend operator fun invoke(parameters: P): Result<R> {
return try {
// Moving all use case's executions to the injected dispatcher
// In production code, this is usually the Default dispatcher (background thread)
// In tests, this becomes a TestCoroutineDispatcher
withContext(coroutineDispatcher) {
execute(parameters).let {
Result.Success(it)
}
}
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Override this to set the code to be executed.
*/
@Throws(RuntimeException::class)
protected abstract suspend fun execute(parameters: P): R
}
UseCase的作用是
- 切线程(例如把网络请求切换到非UI线程)
- Try-Catch处理(例如处理网络请求异常)
- 业务逻辑处理(业务逻辑放在UseCase里面可以实现复用,放在ViewModel里面不好复用)
ViewModel编写规范
View无法决定自己跳转页面、展示Dialog、显示加载对话框、显示一个SncakBar,View只能告诉ViewModel我的某个按钮被点击了,如
override fun openEventDetail(id: SessionId) {
analyticsHelper.logUiEvent(
"Home to event detail",
AnalyticsActions.HOME_TO_SESSION_DETAIL
)
_navigationActions.tryOffer(FeedNavigationAction.NavigateToSession(id))
}
ViewModel类中应该包含一个NavigationAction密封类,通过这个类,告诉View应该跳转到哪个页面或者弹出一个对话框,如
sealed class FeedNavigationAction {
class NavigateToSession(val sessionId: SessionId) : FeedNavigationAction()
class NavigateAction(val directions: NavDirections) : FeedNavigationAction()
object OpenSignInDialogAction : FeedNavigationAction()
class OpenLiveStreamAction(val url: String) : FeedNavigationAction()
object NavigateToScheduleAction : FeedNavigationAction()
}
ViewModel和View的交互方式
不同于MVP,在MVP中,View和Presenter相互持有,P层可以直接调用V层的方法。但是在MVVM中,View可以持有ViewModel,但ViewModel不能持有View,ViewModel需要把一个可观察的数据源给View去观察,在数据变化的时候View去更新数据。可观察的数据源可分为3大类
RxJava
天生的观察者模式,但存在两个问题
- 如何在View不可见的时候不再发送数据给View,避免在View不可见的时候毫无意义的绘制
- 如何在View可见的时候把最新的数据发送给View,让View展示最新的数据
LiveData
LiveData不同于RxJava,RxJava是一个数据流,你可以进行Map,Filter等数据流操作,但LiveData是一个DataHolder,帮你保存一个数据,当页面不可见的时候不会给View发送数据,当页面可见的时候把最新的数据给View。
但LiveData存在一个非常大的问题。比如我要让View去跳转到一个新的Activity要怎么做,如果用LiveData去发送这个消息的话,View收到消息后跳转页面,看起来没什么问题,但是当重新回到页面的时候,由于LiveData的机制,当页面可见的时候会发送最新的数据给View,然后我们又跳转了一次页面
Flow
kotlin才有的数据流处理方式,适合Android的MVVM项目。注意我们要暴露两种不同类型的Flow让View去观察
- 展示型。比如显示用户名,私有化一个MutableStateFlow,以便我们可以更新数据,暴露StateFlow出来让View去观察。写法如下
private val _mapVariant = MutableStateFlow<MapVariant?>(null)
val mapVariant: StateFlow<MapVariant?> = _mapVariant
- 消费型。比如跳转一个新的页面。这种写法样式很固定,只需要把泛型类型换成别的就可以
private val _navigationActions = Channel<EventInfoNavigationAction>(Channel.CONFLATED)
val navigationActions = _navigationActions.receiveAsFlow()
NavigationAction的写法
- 类名是XXXNavigationAction
- 如果需要带参数就是Data Class,不需要带参数就是object
- 跳转新的Activity以NavigateTo开头
- 打开Dialog以Show开头
sealed class FeedNavigationAction {
data class NavigateToSession(val sessionId: SessionId) : FeedNavigationAction()
object ShowSignInDialog : FeedNavigationAction()
object NavigateToSchedule : FeedNavigationAction()
}
ViewModel中获取view中的数据方式(Activity中的Intent或Fragment中的Argument)
@HiltViewModel
class AdvertisingViewModel @Inject constructor(
savedStateHandle: SavedStateHandle
) : ViewModel() {
val advertising: Advertising =
savedStateHandle.get<Advertising>(ExtraKey.ADVERTISING) !!
注意事项
- ViewModel暴露给View用的方法不允许有返回值
View编写规范
-
ui包下的view包应该包含一个View类(Activity/Fragment)和一个ViewModel类
image.png - view类只能从ViewModel获取数据,禁止从ViewModel之外的地方获取数据
- view不能决定自己跳转到哪里
网友评论