架构探险-轻量级微服务架构

This series takes a basic MVP app using Retrofit and RxJava to display a list of Github repositories; and converts it into a modern Android app — along the way it will give an introduction to a variety of techniques used when architecting Android apps, explain why those techniques are used, and perform a few experiments to boot.

本系列使用一个基本的MVP应用程序,该应用程序使用Retrofit和RxJava来显示Github存储库列表。 并将其转换为现代的Android应用程序-在此过程中,它将介绍架构Android应用程序时使用的各种技术,解释为什么使用这些技术,并执行一些启动实验。

If you just want to see the code go here the repo will be updated as the series progresses.

如果您只想查看代码,请此系列进行过程中更新仓库。

Part 1 — Simple dependency injection with Dagger

第1部分-使用Dagger进行简单的依赖注入

Part 2 — Converting Presenters into ViewModels

第2部分-将演示者转换为ViewModels

Part 3— Single activity architecture + some funky Dagger

第3部分-单活动架构+一些时髦的Dagger

To follow along, this is the project after using MVVM. The next change will be to make use of single activity architecture and the navigation androidx library, plus there’s something funky we can do with Dagger. If you’re comfortable converting activities to fragments, skip on to halfway through the post.

接下来这是使用MVVM之后的项目 。 下一个更改将是利用单一活动架构和导航androidx库,此外,我们可以使用Dagger做一些时髦的事情。 如果您愿意将活动转换为片段,请跳到帖子的一半。

2020年的碎片 (Fragments in 2020)

Fragments have come a long way since an infamous Advocating Against Fragments post by square in 2014. Back then Fragments were complex to navigate between, bug prone, and had a cumbersome lifecycle.

自从2014年臭名昭著的提倡“碎片倡导”以来,碎片就已经走了很长一段路。那时,碎片在导航之间很复杂,容易出错,并且生命周期繁琐。

Fast forward to 2020 and the androidx navigation library solves fragment navigation, bugs are few and far between, and the lifecycle…well the lifecycle is still quite cumbersome.

快进到2020年,androidx导航库解决了片段导航,错误很少且相差甚远,而且生命周期…嗯,生命周期仍然很麻烦。

But, for better or worse, single activity architecture is the recommended approach from Google and seems to be the future of Android development. So lets go ahead and convert this project to single activity architecture, and see some of the benefits it can bring.

但是,无论好坏,单一活动架构是Google推荐的方法,并且似乎是Android开发的未来。 因此,让我们继续将该项目转换为单一活动架构,并了解它可以带来的一些好处。

一项活动 (A single activity)

This will be the only activity in the entire app, conceptually similar to your application class, but specific to your apps UI. All our fragments will be launched within this activity.

这将是整个应用程序中的唯一活动,从概念上讲类似于您的应用程序类,但特定于您的应用程序UI。 我们的所有片段都将在此活动中启动。

As it’s our only activity a good place to put it can be as a direct descendent of the UI folder.

因为这是我们的唯一活动,所以可以将它放置为UI文件夹的直接后代。

将活动变形为碎片 (Morphing Activities into Fragments)

Currently there are two screens, both activities. Both these activities extend BaseActivity. So the change here is simple, rename the base activity and change the class it extends.

当前有两个屏幕,均处于活动状态。 这两个活动都扩展了BaseActivity。 因此,此处的更改很简单,重命名基础活动并更改其扩展的类。

Inheritance can sometimes receive its fair share of flak, but used well, it can make code changes a breeze. In this case our BaseActivity contains all our common screen based logic which applies equally well to fragments. Here we only have 2 activities to refactor, but imagine a larger app with 10 or 20? By changing one line every Fragment has all the common screen behaviour we need.

继承有时可以得到应有的帮助,但使用得当,可以使代码更改变得轻而易举。 在这种情况下,我们的BaseActivity包含我们所有基于屏幕的通用逻辑,这些逻辑同样适用于片段。 在这里,我们只有2个活动可重构,但想象一个更大的应用程序(包含10或20)? 通过更改每一行,每个Fragment都具有我们所需的所有常见屏幕行为。

