Android Clear架构最强官方指南Kotlin版

虎哥LoveOpenSource 发表于 2月以前  | 总阅读数:578 次

Android Clear架构最强官方指南Kotlin版

在这篇文章中,我将介绍关于Android应用程序架构的一些内容。尽管自从早期更稳健的Android架构方法在移动开发中变得流行以来已经说了很多话,但改进和演进的空间总是存在的。

基于上述文章中的清晰架构示例,代码库中有明显的演进,特别是在当今应用程序在业务层面至关重要的情况下,更加需要扩展、模块化和组织围绕移动开发的团队(主要是由于其复杂性)。

因此,我们的目标是提出一种优雅的解决方案,以便在以下方面使我们的工作更加轻松:

  • 解决问题。
  • 可扩展性。
  • 模块化。
  • 可测试性。
  • 独立于框架、UI和数据库。

这是一个架构图,如果您在Android应用程序中使用了Clear架构,它应该看起来很熟悉。

我们的场景

一个简单的电影Android应用程序。采用Kotlin编写,我们希望利用现代语言的特性,如不可变性、简洁性、函数式编程等。

以下是App项目的截图:

我们有3个主要的用例:

  • 获取电影列表。
  • 显示特定点击电影的详细信息。
  • 播放电影。

通用架构

总体架构是基于基本的三层架构。好处在于,它非常容易理解,许多人都熟悉它。因此,我们将分解我们的解决方案以遵守依赖关系规则, 其中依赖关系沿着一个方向流动:请参阅下面的圆形Clear架构图。

领域层(Domain Layer):功能用例

用例是我们应用程序中要做的意图,换句话说,是我们的主要参与者之一。它的主要责任是协调我们的领域逻辑以及与UI和数据层的连接。

通过使用Kotlin的强大功能和将函数作为一等公民对待的方式(即将在稍后提到的内容),在我们的框架中有一个UseCase抽象,它充当我们应用程序中所有用例的契约。

abstract class UseCase<out Type, in Params> where Type : Any {

    abstract suspend fun run(params: Params): Either<Failure, Type>

    fun execute(onResult: (Either<Failure, Type>) -> Unit, params: Params) {
        val job = async(CommonPool) { run(params) }
        launch(UI) { onResult.invoke(job.await()) }
    }
}

这里发生了什么?

我们有一个抽象类,它接受两个泛型参数:

  • <out Type>:执行用例后的返回类型。
  • <in Params>:一个参数类,在我们需要用例的额外数据时,将在run()函数内使用。

execute()函数是魔法发生的地方:

  • • 我们传递了一个onResult 函数作为参数,该函数接受Either<Failure, Type>类型的参数,并返回Unit(在错误处理部分,我将扩展对Either<L, R>的解释,请耐心等待)。好处是,UseCase的调用者通过传递这个不可变函数(onResult),实际上确定了所需的行为,从而避免了任何内部暴露或副作用(这是函数式编程的好处之一,稍后会有更多)。
  • • 同样,通过使用Kotlin协程,我们在不同的线程中调用传递的onResult函数,因此从这一点开始,我们可以以同步的方式编写代码。结果将发布在Android主UI线程上。

当扩展UseCase<out Type, in Params>抽象时,我们必须覆盖abstract suspend fun run(params: Params)函数。例如,这就是我们的GetMovies用例的样子:

class GetMovies 
@Inject constructor(private val moviesRepository: MoviesRepository) : 
    UseCase<List<Movie>, None>() {

    override suspend fun run(params: None) = moviesRepository.movies()
}

在这个例子中,我们将电影的获取委托给一个Repository。

UI层(UI Layer):从MVP到MVVM

Model-View-ViewModel(MVVM)模式在用户界面和领域逻辑之间提供了清晰的责任分离。

它有3个主要组件:模型(model)、视图(view)和视图模型(view model)。它们之间存在关系,尽管每个组件都有不同且独立的角色:

在最高级别上,视图“知道”视图模型,而视图模型“知道”模型,但模型不知道视图模型,视图模型也不知道视图。视图模型将视图与模型类隔离开来,并允许模型独立于视图进行演进。

在我们的示例中,MVVM的实现是通过使用架构组件完成的,它的主要优势是在屏幕旋转时处理配置更改,这对于Android开发人员来说是一个常见的头痛问题(我想你明白我在说什么)。

免责声明:这并不意味着我们不再需要关心生命周期,但处理起来要容易得多。

关于前面示例中的MVP(Model View Presenter)的一点注释:我发现很难避免由于活动和片段被重新创建而导致的内存泄漏,所以我使用了一个简单的解决方案:保留片段(retain fragments)。

然而,我无论如何都会遇到这种情况。这就是为什么我决定去尝试MVVM的原因。

让我们看看MVVM与之前示例相比有何变化以及它是如何工作的:

  • 片段充当视图,在这里发生与屏幕上数据显示相关的所有逻辑。
  • 片段也知道视图模型,它们实际上是订阅视图模型的。
  • 视图模型包含LiveData对象和对UseCases的引用。
  • UseCases更新LiveData,LiveData对这些更改做出反应并通知视图模型。
  • 视图模型与订阅的片段进行通信,以更新UI。

