当您参与“抢先体验计划”时,您永远不会事先知道最终会发生什么。 当然,您希望该技术能够腾飞,您的应用程序将领先于市场,并在Google I / O上获得一定的PR。 这是在初始阶段阅读源代码而不是文档的良好动机,此外,还必须从机密档案中下载文档。
在
AppsConf上, Evgeny Saturov展示了通过开发具有即时启动功能的应用程序可以导致参与Early Access,并解释了Google Play Instant的所有功能。 在解读他的报告时,我们将弄清Android App Bundle来自何处,以及Dynamic Delivery与它有什么关系,了解新的Gradle插件,并了解如何处理SDK开发人员为我们准备的惊喜。
关于演讲者: Evgeny Saturov(
saturovv )在Surf公司工作,该公司
专门从事自定义Android和iOS开发,最近是Flutter的开发。 Eugene Flutter是FlutterDevPodcast的爱好者和创始人。
短暂的历史之旅
两年前,一个很棒的人到Surf来找我们,他说:“我们拥有的技术到目前为止还很少有人知道。 并且您有客户和有趣的案例。 让我们为您提供我们的技术,将其集成到某个地方,您会得到很酷的共生。 我们将在Google I / O上推广这一做法,每个人都会没事的。”
实际上,
与“抢先体验计划”一起工作真是无济于事 。 您必须使用原始代码,而原始代码当然并不总是按预期和描述的方式工作。 在我们的情况下:
- 所有工件均以ZIP存档的形式提供,必须每隔几天从超级秘密存储中下载一次,并只能在本地计算机上手动进行更新。
- 您只能在“金丝雀”中工作。
- 几乎没有文档,只有分散的非结构化Google文档,通常类似于开发人员的思想流程。
- 当然,不可能在食品中进行部署-只有在技术公开发布后才有可能。 也就是说,整个部署仅在alpha轨道中。
- 公开发布后,可能会发现该SDK已完全重写,而没有向后兼容性支持。 您将获得完全不同的接口和API-您需要重新做所有事情。
这是我们如何参与“抢先体验计划”的简短摘要。
这一切都归功于我们的常规合作伙伴-Labyrinth公司-俄罗斯最大的书籍和文具在线商店。 他们加入了该项目,甚至不确定最终是否会有任何好处。 而且,在2017年,Surf作为工作室成为Google认证代理计划的一部分。 不幸的是,该计划于今年关闭。
Android Instant Apps
在2017年,为了使该应用程序无需安装即可运行,必须正确地将其锯成模块。

我们有新的Gradle插件来生成相应的工件:
- Instant App Module-一个应用程序插件,生成带有APK的ZIP存档,每个应用程序功能模块一个;
- 可安装的应用模块-生成的APK。
来自功能模块的两级层次结构已经出现。 永远只能有一个基本功能模块,并且包含可用于所有功能,资源,依赖项等的所有基本代码。 顶级功能模块包含特定屏幕的实现。
但这
又长又昂贵 ,原因有很多:
- 刚性模块化结构本身是一个很大的限制。 很难想象,任何大型公司都将在数年之内将其正在开发且运行良好的应用程序变成一堆吸烟模块,只是为了支持一项尚不为人所知的新技术。
- 对程序集大小的严格限制,即一项功能不应超过4 MB。 这是一个客观上很小的限制,有时甚至是完全无法达到的限制,例如,由于严重的依赖性或类似原因。
- 确保使用AppLink,因为这是进入Instant App的唯一方法。 用户单击邮件,信使,搜索结果中的链接,并通过拦截链接进入您的屏幕。
- 最后,大量的技术限制。 最初,甚至不可能使用NDK。 您无法发送推送通知,接收敏感数据,更改设备设置以及启动后台进程。 无法使用服务,广播接收器和内容提供者。 仅活动。
Instant App的主要任务只是UI的演示,仅此而已。 重构非常痛苦,因此在生产中仍然几乎找不到这个故事。
但公平地说,在迷宫中实施Instant App后,通过移动客户端购买的商品增加了5%。
2019. Google Play即时
两年过去了,在2019年-即时应用程序仍然存在,但还不属于独立技术。 它在生产中仍然很少见,我没有故意找它,但我只知道几个例子:Sports.ru,Vimeo。 Google宣布这项技术时,不太可能指望这种结果。
现在,即时应用的名称有所不同-Google Play即时。 名称的更改有助于清除不相关的文档。 如果您看到了Android Instant App,则可以立即清楚这不再相关。
除了名称之外,其他所有内容都发生了变化,包括模块化结构。
需求变得更加忠诚 。 该技术以完全不同的方式集成到项目中,不需要繁琐的重构,这当然是很好的。
但是,不那么明显,在我看来,更重要的是,这项技术仍然非常小众和稀有,已经成为整个技术家族的先驱,而现在它已成为其中的一部分。
最初,Google将Instant App定位为一种吸引人们业务而不是应用程序的技术。 有一个设计指南仅用一个按钮“下载完整的应用程序”就禁止着陆应用程序。 但是另一个根本性的问题得到了侧面解决,我怀疑这是偶然发生的。
今年2月,机载1 TB内存的非凡设备Samsung Galaxy S10 +亮了起来。 只需考虑一下-1 TB! 为什么你需要那么多?
根据Google的官方统计,过去7年中,APK的平均大小增加了5.5倍。