There are areas of programming that are regularly abused — inheritance being one of those — but often these areas are also very powerful. Instead of avoiding those areas, a better strategy can be to learn when they’re useful and when they’re not so useful.

有些编程领域经常被滥用-继承就是其中之一-但通常这些领域也非常强大。 除了避开这些领域,更好的策略可以是学习它们何时有用,何时不那么有用。

But there’s still some work to do to make our screens work with the fragment lifecycle.

但是,仍有一些工作要做,以使我们的屏幕在片段生命周期中正常工作。

片段生命周期 (Fragment lifecycle)

In an Activity most view initialisation code goes in onCreate. A fragment has a significantly more complex lifecycle. There’s a lot of choice when it comes to choosing where to put the code which initialises your view. I try to use:

Activity大多数视图初始化代码都放在onCreate 。 片段的生命周期复杂得多。 在选择将代码初始化视图的位置时,有很多选择。 我尝试使用:

  • onCreate for initialising data, e.g. fetching values from the Bundle.

    onCreate用于初始化数据,例如从Bundle中获取值。

  • onCreateView for inflating views. In recent versions of the androidx fragment library the layout id can be passed directly into the constructor, as I plan to add data binding to this project, I won’t use that functionality here.

    onCreateView用于扩大视图。 在最新版本的androidx片段库中,布局ID可以直接传递到构造函数中,因为我计划将数据绑定添加到该项目中,因此在此我将不使用该功能。

  • onViewCreated for putting views in the their correct state, e.g. applying listeners and initial values.

    onViewCreated用于将视图置于正确的状态,例如,应用侦听器和初始值。

  • onStart/onStop etc. as usual

    照常onStart / onStop

There’s a lot of conflicting advice on which lifecycle methods are best to use for what. Ultimately, find a structure you like and stick to it throughout your project. And if you’re ever unsure on which lifecycle method to use, check the fragment source code — it’s well documented and can tell you exactly when various lifecycle methods are called to suit your purposes.

关于哪种生命周期方法最适合用于什么有很多相互矛盾的建议。 最终,找到您喜欢的结构并在整个项目中坚持下去。 并且,如果您不确定要使用哪种生命周期方法,请检查片段源代码-它有充分的文档记录,可以准确地告诉您何时调用各种生命周期方法以适合您的目的。

And the Main Fragment

和主要片段

Although this step is quite time consuming, there’s nothing complex here. All the code stays exactly the same, it’s just rearranged into new lifecycle methods.

尽管此步骤非常耗时,但这里没有什么复杂的。 所有代码都完全相同,只是重新排列成新的生命周期方法。

需要活动的代码 (Code that needs an Activity)

There are always some places in a fragment where you’ll need access to the activity or context. But there’s no guarantee in the api that the context or activity will be non-null when you need it. So to avoid a mountain of null checks, the androidx libraries helps you out.

片段中总是存在一些需要访问活动或上下文的地方。 但是在api中不能保证上下文或活动在需要时不会为空。 因此,为了避免大量的null检查,androidx库可以帮助您。

Toast.makeText(requireContext(), it.error, Toast.LENGTH_LONG).show()

There’s also a requireActivity method if needed. And for cases where you specifically need an AppCompatActivity, ComponentActivity, or even your single activity (AndroidPlaygroundActivity)— you can create your own extension function.

如果需要,还有一个requireActivity方法。 对于特别需要AppCompatActivityComponentActivity甚至是单个活动( AndroidPlaygroundActivity )的情况,您可以创建自己的扩展功能。

fun Fragment.requireAppCompatActivity(): AppCompatActivity {
    return requireActivity() as? AppCompatActivity
           ?: throw IllegalStateException("Activity this fragment is attached to does not extend AppCompatActivity")
}
requireAppCompatActivity().supportActionBar?.title = it.toolbarTitle

When using these methods just be aware that it isn’t completely clean. By using methods such as these we are making assumptions about where the fragment is being used and what state the fragments in. Most of the time thats fine, but sometimes it’s not.

