
在10月8日至9日举行的AppsConf 2018上,我做了一个关于完全在一个Activity中创建android应用程序的演示。 尽管这个话题是众所周知的,但是对于这种选择还是有很多偏见的-拥挤的房间和演讲后确认问题的数量。 为了不等待录像,我决定写一篇带有演讲稿的文章。

我会说什么
- 为什么以及为什么我应该切换到单一活动
- 一种通用的解决方案,用于解决您在多个活动中要解决的任务
- 标准业务任务示例
- 通常支持代码的瓶颈,而不是诚实地做所有事情
为什么单一活动正确?
生命周期

所有android开发人员都知道该应用程序的冷启动方案。 首先,在Application类上调用onCreate,然后执行第一个Activity的生命周期。
如果我们的应用程序中有多个“活动”(并且此类应用程序占大多数),则会发生以下情况:
App.onCreate() ActivityA.onCreate() ActivityA.onStart() ActivityA.onResume() ActivityA.onPause() ActivityB.onCreate() ActivityB.onStart() ActivityB.onResume() ActivityA.onStop()
这是来自ActivityA的抽象activityB启动日志。 空行是调用新屏幕的时刻。 乍一看,一切都很好。 但是,如果我们看一下文档,它将变得很清楚:为确保用户可以看到该屏幕并且该用户可以与该屏幕进行交互,只有在每个屏幕上调用onResume
后才有可能:
App.onCreate() ActivityA.onCreate() ActivityA.onStart() ActivityA.onResume() <-------- ActivityA.onPause() ActivityB.onCreate() ActivityB.onStart() ActivityB.onResume() <-------- ActivityA.onStop()
问题是这样的日志无助于理解应用程序的生命周期。 当用户仍在里面时,以及当他已经切换到另一个应用程序或最小化我们的应用程序时,等等。 当我们要将业务逻辑绑定到应用程序的LC时,这是必要的,例如,当用户在应用程序中时保持套接字连接,而在退出时将其关闭
在单一活动应用程序中,一切都很简单-LC活动成为LC应用程序。 任何逻辑所需的一切都易于绑定到应用程序的状态。
启动画面
作为用户,我经常遇到以下事实:单击联系人后,不会发生电话簿中的呼叫(这显然是单独的Activity的启动)。 目前尚不清楚这与什么有关,但是我尝试无法接通的那些人说他们接到了电话,听到了台阶的声音。 同时,我的智能手机早已放在口袋里了。