构建规模确实很重要,
对这些统计数据的
研究表明:
- 每增加6 Mb的组件,安装转换率就会降低1%;
- 70%的用户在下载前检查应用程序的大小;
- 50%的用户对应用程序在安装后将在设备上占用多少空间感兴趣。
如果您的目标受众是年龄较大或收入不高的人群,或者这些人群是新兴市场,这一点就显得尤为重要。
后者越来越难以忽视,因为在2018年,印度的安装量激增。

请注意,iOS很少,Android应用程序的安装总数超过了美国,巴西和印度尼西亚的总和。
现在很清楚,Google Play Instant可以解决什么问题。 您可以使用难以理解的条款。
Android应用程式套件
Android App Bundle-一种在Google Play上发布应用程序的新格式。 在内部,所有内容与APK并没有太大区别:所有相同的dex文件,清单,资源,资产等。 等 但是,还有元数据,它不会到达用户的设备。

元数据由三个文件表示:resources.pb,assets.pb,native.pb。 实际上,这些是设备的组装和配置设置中的资源对应表。
Android动态交付
所有人都知道Google Play存在应用签名功能。 但是并不是每个人都准备好将其应用程序的发行密钥存储在Google Play上,因为不会回头。
无法通过Google Play下达App Signing。一旦将释放密钥提供给Google Play,您就再也无法像以前那样对应用程序进行签名。 但是,为此,您可以充分利用Android App Bundle作为一种格式。 现在,程序集签名过程看起来会有所不同。

在Google Play上发布该程序集之前,您仍将对其进行签名,但是您将使用不唯一的上载密钥对其进行签名。 可以从控制台中调用它,如果它被破坏或丢失,则可以重新发布。 您将释放密钥交给控制台,然后告别它-Google现在将为您签名程序集并发誓它将确保密钥安全。
但是,如果您不将发布密钥提供给Google Play,那么您将无法在项目中使用任何将进一步讨论的内容。 Google拧紧了螺丝,即使在不放弃密钥的情况下,即时应用程序现在也将无法部署。
实际上,这一点都不好笑,因为购买了Instant App促销并了解整个故事的人们重构了他们的应用程序,但是由于某些原因,他们无法将其密钥提供给Google Play(或者安全部门坚决反对或出于其他客观原因)发现自己无法再支持该决定。 实际上,数百小时的工作被扔进了垃圾箱。
2014.拆分APK支持出现在Android Lollipop中
由于我们今天怀旧,因此我们将更早回到过去-2014年。
我仍然记得在世界上最好的手机NEXUS 5上,Android Lollipop组件是如何以令人难以置信的材料设计飞来飞去的。 但是有些更改并未引起很多人的注意-这是对Split APK的支持。
拆分APK-一种机制,可让您将应用程序拆分为多个小APK,并在一个设备上安装后,使其表现为单个应用程序。
记住这一点,继续前进。
Android Dynamic Delivery是Google Play上的一种新的应用程序分发格式。

我们曾经有一个APK,现在出现了Android App Bundle,以作为替代。 AAB充当了这些非常拆分的APK的孵化器生成器。 AAB推出了APK的包子,然后通过并行安装将其用作常规应用程序。
让我们看看它是哪种APK。