使用这些方法时,请注意它并不完全干净。 通过使用诸如此类的方法,我们对片段的使用位置和片段的状态进行了假设。在大多数情况下,这很好,但有时并非如此。

更新清单 (Update the manifest)

With all our screens converted to Fragments, our Manifest can now be significantly simplified — with a single Activity declaration!

将所有屏幕转换为片段后,现在可以通过单个Activity声明来显着简化清单。

<activity android:name=".ui.AndroidPlaygroundActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />


        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

And that’s it, single activity architecture is ready (minus navigation). You can manually navigate between fragments, but that’s often complex and bug prone. With the navigation androidx library, navigation is a breeze. There’s a lot of great docs and articles out there on the navigation library so I won’t go into how to set it up here. But you can look at the repository to see it in action.

就是这样,单活动架构已准备就绪(减去导航)。 您可以手动在片段之间导航,但这通常很复杂且容易出错。 使用导航androidx库,导航变得轻而易举。 导航库中有很多很棒的文档和文章,所以我不会在这里进行设置。 但是您可以查看存储库以了解其运行情况。

时髦的依赖注入 (Funky dependency injection)

Now the interesting bit!

现在有趣的一点!

Image for post

更新17/06/2020: (Update 17/06/2020:)

With the alpha release of hilt dependency injection with Dagger is about to get a hell of a lot easier! Hilt is an opinionated library over the top of dagger that removes almost all boilerplate dagger code. It makes DI simpler and attempts to standardise how DI is written in Android. From an early look it’s a huge improvement and is sure to become the favoured approach for DI in Android.

随着Dagger 的hilt依赖注入的alpha版本的发布,地狱将变得容易得多! Hilt是匕首上方的自定义库,可删除几乎所有样板匕首代码。 它使DI变得更简单,并试图标准化DI在Android中的编写方式。 从早期的角度来看,它是一个巨大的改进,并且肯定会成为Android中DI的首选方法。

The rest of this article goes into some principles about DI, but be aware that with the release of hilt the DI method used in this article should be considered legacy and is actually much easier to achieve with hilt.

本文的其余部分介绍了有关DI的一些原则,但是请注意,随着hilt的发布,本文中使用的DI方法应被认为是旧有的,实际上,使用hilt可以轻松实现。

新组件 (A new component)

So far our Dagger setup has been made of a single component — AndroidPlaygroundComponent — and every object within that component has access to every other object. That works fine for simple apps, but as apps get bigger we often need to create additional components which only have access to a limited selection of objects.

到目前为止,我们的Dagger设置由单个组件AndroidPlaygroundComponent组成,该组件中的每个对象都可以访问其他每个对象。 对于简单的应用程序来说,这很好用,但是随着应用程序变得越来越大,我们经常需要创建只能访问有限对象选择的其他组件。

To try this out lets create a second component…in fact we’ve already been creating additional components, every time @ContributesAndroidInjector is used, Dagger is creating a (Android specific) subcomponent behind the scenes for reasons I won’t go into here — suffice to say the Android OS is not DI friendly.

要尝试进行此操作,请创建第二个组件……事实上,我们已经在创建其他组件,每次使用@ContributesAndroidInjector ,Dagger都会在幕后创建一个(特定于Android的)子组件,原因是我不会在这里介绍–足以说Android操作系统不兼容DI。

With knowledge of @ContributesAndroidInjector, it’s easy to create a component for our single activity which contains scoped objects that can only be accessed by other objects in the same component (psst. this means all these objects can only be alive while the Activity is alive).

有了@ContributesAndroidInjector知识,很容易为我们的单个活动创建一个组件,该组件包含只能由同一组件中的其他对象访问的作用域对象(psst。这意味着所有这些对象只能在Activity处于活动状态时处于Activity状态) 。

@Module
interface SingleActivityModule {


    @SingleActivity
    @ContributesAndroidInjector(
            modules = [
                MyScopedModule::class
            ]
    )
    fun androidPlaygroundActivity(): AndroidPlaygroundActivity
}


@Module
interface MyScopedModule {


