Android应用程序的体系结构和设计(我的经验)

哈伯,你好!

今天,我想谈谈我在Android应用程序中遵循的架构。我以Clean Architecture为基础,并使用Android Architecture组件(ViewModel,LiveData,LiveEvent)和Kotlin Coroutines作为工具。随附的是在GitHub上可用的虚拟示例代码

免责声明


我想分享自己的开发经验,但我绝不假装我的解决方案是唯一的,没有缺陷的解决方案。应用程序的体系结构是我们选择用来解决特定问题的一种模型,对于所选模型,其适用于特定任务的重要性很重要。

问题:为什么我们需要建筑?


我参与的大多数项目都存在相同的问题:应用程序逻辑放置在android环境中,这导致Fragment和Activity中包含大量代码。因此,代码被根本不需要的依赖项所包围,单元测试几乎变得不可能,而且重用。随着时间的流逝,碎片成为上帝的对象,即使很小的变化也会导致错误,支持项目变得既昂贵又情感上昂贵。

有些项目根本没有架构(这里一切都很清楚,没有问题),有些项目声称拥有架构,但是无论如何还是出现了同样的问题。现在,在Android中使用Clean Architecture变得很流行。我经常看到,Clean Architecture仅限于创建存储库和脚本,这些存储库和脚本可以调用这些存储库而无所作为。更糟糕的是:此类脚本从被调用的存储库中返回模型。在这种架构中,根本没有意义。而且因为 由于脚本仅调用必要的存储库,因此逻辑通常基于ViewModel,或者更糟糕的是,它们位于片段和活动中。这一切都变成了混乱,不适合自动测试。

建筑和设计的目的


架构的目的是将我们的业务逻辑与细节分开。详细地说,我的意思是,例如,外部API(当我们为REST服务开发客户端时),Android-环境(UI,服务)等。核心是使用Clean体系结构,但要考虑实现的前提。

设计的目的是将UI,API,业务逻辑,模型联系在一起,以便所有这些都可以自动测试,松散耦合并易于扩展。在设计中,我使用Android体系结构组件。

对我来说,架构应满足以下条件:

  1. UI尽可能简单,只有三个功能:
  2. 向用户展示数据。数据准备显示。这是UI的主要功能。这是小部件,动画,片段等。
  3. . ViewModel LiveData.
  4. . framework, . .
  5. - . .


下图显示了该体系结构的示意图:

图片

我们从下到上逐层移动,而下面的层对上面的层一无所知。顶层仅指的是下一层的层。那些。 API层不能引用域。

域层包含具有自己逻辑的业务实体。通常,存在不存在应用程序的实体。例如,对于一家银行,可能存在具有复杂逻辑以计算利息的贷款实体等。

应用程序逻辑层包含应用程序本身的脚本。在此确定了应用程序的所有连接,并建立了其本质。

api层android只是我们在Android环境中应用程序的特定实现。理想情况下,该层可以更改为任何层。

, , — . . 2- . , . . TDD , . Android, API ..

Android-.

图片

因此,逻辑层是关键,它是应用程序。只有逻辑层才能引用域并与域交互。此外,逻辑层包含允许逻辑与应用程序(api,android等)的细节进行交互的接口。这就是所谓的依赖倒置原理,它允许逻辑不依赖细节,反之亦然。逻辑层包含应用程序的使用场景(用例),它们对不同的数据进行操作,与域,存储库等交互。在开发中,我喜欢用脚本思考。对于系统中的每个用户动作或事件,都会启动某个脚本,该脚本具有输入和输出参数以及一种方法-运行脚本。

有人介绍了交互器的另一个概念,该概念可以组合多种使用场景并结束其他逻辑。但是我不这样做,我相信每个脚本都可以扩展或包含任何其他脚本,这不需要交互器。如果查看UML模式,则可以看到包含和扩展的连接。

该应用程序的一般方案如下:

图片

  1. 创建了一个android环境(活动,片段等)。
  2. 创建一个ViewModel(一个或多个)。
  3. ViewModel创建可以从此ViewModel运行的必要脚本。最好将场景注入DI。
  4. 用户提交一个动作。
  5. UI的每个组件都与可以运行的命令相关联。
  6. 使用必要的参数运行脚本,例如Login.execute(登录名,密码)。
  7. DI , . ( api, ). . , , , REST JSON . , , . , . , . - . , . , , . , , .
  8. . ViewModel, UI. LiveData (.9 10).