问题在于启动一个Activity是一个完全异步的过程! 无法保证立即启动,更糟糕的是,我们无法控制该过程。 绝对是
在单活动应用程序中,与片段管理器一起使用,我们可以控制该过程。
transaction.commit()
-将异步切换屏幕,使您可以一次打开或关闭多个屏幕。
transaction.commitNow()
-如果不需要将屏幕添加到堆栈中,则同步切换屏幕。
fragmentManager.executePendingTransactions()`允许您立即执行所有先前启动的事务。
屏幕堆栈分析
想象一下,应用程序的业务逻辑取决于屏幕堆栈的当前深度(例如,嵌套限制)。 或者,在某些过程的最后,您需要返回到特定的屏幕,如果有多个相同的屏幕,则返回最接近根的屏幕(链的开头)。
如何获得一个活动栈? 启动屏幕时应指定哪些参数?

顺便说一下,关于Activity启动选项的魔力:
- 您可以在Intent中指定启动标志(也可以将它们混合在一起,并在不同位置进行更改);
- 您可以在清单中添加启动参数,因为所有活动都应在此处进行描述;
- 在此处添加Intent过滤器以处理外部触发;
- 最后考虑一下“多任务”,即“活动”可以在不同的“任务”中运行。
同时,这会导致混乱和支持调试方面的问题。 您永远无法确切地说出启动屏幕的方式以及它如何影响堆栈。
在单活动应用程序中,所有屏幕仅通过片段事务切换。 您可以分析当前的屏幕堆栈和已保存的交易。
在Cicerone库演示中,您可以看到如何在工具栏中显示堆栈的当前状态。

注意:在最新版本中,支持库阻止了对片段管理器内部片段数组的访问,但是,如果您确实愿意,可以始终解决此问题。
屏幕上只有一项活动
在实际的应用程序中,我们绝对需要将“逻辑”屏幕组合到一个活动中,然后您不能仅在活动上编写真实的应用程序。 这种方法的双重性总是很糟糕,因为可以以不同的方式解决同一问题(某个地方的布局直接位于Activity中,而某个地方的Activity只是一个容器)。
不要继续活动
这个测试标志确实允许您在应用程序中发现一些错误,但实际上它不会重现它的行为! 申请流程不会保留,到那时活动(尽管不活跃)就会消失! 活动只能随着申请过程而终止。 如果将应用程序显示给用户,并且系统没有足够的资源,则周围的所有内容都会消失(其他不活动的应用程序,服务甚至启动器),并且您的应用程序将活到最后,如果必须终止,那么它将完全消失。
您可以检查。
旧版
从历史上看,活动中有大量不必要的逻辑,这些逻辑很可能对您没有用。 例如,使用loaders
, actionBar
, action menu
所需的一切。 这使班级本身变得相当庞大和沉重。
动画制作
也许,在“活动”之间切换时,任何人都可以制作简单的移位动画。 在这里有必要澄清一下,您需要对我们之前提到的Activity启动的异步性进行折扣。
如果您需要一些更有趣的东西,可以回想一下在Activity上制作的过渡动画的示例:

但是有一个大问题:定制该动画几乎是不可能的。 这不太可能取悦设计师和客户。
使用片段,一切都不同。 我们可以直接进入视图层次结构,并制作您可以想象的任何动画! 直接证据在这里 :

如果查看源代码,您会发现这是在常规布局上完成的。 是的,那里的代码很不错,但是动画总是很困难,拥有这样的机会永远是加号。 如果您切换了两个“活动”,则该应用程序没有可用于进行此类转换的公共容器。
即时配置
这一点不是我的发言,但也很重要。 如果您具有在应用程序内切换语言的功能,那么通过多个活动将很难实现它,尤其是您不需要重新启动应用程序,而是将其停留在用户调用功能时的原处。
在Single-Activity应用程序中,只需在应用程序上下文中更改已安装的语言环境并在Activity上调用recreate()
,系统的其余部分便会自己完成所有事情。
最后
Google有一个导航解决方案,其文档明确指出建议编写Single-Activity应用程序。
在这一点上,我希望您毫无疑问的是,带有多个Activity的经典方法包含许多缺点,习惯上对此视而不见,并且隐藏了Android不满情绪的总趋势。
如果是这样,那么为什么单项活动还不是开发标准?
在这里,我将引用我的好朋友:

开始一个新的严肃项目时,任何领导都会害怕犯错误并避免冒险的决定。 这是正确的。 但是,我将尝试为过渡到单一活动提供一个全面的计划。
切换至单一活动

如果您研究此应用程序,则可以从特征动画和行为中确定它是写在几个Activity上的。 我可能是错的,即使在自定义视图上也已完成所有操作,但这不会影响我们的推理。
现在注意! 我们这样做:

我们仅进行了两项更改:添加了AppActivity类,并用FlowFragment替换了所有Activity。 更详细地考虑每个更改。
AppActivity负责什么:
- 仅包含碎片的容器
- 是Scope UI对象的初始化点(它曾经在Application中完成,这是错误的,因为例如,我们的应用程序中的Service对象肯定不需要此类对象)
- 是应用程序提供商
- 带来了单一活动的所有好处。
什么是FlowFragment :
新导航
与旧方法的主要区别在于导航。

以前,开发人员可以选择:在当前活动中启动新的Activity或片段事务。 选择尚未消失,但方法已更改-现在我们需要确定是在AppActivity中还是在当前FlowFragment内部启动片段分段。

与“返回”按钮的处理类似。 以前,Activity将事件传递给当前片段,如果不对其进行处理,它将自行做出决定。 现在,AppActivity将事件传递给当前的FlowFragment,然后将其传递给当前的片段。
在屏幕之间传输结果
对于没有经验的开发人员,在屏幕之间传输数据的问题是新方法的主要问题,因为更早的时候可以使用startActivityForResult()功能!
不是第一年,已经讨论了编写应用程序的各种体系结构方法。 同时,主要任务仍然是将UI与数据层和业务逻辑分离。 从这个角度来看,由于一个应用程序的屏幕之间的数据是在UI层的实体侧传输的,因此startActivityForResult()打破了常规。 我强调这只是一个应用程序,因为我们有一个通用的数据层,一个全局范围内的通用模型,等等。 我们不会利用这些机会,而是将自己置身于一个捆绑包(序列化,规模等等)的框架中。
我的建议 :不要在应用程序内部使用startActivityForResult()! 仅将其用于预定目的-运行外部应用程序并从中获取结果。
然后如何启动一个可以选择另一个屏幕的屏幕? 共有三个选项:
- 目标片段
- 事件总线
- 喷气模型
TargetFragment-选项“开箱即用”,但在UI层的侧面进行相同的数据传输。 不好的选择。
EventBus-如果您可以就团队达成一致意见,并且-最重要的是-控制安排,那么您可以在全局数据总线上的屏幕之间实现数据传输。 但是由于这是一个危险的举动,因此得出结论是一个错误的选择。
响应式模型-这种方法意味着存在回调等。 如何实现它们取决于每个项目的团队。 但是这种方法是最佳的,因为它可以控制正在发生的事情,并且不允许将代码用于其他目的。 我们的选择!
总结
当新方法简单且有明显好处时,我会喜欢它们。 我希望在这种情况下就是这种情况。 好处在第一部分中进行了描述,您应该判断难度。 用FlowFragment替换所有Activity就足够了,保持所有逻辑不变。 稍微更改导航代码,并考虑处理屏幕之间的数据传输(如果尚未完成的话)。
为了展示这种方法的简单性,我本人将开放式应用程序切换为Single-Activity,仅花费了几个小时(当然,值得考虑的是这不是古老的遗产,那里的体系结构或多或少都很好)。
怎么了
让我们看看如何用新方法解决标准问题。
BottomNavigationBar和NavigationDrawer
使用将所有Activity替换为FlowFragment的简单规则,侧面菜单现在将位于某些片段中,并在其中切换嵌套片段:

与BottomNavigationBar相似。
可以将一些FlowFragment投资于其他片段,这更有趣,因为它们仍然是普通片段!

可以在GitFox中找到此选项。
可以简单地将某些片段组合在其他片段中,从而可以为不同设备(平板电脑和智能手机)创建动态UI而没有任何问题。
DI范围
如果您有多个屏幕上的产品购买流程,并且需要在每个屏幕上显示产品名称,则可能已经将其放置在一个单独的活动中,该活动存储产品并将其提供给屏幕。
对于FlowFragment来说将是相同的-它将包含一个带有所有嵌套屏幕模型的DI缩放比例。 通过将它与FlowFragment的生命周期绑定在一起,该方法消除了对示波器生命周期的复杂控制。

深层连结
如果您在清单中使用过滤器来启动深层链接上的特定屏幕,则启动我在第一部分中提到的活动可能会遇到问题。 在新方法中,所有深层链接都属于AppActivity.onNewIntent。 此外,根据获得的数据,可以转换到所需的屏幕(或一系列屏幕。我建议在Chicheron中查看这种功能 )。

进程死亡
如果应用程序是在多个Activity上编写的,则应该知道当应用程序死亡时,然后在恢复过程时,用户将位于最后一个Activity上,而所有先前的Activity仅在返回给他们时才被还原。

如果不事先考虑,可能会出现问题。 例如,如果在上一个活动上打开的上一个活动所需的作用域,则没有人会重新创建它。 怎么办 将此带到Application类吗? 多点开放范围吗?
使用片段,一切都变得更加简单,因为它们都在一个Activity或另一个FlowFragment中,并且在重新创建该片段之前,任何容器都将被还原。

我们可以在评论中讨论其他实际任务,否则可能会导致文章过于冗长。
现在是最有趣的部分。
瓶颈(您需要记住并思考)。
这里收集了您在任何项目中都应该考虑的重要事项,但是每个人都习惯于在多个活动的项目中“累加”它们,值得回顾一下,并告诉他们如何使用新方法正确解决它们。 首先在名单上
屏幕旋转
这是抱怨球迷最可怕的故事 ,当屏幕旋转时,Android会重新创建Activity。 最受欢迎的解决方法是固定人像方向。 而且,该提议不再由开发人员提出,而是由对诸如“ 保持转弯非常困难且成本要高出几倍 ”之类的措辞感到恐惧的管理人员提出。
我们不会争论这种决定的正确性。 另一件事很重要: 修复轮换并不能免除Activity的死亡! 由于其他许多事件也会发生相同的过程:拆分模式,当屏幕上显示多个应用程序时,连接外部监视器,即时更改应用程序配置,等等。
此外,屏幕的旋转允许您检查布局的正确“橡胶性”,因此,在我们的圣彼得堡团队中,即使不是销售版本,我们也不会关闭所有销售装配中的旋转。 更不用说在验证期间仍然会发现的典型错误。
从Moxy到各种MVVM实施结束,已经为转弯编写了许多解决方案。 使它变得比其他任何事情都难。
考虑另一个有趣的情况。
想象一个产品目录应用程序。 我们在单一活动中进行。 人像模式在任何地方都是固定的,但是当用户在查看图库时可以以横向观看它们时,客户想要一个功能。 如何支持呢?
有人会提供第一个拐杖 :
<activity android:name=".AppActivity" android:configChanges="orientation" />
override fun onConfigurationChanged(newConfig: Configuration?) { if (newConfig?.orientation == Configuration.ORIENTATION_LANDSCAPE) {
因此,我们不能调用super.onConfigurationChanged(newConfig)
,而是自己处理它并仅旋转屏幕上必要的视图。
但是使用API 23时,该项目将因SuperNotCalledException
而崩溃,因此是一个错误的选择 。
上面的陈述犯了一个错误:
在评论中,我得到了合理的纠正,即添加android:configChanges =“ orientation | screenSize”就足够了,然后您可以调用super,并且旋转时不会重新创建Activity。 当WebView或地图显示在屏幕上时,使用它很有用,这需要很长时间进行初始化,并且您要避免这种情况。
这将有助于解决与画廊有关的情况,但本节的主要内容是: 不要忽略Activity的重新创建 ,这在许多其他情况下都可能发生。
有人可能会建议另一种解决方案:
<activity android:name=".AppActivity" android:screenOrientation="portrait" /> <activity android:name=".RotateActivity" />
但是通过这种方式,我们摆脱了单一活动方法来解决一个简单的问题,并使自己丧失了该方法的所有好处。 这是拐杖,而拐杖总是一个坏选择 。
这是正确的解决方案:
<activity android:name=".AppActivity" android:configChanges="orientation" />
override fun onResume() { super.onResume() activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR } override fun onPause() { super.onPause() activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT }
也就是说,打开片段时,应用程序开始“旋转”,返回时,它将再次修复。 以我的经验,这就是AirBnB应用程序的工作方式 。 如果打开包含照片的视图,则会激活转向处理,但是在横向方向上,您可以将照片向下拖动以退出图库。 在它下面,上一个屏幕将以横向显示,而通常不会找到,因为离开画廊后,该屏幕会立即变成人像并固定。

在此及时进行屏幕转向准备会有所帮助。
透明状态栏
只有“活动”可以与系统栏一起使用,但是现在只有一个,因此您应该始终指定
<item name="android:windowTranslucentStatus">true</item>
但是在某些屏幕上,无需在其下“爬网”,并且需要在下面显示所有内容。 旗帜来了
android:fitsSystemWindows="true"
指示您不应在系统栏下方绘制的布局。 但是,如果您在片段的布局中指定了片段,然后尝试在片段管理器中通过事务显示片段,那么您将感到失望...它将无法正常工作!
答案很快是谷歌
我强烈建议您熟悉一个真正全面的答案和许多有用的链接。
一种快速且可行( 但不正确 )的解决方案是将布局包装在CoordinatorLayout
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> </android.support.design.widget.CoordinatorLayout>
更好的解决方案也有助于处理键盘。
键盘出现时更改布局
当键盘离开时,布局应更改,以使UI的重要元素不会超出范围。 而且,如果以前我们可以为不同的活动为键盘指定不同的反应模式,那么现在我们需要在“单一活动”中执行此操作。 因此有必要使用
android:windowSoftInputMode="adjustResize"
如果使用上一节中的方法来处理透明的状态栏,则会发现一个不幸的错误:如果片段成功爬过了状态栏,那么当键盘出现时,它将在上方和下方收缩,因为系统内部的状态栏和键盘会一直通过SystemWindows
。
注意标题

怎么办 阅读文档!并且一定要看到Chris Banes谈论WindowInsets。
使用WindowInsets将允许
- 找出正确的条形状态高度(而不是51dp硬代码)
- 为新智能手机屏幕上的所有切口准备应用程序
- 找出键盘的高度(这是真的!)
- 接收事件并响应键盘的外观。
每个人都学习WindowInsets!
开机画面
如果其他人不知道,那么规范的启动屏幕不是应用程序中加载数据的第一个屏幕,而是用户在启动时看到的内容,直到活动内容有时间渲染为止。关于这个主题有很多文章。

, Single-Activity, Splash screen. , deep-link Splash screen .
, , , .
, . Single-Activity. - , , .
...
Intent, , ...
接下来是什么? :
, . ? — «» «» .
怎么办 , .
Activity!
, : , — .
— , ( Activity), .
Activity — . Activity, . .

结论
() , Activity, Android-. , .
: Google . — , , Activity .
, , , ! 谢谢你