至少,这是基本的APK,它与Instant App中的角色相同:这是在所有功能之间翻阅的基本代码,基本资源和业务逻辑。
同时出现:
- APK,其名称可疑地类似于图形资源修饰符前缀(图中的第一行)。
- 另一个APK系列使我们想起了处理器架构。
- 本地化APK。
大约两年前,当矢量图形不那么普及时,许多应用程序主要包含光栅资源,这些光栅资源被切割为屏幕上不同的像素密度。 组装可能会变得很重。 用户下载了所有这些资源,其中大部分资源都以无负担的方式存储在设备上。
现在,这种机制允许用户仅接收专门为其设备所需的资源集。 一个人来到Google Play,选择应用程序,Google Play会了解用户设备的特征,并提供所需的一组拆分APK-每个类别中都有一个APK。
动态投放类型APK:
- 一个且只有一个Base APK。
- 配置APK最多有以下三种类型: res * x,资源* y,lib * z。 此处:x是使用的资源修饰符的数量; y是所使用的体系结构类型的数量; z是语言本地化的数量。 例如,如果项目不使用本机代码和本机库,则将没有与本机代码相关联的类别,并且将保留两个APK。
- 无限动态功能APK。
动态功能APK将在下面更详细地讨论。 但首先,我们将告别Split区块。
再见拆分
您可能会争辩说,以前可以做类似的事情,只用正确的资源手动生成一堆APK,然后全部手动部署到Google Play。
android { splits { density { enable true exclude "ldpi", "xxhdpi", "xxxhdpi" compatibleScreens 'small', 'normal', 'large', 'xlarge' } } }
这种冒险本身是相当可疑的,现在所有这些都被忽略了。 如果您构建Android App Bundle,则会出现Bundle块,该块可让您手动禁用拆分为类别之一的项目。
android { bundle { language { enableSplit = false } density { enableSplit = true } abi { enableSplit = true } } }
您可以指定应用程序仅支持例如俄语或仅英语,并在组装过程中跳过此步骤。
现在,最细心的人可能正在考虑如何使用preLollipops。 仅在Android 5中出现了对Split APK的支持。 我们必须以某种方式摆脱这种局面,因为Min SDK绝不是全部21。
对于preLollipops,情况相当笨拙,但只有可能。 Google Play为preLollipops收集了多个APK,其中包括配置APK的各种组合。 只有一个APK,但有很多选项。
Android App Bundle改变了我们的生活
而且非常重要。 首先,构建项目可能会容易得多,尤其是如果您曾经手动构建Split APK。 但是根据我的观察,这样的人并不多。
其次,您不再承担丢失或破坏发布密钥的风险。 不会有大的悲剧,如果您丢失了上传密钥,则可以撤回并重新发行它。
我们不会指责-在w3bsit3-dns.com上,俄语Google Play的顶部有很长一段时间具有释放密钥的应用程序,所有自定义程序集都用释放密钥签名,并且在接下来的五年中将无法进行任何操作。 它仍然等待着到只有28个API出现的Signing V3过渡。
Android App Bundle的优势无疑是:用户停止在不需要的资源上花费流量和磁盘空间。 这大大提高了应用程序的保留率。
但是,如果您在矢量中拥有所有图形,两个本地化并且没有本地库,那么好处将是微不足道的。
动态功能模块
动态功能模块是在安装应用程序时不提供的功能模块,而是从Google Play下载并仅按需安装的功能模块。
此类模块与基本APK相当。

这些功能模块本身每个都还包含一组配置拆分APK,这一点很重要。 因此,APK的总数可能会增加而无法衡量。 但这不是您所关心的,Google Play正在这样做。
动态功能模块的应用:
极少数观众使用的功能 ,但对您的产品而言仍然很重要。 例如,这是一个内容娱乐应用程序,其95%的用户消费内容。 但是,只有极少数的编辑器生成内容。 对于他们来说,有一个很酷的视频编辑器,它的确占了很大的比重,工作起来非常酷。 然后,为每个人和每个人加权装配都是没有意义的,您可以将此功能放入动态功能模块中,并仅将其提供给需要的人,然后再下载。
与主要应用程序使用情况无关的重要功能。 例如,地图服务中的AR导航。 任何AR功能最多都可以放在动态功能模块中。
用户无需安装应用程序本身即可使用的功能 (例如,在目录中选择产品并下订单)。 没错,这是所有事物的可疑之处。
Android Instant Apps现在是即时启用的动态功能模块。
因此,事实证明有两种类型的动态功能模块:
- 通用动态功能模块是与主要应用程序分开下载并安装在设备上的功能模块。 只要应用程序本身存在,它们就一直在那里。 在删除它之前,将使用动态功能模块。
- 即时启用的动态功能模块-无需安装在设备上即可运行的功能模块。 该模块的寿命有限。
Google Play上的第二种应用程序可以通过“尝试”按钮的存在来区分。 当您单击它时,将替换默认URL,并且至少可以看到该应用程序的外观作为一个主要功能的示例。
通常可以在游戏部分找到它。 这比应用程序更方便,甚至更适用于应用程序,因为您可以下载游戏的一小部分作为演示并查看其全部内容,无论是否值得花费您的流量和时间。
模块化结构
我已经说过,模块化结构已经简化。 让我们看看如何。
最初,我们有一个相当可怕的结构。 除其他外,它具有空模块,例如,Instant App模块始终为空,没有代码,没有资源,只有build.gradle文件,仅此而已。
开发人员认为,为什么随后仅为了收集某种类型的工件而产生额外的模块。 他们看到了,功能转移到了App Module。
但是随后他们走得更远,开始思考-为什么我们需要基本功能模块? 他有一些问题,因为我们在那里初始化了所有东西,并且他的应用程序ID不同。 拐杖来自这里,例如,将应用程序ID从应用程序模块传输到基本功能模块,并将其替换为应用程序的真实应用程序ID,以便所有内容都能在Crashlytics中进行跟踪等。
结果,事实证明是这样。