    @Binds
    fun objectOne(objectOneImpl: ObjectOneImpl): ObjectOne


    @Binds
    fun objectTwo(objectTwoImpl: ObjectTwoImpl): ObjectTwo
}

The @ContributesAndroidInjector annotation takes a modules parameter which defines all the objects available to this component. Every object within @ContributesAndroidInjector can access every other object. But if you try to inject the object into another component, dagger will throw a compile time error.

@ContributesAndroidInjector批注采用modules参数,该参数定义了此组件可用的所有对象。 @ContributesAndroidInjector每个对象都可以访问其他每个对象。 但是,如果您尝试将对象注入到另一个组件中,则匕首将引发编译时错误。

What about if we need an object fromAndroidPlaygroundComponent ? No problem, each subcomponent has access to all the objects in it’s parent component, grandparent component, great-grandparent component, and so on.

如果我们需要AndroidPlaygroundComponent的对象怎么办? 没问题,每个子组件都可以访问其父组件,祖父母组件,曾祖父母组件等中的所有对象。

向图中添加片段 (Adding Fragments to the graph)

As we’re using single activity architecture, fragments can only ever exist in the single activity. So they should also be scoped to the Activity component.

当我们使用单一活动架构时,片段只能存在于单一活动中。 因此,它们也应仅限于“ Activity组件。

@Module
interface SingleActivityModule {


    @SingleActivity
    @ContributesAndroidInjector(
            modules = [
                MyScopedModule::class,
                FragmentModule::class
            ]
    )
    fun androidPlaygroundActivity(): AndroidPlaygroundActivity
}
@Module
interface FragmentModule {


    @ContributesAndroidInjector
    fun mainFragment(): MainFragment


    @ContributesAndroidInjector
    fun detailsFragment(): DetailsFragment
}

You’ll notice that the fragments use @ContributesAndroidInjector, this is again due to Androids DI unfriendliness which means Dagger needs to create a new subcomponent for each Fragment.

您会注意到片段使用@ContributesAndroidInjector ,这又是由于Android的DI不友好,这意味着Dagger需要为每个Fragment创建一个新的子组件。

You might rightly say here that Fragments shouldn’t know about each other in the DI graph. No problem, subcomponents can’t use objects in other subcomponents that are not their parent. So in our dagger world, MainFragment has no knowledge of DetailsFragment and visa versa.

您在这里可能会正确地说,碎片在DI图中不应该彼此了解。 没问题,子组件不能使用不是其父级的其他子组件中的对象。 因此,在我们的匕首世界中, MainFragment不了解DetailsFragment ,反之亦然。

但是为什么这有用呢? (But why is this useful?)

Take something like navigation, in most Android apps the code for navigation is scattered throughout our screens. This breaks many fundamental principles of good architecture. Which you realise when upgrading to the androidx navigation library — a change needs made to EVERY screen, in large apps this becomes extremely cumbersome. Simply put, scattered responsibility is hard to maintain and bug prone.

例如导航,在大多数Android应用中,导航代码分散在我们的屏幕上。 这打破了良好架构的许多基本原则。 升级到androidx导航库时,您意识到了这一点-需要对每个屏幕进行更改,在大型应用程序中,这变得非常麻烦。 简而言之,分散的责任很难维护并且容易出错。

Architectural principles often encourage abstracting responsibility behind interfaces so that responsibility becomes an implementation details — easy to swap in and out without effecting the rest of the app. These days this is commonly encouraged for the data layer of the app and implementation details like Retrofit are often hidden behind interfaces. So why not use the same concept in the view layer of our app?

架构原则通常鼓励在接口后抽象责任,以使责任成为实现的细节—易于调换,而不会影响应用程序的其余部分。 如今,通常在应用程序的数据层鼓励这样做,而诸如Retrofit之类的实现细节通常隐藏在界面后面。 那么,为什么不在我们的应用程序的视图层中使用相同的概念呢?

抽象导航 (Abstracting navigation)