为了看到所有这些部分如何协同工作,让我们看一些代码。包含LiveData并通过调用UseCase.execute()函数进行更新的ViewModel:

class MoviesViewModel
@Inject constructor(private val getMovies: GetMovies) : BaseViewModel() {

    var movies: MutableLiveData<List<MovieView>> = MutableLiveData()

    fun loadMovies() = 
        getMovies.execute({ it.either(::handleFailure, ::handleMovieList) }, None())

    private fun handleMovieList(movies: List<Movie>) {
        this.movies.value = movies.map { MovieView(it.id, it.poster) }
    }
}

片段在onCreate()中订阅上述的ViewModel。

我使用了一些扩展函数的技巧来减少一些冗长和样板代码。

class MoviesFragment : BaseFragment() {

    @Inject lateinit var navigator: Navigator
    @Inject lateinit var moviesAdapter: MoviesAdapter

    private lateinit var moviesViewModel: MoviesViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        appComponent.inject(this)

        //subscribtion to LiveData in MoviesViewModel
        moviesViewModel = viewModel(viewModelFactory) {
            observe(movies, ::renderMoviesList)
            failure(failure, ::handleFailure)
        }
    }
    ...
}

数据层(Data Layer):拯救的存储库模式

与之前的示例相比,这里有什么新东西吗?因为我在使用存储库模式时取得了非常好的结果。

需要记住的是:在其核心,存储库模式是一个简单的接口。它存在于我们的领域和数据之间的一层,这样我们的逻辑就不需要关注不同数据源的实现:网络、数据库或内存。

在下面的代码块中,我们可以看到我们的MoviesRepository契约:

interface MoviesRepository {
    fun movies(): Either<Failure, List<Movie>>
    fun movieDetails(movieId: Int): Either<Failure, MovieDetails>
}

在我们的示例中,我们通常将Repository作为UseCase实现的协作者进行注入。

功能性错误处理

整体上,错误/异常处理应该在设计层面而不是实现层面上加以处理,而在我看来,作为开发者我们所犯的最大错误之一就是这个(吸取了教训)。这就是为什么有一个专门的框架来处理这个目的非常重要。

传统的错误处理会发生什么?

观察异常(try/catch块)并基于此做出改变控制流的决策是一种不好的实践:它会导致不可预测性,影响我们的弹性,并且调试变得困难,尤其是在并发环境中。再加上回到C风格的错误处理,使用需要按约定检查的错误代码可能会成为一个噩梦。

说到这里,我们已经看到我们在UseCase抽象中使用Either<L, R>作为返回类型:

abstract suspend fun run(params: Params): Either<Failure, Type>

因此,让我介绍一下Either

Either<L, R>被称为不相交函数,这意味着这个结构被设计为只容纳Left<T>Right<T>值,而不是两者兼有。它是一种函数式编程的单子类型,尚未存在于Kotlin标准库中。

这里有一个简单的实现,非常符合我的需求,并且易于理解和使用:

/**
 * Represents a value of one of two possible types (a disjoint union).
 * Instances of [Either] are either an instance of [Left] or [Right].
 * FP Convention dictates that: 
 *      [Left] is used for "failure".
 *      [Right] is used for "success".
 *
 * @see Left
 * @see Right
 */
sealed class Either<out L, out R> {
    /** 
    * Represents the left side of [Either] class 
    * which by convention is a "Failure". 
    */
    data class Left<out L>(val a: L) : Either<L, Nothing>()

    /** 
    * Represents the right side of [Either] class 
    * which by convention is a "Success". 
    */
    data class Right<out R>(val b: R) : Either<Nothing, R>()

    val isRight get() = this is Right<R>

    val isLeft get() = this is Left<L>

    fun either(fnL: (L) -> Any, fnR: (R) -> Any): Any =
            when (this) {
                is Either.Left -> fnL(a)
                is Either.Right -> fnR(b)
            }

    fun <T> flatMap(fn: (R) -> Either<L, T>): Either<L, T> {...}
    fun <T> map(fn: (R) -> (T)): Either<L, T> {...}
}

让我也引用一下Daniel Westheide(Scala专家)在他的一篇精彩博客文章中的话:

Either<L, R>类型的语义中没有规定其中一个子类型分别表示错误或成功。事实上,Either是一种通用类型,用于处理结果可能为两种可能类型之一的情况。

然而,错误处理是它的一种常见用例,并且根据惯例,当以这种方式使用时,Left<T>表示错误情况,而Right<T>包含成功值。

请不要忘记阅读他的整个Scala系列文章,以拓宽你的视野(从其他语言中获取灵感总是+1):

  • 使用Try进行错误处理。

    http://danielwestheide.com/blog/2012/12/26/the-neophytes-guide-to-scala-part-6-error-handling-with-try.html

  • Either类型。

    http://danielwestheide.com/blog/2013/01/02/the-neophytes-guide-to-scala-part-7-the-either-type.html