他们离开了附加在其功能模块顶部的应用模块-就是这样!
当我发现这一点时,不要表达自己的情感。 大舞台上的家伙说,他们拥有一项很酷的技术:“让我们将所有应用程序都削减到这种结构!” 但这不能在5分钟内起作用,但是会带来很大的后果。
在2017年,拥有整体应用程序还不是一件可耻的事情。 迷宫就是这样,然后它甚至还没有达到公开发布,而是处于测试阶段。 在我们参与“抢先体验计划”时,那里已经有大约90个屏幕。 我们花了另外两个月的时间来重构,测试并确保所有内容都能正常运行。
然后他们说:
“我们太聪明了,可以做得容易得多 。
”但是回到散文。
摇篮配置
为了支持新配置,我们首先需要在Android模块中的app模块的build.gradle文件中列出所有Dynamic Feature Modules:
之后,在每个动态功能模块的build.gradle文件中,将依赖关系写入应用模块:
原则上,没有什么复杂的。 但是,还有清单的配置。
清单配置
在应用程序模块清单中,我们可以设置true标志,以指示此应用程序中至少有一个即时启用的功能:
如果不存在此标志,则无法将其嵌入Google Play上的相应曲目中。
此外,每个动态功能模块还分别具有清单配置,其中有更多设置:
前两个标志是互斥的,因为
onDemand
是通常的基本功能,而
instant
是相同的Installable功能。
标题-模块的技术名称,稍后在我们的应用程序中对其进行硬编码后,根据该名称,我们将从Google Play中抽取该模块。
include
参数是preLollipops的参数。 如果将其设置为false,则preLollipops用户将永远不会看到此功能,也将无法使用它。
Gradle项目配置
Instantapp插件和功能插件的寿命很短,但充满活力。 他们持续了不到两年。 自今年三月以来,不再支持它们。
现在我们只使用一个动态功能模块:
apply plugin: 'com.android.dynamic-feature'
仅在应用程序模块中进行项目配置
重要一点:有关签名(签名配置),程序集(ProGuard配置),versionCode和versionName的所有设置仅需在build.gradle应用程序模块中进行。
否则,它们将被忽略。 避免在动态功能模块的build.gradle文件中指定任何这些配置块。
Google即时播放
现在我们有以下内容。
模块化结构的要求已尽可能简化。 对于那些尚未参与的人来说,这确实是个好消息。 即使您的应用程序具有自己的结构,现在也可以尝试。 这完全不会影响您,您只需将模块附加在顶部,一切就可以正常工作。
对装配尺寸的限制变得更加忠诚。 如果之前是4 MB,现在:
- 动态功能模块的大小通常不受限制;
- 即时启用的动态功能模块最多可能占用10 MB。
但是现在有了一个渐进的规模。