What if our Fragment has no knowledge of how navigation occurs? Instead fragments use a simple navigation interface to order navigation. This simplifies fragments by limiting their responsibility and encourages composable behaviour in the UI — reducing code duplication.

如果我们的Fragment不知道导航如何发生怎么办? 相反,片段使用简单的导航界面来排序导航。 这通过限制片段的责任来简化片段,并在UI中鼓励可组合的行为-减少代码重复。

interface Navigator {
    fun goToDetailsFromMain(githubRepoEntity: GithubRepoEntity)
    fun goToUrl(url: String)
}

As this is code which has no connection to Android, this could now live in your business layer (but preferably your view model layer if your app has that many layers). And the implementation would live on your Android layer.

由于这是与Android无关的代码,因此它现在可以存在于您的业务层中(但如果您的应用程序具有那么多的层,则最好是您的视图模型层)。 实现将在您的Android层上进行。

@SingleActivity
class NavigatorImpl @Inject constructor(
        private val androidPlaygroundActivity: AndroidPlaygroundActivity
): Navigator {


    override fun goToDetailsFromMain(githubRepo: GithubRepoEntity) {
        val action = MainFragmentDirections.actionMainFragmentToDetailsFragment(githubRepo)
        androidPlaygroundActivity.findNavController(R.id.nav_host_fragment).navigate(action)
    }


    override fun goToUrl(url: String) {
        Intent(Intent.ACTION_VIEW, Uri.parse(url)).startActivity(androidPlaygroundActivity)
    }
}

Usually the only requirement of navigation code is that it has an Activity reference. Luckily we added our single activity to the dagger graph earlier so it can be injected into our implementation. And as the Navigator is scoped to the Activity it can only be used inside the subcomponents object graph — so only while the Activity is alive.

通常,导航代码的唯一要求是它具有Activity引用。 幸运的是,我们将我们的单个活动添加到了匕首图中,因此可以将其插入到我们的实现中。 而且,由于Navigator的作用域是Activity ,因此只能在子组件对象图中使用它-仅当Activity处于Activity时才可以使用。

在哪里调用导航器 (Where to call the Navigator)

This is a little more tricky.

这有点棘手。

The simple answer is to inject the Navigator into your fragment and call it from there.

简单的答案是将Navigator注入片段并从那里调用它。

But there is another possibility. Anyone that’s used MVVM in Android will be familiar with one-time actions — there’s a host of medium articles on the subject. For example, the view model wants to display a toast but only wants to display it once, so has to reset view state after the toast is displayed to avoid re-showing the toast after an event such as a screen rotation.

但是还有另一种可能性。 任何在Android中使用过MVVM的人都会熟悉一次性操作-有关该主题的文章很多。 例如,视图模型只想显示一个Toast,但只想显示一次,因此必须在显示Toast之后重置视图状态,以避免在屏幕旋转等事件后重新显示Toast。

As the navigator now uses a similar concept to business logic, and our view models will only exist while an activity exists, our view models can become part of the activity subcomponent, and the navigator can be injected directly into the view model constructor.

由于导航器现在使用与业务逻辑类似的概念,并且我们的视图模型仅在活动存在时存在,因此我们的视图模型可以成为活动子组件的一部分,并且可以将导航器直接注入到视图模型构造函数中。

This means there’s no need to expose one time actions such as navigation through view states. And your view models becomes as simple as:

这意味着无需公开诸如浏览视图状态之类的一次性动作。 您的视图模型变得简单:

fun repoClicked(githubRepoEntity: GithubRepoEntity) {
    navigator.goToDetails(githubRepoEntity)
}
fun onGoToRepositoryClicked() {
    navigator.goToUrl(githubRepoEntity.url)
}

This is now super simple to test or debug, your navigation code is abstracted to a single class, plus no fancy one-time actions are needed in your view states.

现在,这非常易于测试或调试,您的导航代码被抽象为一个类,并且在视图状态中不需要花哨的一次性操作。

The only downfall of this approach is that if you’re using the androidx ViewModel this approach WON’T WORK. The reason being that the ViewModel has a longer lifecycle than an Activity. By using this method you’d be indirectly adding a reference to the Activity within the ViewModel. Therefore on every screen rotation, Activity would leak. There are ways you could get round that, but nothing pretty.