那些。我们拥有的关键角色是逻辑及其数据模型。我们看到了双重转换:第一个是将存储库转换为场景数据模型,第二个是当脚本通过其工作将数据提供给环境时进行转换。通常,脚本的结果会提供给viewModel以便在UI中显示。该脚本应返回viewModel和UI不能执行其他操作的数据。

命令

UI使用命令开始执行脚本。在我的项目中,我使用自己的命令实现,它们不属于体系结构组件或其他任何组件。通常,它们的实现很简单,作为对该概念的更深入了解,您可以在reactui.net中看到命令的实现。对于C#。不幸的是,我无法列出我的工作代码,仅提供一个简化的实现示例。

该命令的主要任务是运行一些脚本,将输入参数传递到其中,并在执行后返回命令的结果(数据或错误消息)。通常,所有命令都是异步执行的。此外,团队还封装了背景计算方法。我使用协程,但是它们很容易用RX代替,而您只需要在抽象的命令+用例中进行即可。作为奖励,团队可以报告其状态:是否正在执行,以及原则上是否可以执行。团队可以轻松解决一些问题,例如,重复通话问题(当用户在操作运行过程中多次单击按钮时)或可见性和取消问题。


实施功能:使用登录名和密码登录到应用程序。
该窗口应包含登录名和密码输入字段以及“登录”按钮。工作逻辑如下:

  1. 如果用户名和密码少于4个字符,则“登录”按钮应处于非活动状态。
  2. 在登录过程中,“登录”按钮必须处于非活动状态。
  3. 在登录过程中,应该显示一个指示器(装载程序)。
  4. 如果登录成功,将显示欢迎消息。
  5. 如果登录名和/或密码不正确,则在登录字段上方应显示一条错误消息。
  6. 如果屏幕上显示错误消息,则在登录名或密码字段中输入任何字符都会删除该消息,直到下一次尝试。

您可以通过多种方式解决此问题,例如,将所有内容放入MainActivity。
但是我始终遵循两个主要规则的执行:

  1. 业务逻辑与细节无关。
  2. UI尽可能简单。他只处理自己的任务(他显示传输给他的数据,并广播用户的命令)。

这是应用程序的外观:

图片

MainActivity如下所示:

class MainActivity : AppCompatActivity() {

   private val vm: MainViewModel by viewModel()

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       bindLoginView()
       bindProgressBar()

       observeAuthorization()
       observeRefreshView()
   }

   private fun bindProgressBar() {
       progressBar.bindVisibleWithCommandIsExecuting(this, vm.loginCommand)
   }

   private fun bindLoginView() {
       loginEdit.bindAfterTextChangedWithCommand(vm.loginValidityCommand)
       passwordEdit.bindAfterTextChangedWithCommand(vm.passwordValidityCommand)

       loginButton.bindCommand(this, vm.loginCommand) {
           LoginParameters(loginEdit.text.toString(), passwordEdit.text.toString())
       }
   }

   private fun observeAuthorization() {
       vm.authorizationSuccessLive.observe(this, Observer {
           showAuthorizeSuccessMsg(it?.data)
       })
       vm.authorizationErrorLive.observe(this, Observer {
           showAuthorizeErrorMsg()
       })
   }

   private fun observeRefreshView() {
       vm.refreshLoginViewLive.observe(this, Observer {
           hideAuthorizeErrorMsg()
       })
   }

   private fun showAuthorizeErrorMsg() {
       loginErrorMsg.isInvisible = false
   }

   private fun hideAuthorizeErrorMsg() {
       loginErrorMsg.isInvisible = true
   }

   private fun showAuthorizeSuccessMsg(name : String?) {
       val msg = getString( R.string.success_login, name)
       Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
   }
}


该活动非常简单,执行了UI规则。我写了一些简单的扩展,例如bindVisibleWithCommandIsExecuting,将命令与UI元素相关联,而不是重复的代码。GitHub上

提供了带有注释的示例代码,如果有兴趣,您可以下载并熟悉自己。 就这样,谢谢收看!


All Articles