如果您的功能:
- 超过10 MB,对不起;
- 从4到10 Mb-仅可通过Google Play中的“尝试”按钮进行访问;仅此而已;
- 少于4 MB-可以使用所有将用户吸引到Instant-Enabled模块的方法(通过广告,链接,消息等启动)。
出现了一种用于加载模块的机制-Play Core API。 很少有人知道Instant App以前是通过Chrome安装的。
@Override public boolean maybeLaunchInstantApp(Tab tab, String url, String referrerUrl, boolean isIncomingRedirect) { if (tab == null || tab.getWebContents() == null) return false; InstantAppsHandler handler = InstantAppsHandler.getInstance(); Intent intent = tab.getTabRedirectHandler() != null ? tab.getTabRedirectHandler().getInitialIntent() : null; if (isIncomingRedirect && intent != null && intent.getAction() == Intent.ACTION_VIEW) { Intent resolvedIntent = new Intent(intent); resolvedIntent.setData(Uri.parse(url)); return handler.handleIncomingIntent(getAvailableContext(), resolvedIntent, ChromeLauncherActivity.isCustomTabIntent(resolvedIntent)); } else { ... } return false; }
这是来自Android版Chrome的真实代码,它拦截了指向您的Instant App的链接,转到了Google Play并从那里替换了Instant Apps。 如果检测到Instant App,它将以某种方式打开活动,然后从中启动Instant App。
因此,滚动此功能存在很大的问题。 对于三星,这是一个完全不同的故事,我怀疑他们的内置浏览器具有比Chrome稍微更多的特权。 Instant App直到最后一个都没有在那里工作。
Play Core Library 让您无需担心传送问题。 您只需将其作为包装插入:
implementation 'com.google.android.play:core:1.4.0'
并从Google Play下载所需的功能模块。
该库具有相当实用的语法,可让您一次下载一个模块:
val splitInstallManager = SplitInstallManagerFactory.create(context)
或立即打包几包:
val request = SplitInstallRequest .newBuilder() .addModule("feature1") .addModule("feature2") .build()
挂断听众:
splitInstallManager .startInstall(request) .addOnSuccessListener { sessionId -> ... } .addOnFailureListener { exception -> ... }
在用户界面中显示此内容,并对已安装模块的事实做出反应。
Play Core Library是一个不错的开始:
- 最后,您可以停止面对Google Chrome浏览器。
- 您可以按照自己的方式演示如何在UI上加载模块。
- 可以批量加载功能模块。
- 有一种方法可以灵活地处理在下载和安装功能模块时发生的错误。
- 安装该功能后,您甚至不需要重新启动应用程序,因为有
SplitCompat.install()
-对其进行调用,您可以立即从新安装的功能中立即访问类。
但是,如果我说Play Core Library不会让您受苦,我会欺骗您:
- 代码被混淆,没有任何形式的文档记录。
- 来自所有活动加载会话的事件都到达一个
SplitInstallStateUpdatedListener
您需要对其进行手动排序。 必须将会话ID提前保存在某个地方,这最终会导致代码不是很漂亮。 - 笨拙的冗余状态管理和错误处理:9个可能的状态,10个可能的错误。 这些错误和状态的组合可以用不同的方式解释,所有状态和错误都以int返回。
- 无法在本地设备上正常测试下载和安装功能的可能性-这根本是不可能的。
唯一的选择是将您在Google Play上的版本嵌入到内部测试频道中,然后尝试一下。 如果发现某种错误,请重新开始整个过程,部署更新,然后再尝试。 如果要检查它在没有构建的情况下是如何工作的,则会出现错误“ -2”,并且只能猜测它的含义。 这是Google动态功能的官方示例在存储库中最常见的问题。
在功能模块之间导航没有良好的实践。 在官方示例中,它们显示了功能模块之间如何发生交互,它们建议解决问题的方法非常简单:打开从一个模块到另一个模块的依赖关系。 每个方向都有自己的依赖性,为什么要回来?