这种方法的唯一缺点是,如果您使用的是androidx ViewModel此方法将无效。 原因是ViewModel的生命周期比Activity更长。 通过使用此方法,您将在ViewModel间接添加对Activity的引用。 因此,在每次屏幕旋转时, Activity都会泄漏。 有很多方法可以解决这个问题,但是没有什么好看的。

So if you’re using ViewModel you’re stuck injecting the navigator into the Fragment.

因此,如果您使用ViewModel ,则必须将导航器注入Fragment

扩展概念 (Extending the concept)

You can even extend the same concept to Toasts, Snackbars, Keyboard listening, and much more. In the final code for this section I’ve solved the bug mentioned at the end of the last article by using this same structure for Toasts. Essentially a whole variety of code in the view layer of an app can follow strong architectural principles and be styled as focused classes of responsibility — all by using the power of dependency injection.

您甚至可以将相同的概念扩展到Toasts,小吃店,键盘监听等等。 在本节的最终代码中 ,我通过使用与Toasts相同的结构解决了上一篇文章结尾提到的错误。 本质上,应用程序视图层中的所有各种代码都可以遵循强大的体系结构原理,并可以将其样式化为重点责任类别-所有这些都可以利用依赖注入的能力来实现。

Image for post

结论 (Conclusion)

Single activity architecture is a step up in Android development, and with the navigation library, converting a codebase to this structure can be a breeze. It also opens up some interesting architecture patterns which now require less boilerplate.

单一活动架构是Android开发中的一大步,借助导航库,将代码库转换为这种结构很容易。 它还打开了一些有趣的架构模式,这些模式现在需要更少的样板。

The ideas in the second half of the article attempt to solve a common problem with Android view code where — even in well architected codebases — many fundamental principles such as SOLID are forgotten and features like navigation are scattered throughout Activities and Fragments.

本文下半部分的想法试图解决Android视图代码的一个常见问题,即使在结构良好的代码库中,也忘记了许多基本原理,例如SOLID ,而导航等功能则分散在整个Activity和Fragments中。

The overriding principle is that just like any other layer of your app, the view layer can be architected as focused classes of composable behaviour. This makes it easy to modify UI down the line, changing from Snackbars to Dialogs to show announcements? A single class can be changed to modify how announcements throughout the app are displayed. Updating to the androidx navigation library? Again, a single class needs modified.

首要原则是,就像应用程序的其他任何层一样,视图层也可以设计为可组合行为的重点类。 这样可以很容易地修改UI,从Snackbars更改为Dialogs以显示公告? 可以更改一个班级,以修改整个应用程序中公告的显示方式。 更新到androidx导航库? 同样,单个类需要修改。

Many parts of your view become implementation details — simple to swap in and out without effecting the rest of your app.

视图的许多部分都变成了实现细节-可以在不影响应用程序其余部分的情况下轻松切入和切出。

On top of this, testing becomes simpler. For standard unit tests too much much responsibility leads to long, hard to write and maintain tests — a code smell. The same holds true for UI tests, by making our view layer composable, UI tests are far easier to write and maintain.

最重要的是,测试变得更加简单。 对于标准的单元测试,太多的责任导致了漫长,难以编写和维护的测试-代码的味道。 UI测试也是如此,通过使我们的视图层可组合,UI测试的编写和维护要容易得多。

I’ve used this approach in a few apps, and I’ve found that it makes developing easier, less code is duplicated, bugs are less common, and most importantly, large UI changes become a breeze. So what do you think? Is this approach useful?

我已经在一些应用程序中使用了这种方法,并且发现它使开发变得更容易,重复代码更少,错误很少见,并且最重要的是,大的UI更改变得轻而易举。 所以你怎么看? 这种方法有用吗?

Final code for this section is here.

本节的最终代码在这里

翻译自: https://proandroiddev.com/part-3-single-activity-architecture-514791724172

架构探险-轻量级微服务架构

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