Android应用程序开发可能具有挑战性,特别是在创建可扩展和可维护的代码以适应不断变化的需求和用户需求方面。为了解决这个挑战,开发者通常依赖于软件架构模式,为代码组织和关注点分离提供了坚实的基础。在Android开发中,两种流行的架构模式是Model-View-ViewModel (MVVM)
和Clean
架构。
这两种方法都强调模块化、可测试性和关注点分离,但它们在具体实施细节上有所不同。在本文中,我们将探讨如何结合MVVM和Clean架构来创建健壮且易于测试、扩展和维护的Android应用程序。我们还将介绍使用这种强大组合的最佳实践和实现技巧。
Model-View-ViewModel + Repository Layer
为什么使用MVVM架构?
- 主要目标:将演示逻辑与应用的业务逻辑分离
- 更好的可测试性
- 遵循SOLID原则
- 等等
Model
代表数据提供者
- REST
- Database
- Preferences
View
代表
- UI组件
- 显示数据
Viewmodel
代表显示逻辑
- 准备数据以供呈现
- 使用观察者模式通知变更
怎样在没有使用“干净架构” (Clean Architecture) 的情况下实现它?
每个开发人员都以学习和快速编写代码的愿望开始自己的职业生涯;然而,我们中的许多人缺乏编写整洁代码的概念。起初,我们努力学习快速提高,改进事物,却不知道它们在以后如何影响我们。我并不特别,当我初次开始作为Android开发人员工作时,也做了同样的事情。
清晰架构(Clean Architecture)的好处
- 关注点的分离
- 从数据模型驱动UI
- 使功能易于更改或删除
- 使代码更易读
- 使用良好的实践和Jetpack库(架构组件)
分层的清晰架构(Clean Architecture)
Layer1:领域层(Domain Layer)
- 位于用户界面和数据层之间
- 在该项目中用于封装业务逻辑
- 使用例可以在多个视图模型中重用
- 定义驱动主要功能的存储库接口
Layer2:数据层(Data Layer) - 分为(本地和远程)
- 包含业务逻辑的实现(存储库实现)
- 从远程数据源获取数据
- 将远程数据缓存至本地 Room 数据库
Layer3:表示/用户界面层(Presentation/UI layer)
- 这一层是将数据显示给用户屏幕的层
- 包含生命周期友好的视图模型,并且将代码从活动/用户界面组件中抽离出来
- 定义了我们的架构,即 MVVM(模型视图视图模型)
- 包含处理逻辑,如加载的状态
示例工程
源码地址如下:
https://github.com/moemaair/Lictionary
代码结构如下:
我们将从将模型实体、对象或POJO类添加到数据目录开始。
接下来是创建 Repository,它只是一个接口,请记得创建其实现,但将其放在数据目录中,而这个接口应该放在领域层以实现互操作性。
我们还没有获取任何数据,所以让我们记住,在数据层中将其分为两个部分(本地和远程)。
数据层的结构
在远程中,我们将其放在数据传输对象(DTO)和接口中,使用您喜欢的HTTP客户端连接到远程API,我使用了Retrofit!
从https://api.dictionaryapi.dev
获取单词。
WordInfoRepo
实现如下:
class WordInfoRepoImpl(
private val api: LictionaryApi,
private val dao:WordInfoDao
) : WordInfoRepo {
override fun getWordInfo(word: String): Flow<Resource<List<WordInfo>>> = flow {
emit(Resource.Loading())
val wordInfos = dao.getWordInfos(word).map { it.toWordInfo()}
emit(Resource.Loading(data = wordInfos))
try {
val remoteWordInfos = api.getWordInfo(word)
dao.insertWordInfos(remoteWordInfos.map { it.toWordInfoEntity() })
} catch(e: HttpException) {
emit(Resource.Error(
message = "Sorry pal, we couldn't find definitions for the word you were looking for.",
data = wordInfos
))
} catch(e: IOException) {
emit(Resource.Error(
message = "Couldn't reach server, check your internet connection.",
data = wordInfos
))
}
val newWordInfos = dao.getWordInfos(word).map { it.toWordInfo() }
emit(Resource.Success(newWordInfos))
}
override fun deleteAll(){
dao.deleteAllWordInfos()
}
UseCase
UseCase是代表应用程序需要执行的单个任务的业务逻辑。对于我们只是希望从API接收“words”,我们称之为“GetWordInfo”。
在Presentation层使用ViewModel
@HiltViewModel
class MainViewModel @Inject constructor(
private val getWordInfo: GetWordInfo,
private val repo: WordInfoRepoImpl
) : ViewModel() {
private val _distinctWords = MutableStateFlow<List<WordInfo>>(emptyList())
val distinctWords: StateFlow<List<WordInfo>> = _distinctWords
var _searchQuery = mutableStateOf("")
var searchQuery: State<String> = _searchQuery
var checkIfDarkmode = mutableStateOf(false)
fun setDarkmode(darkmode: Boolean) {
checkIfDarkmode.value = darkmode
}
private var _state = mutableStateOf(WordInfoState())
var state: State<WordInfoState> = _state
private val _eventFlow = MutableSharedFlow<UIEvent>()
val eventFlow = _eventFlow.asSharedFlow()
private var searchJob: Job? = null
fun onSearch(query: String) {
_searchQuery.value = query
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(500L)
getWordInfo(query)
.onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = state.value.copy(
wordInfoItems = result.data ?: emptyList(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
wordInfoItems = result.data ?: emptyList(),
isLoading = false
)
_eventFlow.emit(
UIEvent.ShowSnackbar(
result.message ?: "Unknown error"
)
)
}
is Resource.Loading -> {
_state.value = state.value.copy(
wordInfoItems = result.data ?: emptyList(),
isLoading = true
)
}
}
}.launchIn(this)
}
}
fun deleteAll(){
repo.deleteAll()
}
sealed class UIEvent {
data class ShowSnackbar(val message: String): UIEvent()
}
现在我们可以使用来自 API 的数据并根据您可能拥有的最佳 UI 设计进行自定义。
源码地址
https://github.com/moemaair/Lictionary