但是,随后拆分为模块的所有好处就消失了。 在解决此类问题的需求很久之前,我们就切换到了模块。 他们这样做的部分原因是,它为缺乏经验的开发人员简化了代码库的工作,由于缺乏经验,开发人员可以将signing-activity和base-activity一起使用-然后寻找它进行代码审查。
如果您将所有具有上下依赖关系的模块连接起来,那么只有Google可以从中受益,而不必考虑导航方面的任何问题。
我们考虑了很长一段时间后,终于找到了解决方案,但仍然让我不寒而栗。 它称为
Class.forName
使用完整的
ClassPath
实例化一个类,该类通过从其路径中重命名任何包的类等来中断。
abstract class ActivityCrossFeatureRoute { override fun prepareIntent(context: Context): Intent? { try { return Intent(context, Class.forName(targetClassPath())) } catch (e: ClassNotFoundException) { Logger.e("Activity with the following classpath was not found in the current " + "application: ${targetClassPath()}. If this activity is the part of Dynamic Feature, " + "please check if this Dynamic Feature is downloaded and installed on the device" + "successfully.") } return null } }
在我进入
Plaid应用程序存储库并看到他们以相同的方式解决了导航问题并建议每个人都在Stack Overflow上解决该问题之前,我感到很ham愧。
格子是最酷的展示案例之一,Google会尝试使用它们的所有最新趋势,出色的动画,设计技巧,最新的UI组件,尤其是模块化。
很多,实际上是很多错误,工程不足,粗糙。 您无需长时间搜索错误。
无需像这样写
R.string.primaryColor
,而每次在代码
ru.appname.package.R.string.primaryColor
都需要这样写。 以另一种方式,该构建将无法正常工作,这是
指向官方问题跟踪器的
链接 。
- 在O +设备上运行JobScheduler的问题。 官方的解决方法是手动启动
TestJobSchedulerService
:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Intent serviceIntent = new Intent(this, TestJobSchedulerService.class); startService(serviceIntent); }
- 连接第三方库(例如Firebase,Fabric,AndroidX-packages等)时冻结冻结错误。
它们都在清单中使用占位符应用程序ID。 应用程序ID错误地插入在那里,因此清单不会冻结,Android Studio无法找到默认活动,并且您一直在寻找问题,直到您在官方问题跟踪器中找到
建议为止-重新定义所有提供程序并在此处指定您的应用程序ID。
<provider android:name="com.crashlytics.android.CrashlyticsInitProvider" android:authorities="ru.app.name.crashlyticsinitprovider" tools:replace="android:authorities" /> <provider android:name="com.google.firebase.provider.FirebaseInitProvider" android:authorities="ru.app.name.firebaseinitprovider" tools:replace="android:authorities" /> <provider android:name="androidx.core.content.FileProvider" android:authorities="ru.app.name.fileprovide" tools:replace="android:authorities" />
假设两个功能模块依赖于一个第三方库。 但是您不能建立直接依赖关系,因为您得到:
org.gradle.api.GradleException: [:feature1, :feature2] all package the same library [com.lib.Name:VeryGoodLib]
。
官方回答 :如果在这些模块中指示不同版本的库,该怎么办?
开发人员建议:将功能模块添加到层次结构中,其中仅依赖第三方库,而使其依赖于两个源功能模块。
- attachBaseContext()中的InstantApps.isInstantApp(上下文)的调用缓存。
Android开发人员文档中
的代码示例崩溃,仅仅是因为它们提供了
attachBaseContext()
来通过
this
访问上下文,如果
context==null
,您甚至无法检查InstantApps是否正在运行。
override fun attachBaseContext(base: Context) { super.attachBaseContext(base) if (!InstantApps.isInstantApp(this)) { SplitCompat.install(this) } }
- 使用
Groups
和Barriers
时, ConstraintLayout
显示ConstraintLayout
。
想象您经历了所有事情:组装了程序集,猜测它需要嵌入到Google Play的测试通道中,而Google Play并未显示红色对话框。 用颤抖的手,将此装配件安装到设备上,安装功能部件,然后转到屏幕-您会看到整个布局都聚集在左上角的一堆中。 这是因为,如果您使用
Groups
和
Barriers
,则有一个很棒的
getPackageName
方法,由于某种原因它会错误地出现。 结果,您的所有视图都无法定位。 所有的约束飞散,一切都随意地放在屏幕上。
最后,克服这些困难后,您会发现...
动态功能仍处于测试阶段! 您不会陷入生产困境-一直以来一直是Google的免费测试人员!

但谷歌却没有。 如果您想成为一个人,则可以填写
兴趣表 ,也许您会很幸运,并且可以进入制作流程。
相信 Instant Apps输掉了这场战斗。 未来几年不太可能宣布这一消息,但是围绕AMP的越来越多的信息活动秘密证实了这一点。
我想提醒你和我自己的思想-
为人写信 。 在做出任何决定之前,请三思而后行,尤其是在开发其他开发人员将使用的工具时。 您的每项决定和行动都会影响某人,并有可能使他有点鲜血。
我不想让某人崩溃。有用的链接
我们制作了Saint AppsConf程序,该程序已于10月21日至22日在圣彼得堡举行,与春季相比,活动更加丰富多彩。 看看吧!
或订阅时事通讯,电报,fb-在此我们讨论会议的个人报告和准备工作。