Clean架构与MVVM:助你打造优质Android应用

Clean架构与MVVM:助你打造优质Android应用_第1张图片
Clean架构与MVVM:助你打造优质Android应用_第2张图片
Android应用程序开发可能具有挑战性,特别是在创建可扩展和可维护的代码以适应不断变化的需求和用户需求方面。为了解决这个挑战,开发者通常依赖于软件架构模式,为代码组织和关注点分离提供了坚实的基础。在Android开发中,两种流行的架构模式是Model-View-ViewModel (MVVM) Clean架构。

这两种方法都强调模块化、可测试性和关注点分离,但它们在具体实施细节上有所不同。在本文中,我们将探讨如何结合MVVM和Clean架构来创建健壮且易于测试、扩展和维护的Android应用程序。我们还将介绍使用这种强大组合的最佳实践和实现技巧。

Model-View-ViewModel + Repository Layer

Clean架构与MVVM:助你打造优质Android应用_第3张图片

为什么使用MVVM架构?

  • 主要目标:将演示逻辑与应用的业务逻辑分离
  • 更好的可测试性
  • 遵循SOLID原则
  • 等等

Model

代表数据提供者

  • REST
  • Database
  • Preferences

View

代表

  • UI组件
  • 显示数据

Viewmodel

代表显示逻辑

  • 准备数据以供呈现
  • 使用观察者模式通知变更

怎样在没有使用“干净架构” (Clean Architecture) 的情况下实现它?

Clean架构与MVVM:助你打造优质Android应用_第4张图片
每个开发人员都以学习和快速编写代码的愿望开始自己的职业生涯;然而,我们中的许多人缺乏编写整洁代码的概念。起初,我们努力学习快速提高,改进事物,却不知道它们在以后如何影响我们。我并不特别,当我初次开始作为Android开发人员工作时,也做了同样的事情。

清晰架构(Clean Architecture)的好处

  • 关注点的分离
  • 从数据模型驱动UI
  • 使功能易于更改或删除
  • 使代码更易读
  • 使用良好的实践和Jetpack库(架构组件)

分层的清晰架构(Clean Architecture)

Clean架构与MVVM:助你打造优质Android应用_第5张图片
Layer1:领域层(Domain Layer)

  • 位于用户界面和数据层之间
  • 在该项目中用于封装业务逻辑
  • 使用例可以在多个视图模型中重用
  • 定义驱动主要功能的存储库接口

Layer2:数据层(Data Layer) - 分为(本地和远程)

  • 包含业务逻辑的实现(存储库实现)
  • 从远程数据源获取数据
  • 将远程数据缓存至本地 Room 数据库

Layer3:表示/用户界面层(Presentation/UI layer)

  • 这一层是将数据显示给用户屏幕的层
  • 包含生命周期友好的视图模型,并且将代码从活动/用户界面组件中抽离出来
  • 定义了我们的架构,即 MVVM(模型视图视图模型)
  • 包含处理逻辑,如加载的状态

示例工程

源码地址如下:

https://github.com/moemaair/Lictionary
Clean架构与MVVM:助你打造优质Android应用_第6张图片

Clean架构与MVVM:助你打造优质Android应用_第7张图片
代码结构如下:
Clean架构与MVVM:助你打造优质Android应用_第8张图片
我们将从将模型实体、对象或POJO类添加到数据目录开始。
Clean架构与MVVM:助你打造优质Android应用_第9张图片
接下来是创建 Repository,它只是一个接口,请记得创建其实现,但将其放在数据目录中,而这个接口应该放在领域层以实现互操作性。
Clean架构与MVVM:助你打造优质Android应用_第10张图片
我们还没有获取任何数据,所以让我们记住,在数据层中将其分为两个部分(本地和远程)。

数据层的结构
Clean架构与MVVM:助你打造优质Android应用_第11张图片

在远程中,我们将其放在数据传输对象(DTO)和接口中,使用您喜欢的HTTP客户端连接到远程API,我使用了Retrofit!
Clean架构与MVVM:助你打造优质Android应用_第12张图片

https://api.dictionaryapi.dev获取单词。
Clean架构与MVVM:助你打造优质Android应用_第13张图片
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”。
Clean架构与MVVM:助你打造优质Android应用_第14张图片
在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

你可能感兴趣的:(Android架构,android,架构,ui)