那么我们的代码示例呢?

在GetMovies UseCase中,在实现层面上,我们总是返回一个Either<Failure, List<Movie>>,从数据层开始一直到我们的MoviesViewModel,它会更新either失败LiveData<Failure>(如果失败,则为Left<T>)或电影LiveData<List<MovieView>>(成功,Right<T>):

class MoviesViewModel
@Inject constructor(private val getMovies: GetMovies) {

    var movies: MutableLiveData<List<MovieView>> = MutableLiveData()
    var failure: MutableLiveData<Failure> = MutableLiveData()

    fun loadMovies() = 
        getMovies.execute({ it.either(::handleFailure, ::handleMovieList) }, None())

    private fun handleMovieList(movies: List<Movie>) {
        this.movies.value = movies.map { MovieView(it.id, it.poster) }
    }

    private fun handleFailure(failure: Failure) {
        this.failure.value = failure
    }
}

在视图层级的MoviesFragment中,我们订阅来自视图模型的更新:

moviesViewModel = viewModel(viewModelFactory) {
    observe(movies, ::renderMoviesList)
    failure(failure, ::handleFailure)
}

这是用于处理Failure的handleFailure()函数的样子:

private fun handleFailure(failure: Failure?) {
    when (failure) {
        is NetworkConnection -> renderFailure(R.string.failure_network_connection)
        is ServerError -> renderFailure(R.string.failure_server_error)
        is ListNotAvailable -> renderFailure(R.string.failure_movies_list_unavailable)
    }
}

顺便说一下,Failure是一个密封类,它提供了全局默认的Failures:

/**
 * 用于处理错误/失败/异常的基类。
 * 每个特定功能的失败应该扩展[FeatureFailure]类。
 */
sealed class Failure {
    class NetworkConnection: Failure()
    class ServerError: Failure()

    /** * 扩展此类以获取特定功能的失败。*/
    abstract class FeatureFailure: Failure()
}

我希望现在对Either<L, R>的用法更清晰了,你理解了这种应用技术的原因和好处。

模块化第一步

首先,我想解释一下,这篇文章并不是专门讨论一个具体问题的,但我想分享一些经验,以便初学者更容易入门。从我的角度来看,模块化开发的方式早晚都会被广泛采用,并且通过良好的架构设计可以更好地实现这个目标。

什么是模块化?

模块化是一种将代码的逻辑组件分离并创建清晰边界的方法。

如果你已经做了功课,并且看过我之前关于Android架构的帖子,你可能已经注意到我使用Android模块来表示每个层级的架构。

在讨论中,一个常见的问题是:为什么要这样做?答案很简单... 这是为了避免错误的技术决策。通过建立更严格的依赖规则和边界,我们可以减少模块之间的相互影响。

然而,权力伴随着巨大的责任。虽然一开始看起来效果很好,但模块化也会带来一些问题:

  • 当我们修改或添加新功能时,我们必须同时修改每个单独的模块/层(因为它们之间存在强依赖/耦合)。
  • 开发人员在共同使用代码库时可能会发生冲突(特别是在团队规模较大、与代码审查和git相关的情况下)。

拥抱App模块化

我倾向于模块化的第一个提示是按功能组织Package,这样我们可以实现:

更高的模块性。 更高的内聚性。 更容易的代码导航。 最小化作用域。 隔离和封装。

代码/包的组织是良好架构的关键因素之一:包结构是程序员在浏览源代码时遇到的第一件事。一切都从这里开始。一切都依赖于它。

我的第二个提示是创建一个核心模块,它将拥有以下主要职责:

处理全局依赖注入。 包含扩展函数。 包含主要框架抽象。 在主应用程序中启动常见的第三方库,如Analytics、Crash Reporting等。

我的第三个提示不涉及到代码库级别,但如果我们正在与功能团队合作,添加代码所有权可能会有所帮助,这对于在许多开发人员共同使用代码库的不断发展的组织中获得胜利来说是一件好事。

这些是模块化的主要优点:

更快的构建时间。 包内凝聚力。 共享常见功能的可重用性。 减少冲突(特别是在使用git流时)。 特性封装。 更加控制的依赖项。 团队合作:团队之间的协作。

我知道这些听起来在纸面上很好,虽然将Android代码库模块化是棘手和具有挑战性的(因为涉及到许多部分),但优点是巨大的。

结论

本文使用Kotlin实现了Clear Arch, 涉及到比较抽象的理论知识,给出了相关理论实现,希望对你今后Android开发有大的帮助。

项目地址

本文由微信公众号虎哥LoveOpenSource原创,哈喽比特收录。
文章来源:https://mp.weixin.qq.com/s/-g7_3Q2QjKuxManqFM2FhA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 相关文章
简化Android的UI开发 4年以前  |  520723次阅读
Android 深色模式适配原理分析 3年以前  |  28664次阅读
Android阴影实现的几种方案 1年以前  |  10826次阅读
Android 样式系统 | 主题背景覆盖 3年以前  |  9614次阅读
 目录