当前位置:首页 > 旅游指南

美团猫眼电影 美团猫眼电影Android模块化实战

作者:陈|来源:微信官方号何

1写这个博客的初衷

首先我想用一句话总结一下:我想记录下我最近几个月的所作所为,希望尽可能详细。希望读者能了解项目模块化,业务框架从一个项目换到另一个项目可能会遇到什么问题,每一步该怎么做,而不是泛泛而谈。

现在很多人都在说模块化,网上很多博客实践都在说模块化。很多讲模块间解耦,大部分讲通过路由器路由解耦。别人话不多,也不乏泛泛之谈。但真正的解耦并运行一个应用。需要解决的远不止脱钩。业务架构、进程间通信、资源处理、解耦方法等。所有问题都需要解决。就为了猫眼模块化的全过程实施,从始至终,我都在分析解决各种问题,几个月了。猫眼app的版本历史是一个高度耦合的项目。从这样一个版本历史到最后,每个业务模块都可以独立运行,在流程之间进行沟通,这就要涉及到各个方面和一些其他东西的解耦。今天我就以这个应用为例(其他应用在解耦时可能会遇到不同的问题,所以注意这一点),完整的讲一下猫眼模块化的全过程。每个方面都不是照搬网络的一些做法,而是分析比较,采用更好的设计方法。比如解耦使用serviceloader而不是routing例如,架构使用了一个更适合我们业务的生命周期的mvp变体。我也讲讲具体花的时间和一些经验,让大家知道以后做模块的时候该怎么做。(注意,其实模块化的过程除了文中提到的还有很多东西涉及。有些就不提了,因为之前已经完成了,比如网络图书馆的缓存是数据库做的-->:Text,这个读者注意。如有遗漏可以沟通~)。

主要内容:serviceloader解耦,mvp变体框架,模块通信,lib独立运行,多终端复用。

2为什么模块化

首先要说模块化不是为了作秀。如果没有业务场景需求,不推荐。

需要模块化的原因有很多。这里我简单说一下为什么要做猫眼:

猫眼需要快速移植到其他app(美团,点评..)。解耦首页,减少冷启动时间。开发时减少build时间,代码责任制。服务快捷替换3 解耦到什么程度?

首先,什么是模块化?每个人都必须熟悉这一点:将不同的服务分成不同的libmodules的能力。那么,模块化后,我们的一个业务库有什么功能呢?我认为是:

总结一下:没有通信成本,在任何应用上运行都很快,像傻瓜一样。具体来说,这个lib不耦合特定应用的服务,也不耦合特定应用的活动。给我一个应用程序(或者一个假的应用程序外壳),通过它的基本活动和它们的服务,我可以很快在那个应用程序上运行这个库。停下!你可能会说这是什么服务,我来详细解释一下~

3.1可以非侵入性地配置各种服务

我们知道每个app都会提供账号信息、设备信息、网络服务、图片加载服务、dot服务、下拉刷新样式、错误状态等等。每个应用的这些服务可能不同。比如美团用的网络服务是okhttp,评论用的是长连接。因此,我们的业务逻辑库不能耦合这些特定的服务。只有从服务中抽象出来的接口才能被耦合。当使用特定的应用程序时,我们将向该库提供应用程序的服务。那么如何提供这些服务呢?如果我在需要服务的时候留了一个引用洞,我需要把app的服务一个一个的插到lib里需要的地方。这代价太大了。我不想这么麻烦。我想把服务实现作为txt文本直接放在app的一个文件夹里,你的lib可以为我运行。所以我根本不在乎图书馆里有什么。你要做的就是给我一个业务lib,我加一个txt文本,它就运行了。

3.2 lib使用快捷方便

谈谈不耦合的活动。我们知道每个app都有自己的baseAtivity,可以做统计,处理异常,初始化一些库等功能。另外,每个应用的actionBar是不一样的,不同的应用中每个页面的manifest架构也是不一样的。所以,如果lib中的业务是一个业务,那么它应该是一个视图/片段,而不是一个activity,所以我们可以直接为任何一个app创建一个activity,然后把lib中的页面放到那个Activity中。同样的,考虑到合作的成本,我放这个页面的时候也不想处理很多其他的事情,比如数据加载。我希望你能给我这个业务页面寻呼机(实际上是一个视图),我可以把它放在onCreate的setContentView活动中,它就会运行。不要让我做生命周期、数据绑定、销毁等其他事情。,这些都需要在寻呼机内完成。

3.3演示示例

以上两点可能有雾。最近写了一个猫眼问答要求,涉及5页,所以做了个lib。然后我会用我最近写的lib来说明。

这个库是问答的业务库。这不耦合特定的服务,只耦合服务接口。里面的页面(页面包下)不是活动,而是视图。然后这个时候,另一个同事想在猫眼app上用这个lib。怎么做?

在猫眼应用的build.gradle下添加这个lib依赖项:

编译' com . Mao Yan . Android . business:movie:1 . 0 . 2 . 3 '

在猫眼主机app中添加一个lib需要的服务配置:服务实现txt文本(因为是主机app,所以之前实际存在)。

Txt里面是:

在宿主应用程序中创建活动,将页面放在lib中,并填写清单,例如(有时可能需要在其中编写actionBar的交互逻辑)

就这样,它开始跑了。使用的各种服务,比如下拉刷新,都是这个app提供的。是不是很快,没有交流,很蠢?如果要测试这个app,也很简单。随意创建一个app shell,创建一个新的活动,把页面放入lib。然后添加服务实现所需的txt文本(因为是测试,所以服务实现可以自由配置),就大功告成了。这种修复bug和调整ui的方法比启动宿主应用程序修改代码节省了大量时间。让我们看看我随机写了一个测试lib的应用:

我们可以看到下拉刷新,状态服务等等都和猫眼app里的不一样,可以定制。这样写的话,其实所有的模块都可以快速、傻乎乎、可定制的做成app。这种脱钩程度好吗

感觉好的话那就开始工作吧~

4开始模块化之旅4.1原项目耦合结构

要开始模块化的工作,我得给你看以前没有模块化的时候高度耦合的猫眼app。我们以电影详情页为例,看看他的耦合:

电影详细信息页面基于基类的层,这些层与各种服务(如特定的网络加载)相耦合。因为详细页面有想看、评分、赞等可编辑状态。,也是和greendao数据库耦合的(这个数据库之前也是和网络加载耦合的,但是被改装+rxjava代替了,所以这个耦合被代替了,感谢上帝)。这个页面需要和其他页面交互(比如跳转、评分同步等。),所以也与其他页面的类相耦合。另外还有utils,view,model等等。如果你想拉出电影细节页面,所有这些耦合都应该被剥离。需要解决的具体问题如下:

4.2准备工作-4.2.1工作量评估

首先说一下脱钩的准备工作。因为这些任务是解耦和拆分的基础。有两件事要做,如下所示:

首先,我不喜欢玩五星。确实这部分工作量比较大~ ~ ~

-4.2.2公共资源、模型、设施等的分割。-4.2.2.1耦合示例

第一点是公共资源、模式、公用事业等的分割。这些东西虽然不需要考虑太多,但是很复杂。这个地方模块化的时候花了很多时间。很大一部分原因是之前的猫眼版本历史代码不规范,对代码耦合不够敏感。举几个例子:

我们之前的utils基本都写在一个类MovieUtils里面了。这个类就像大染缸。什么都向里面放。在传入的参数方面也不够规范,甚至MaoyanBaseFragment这种业务代码都作为参数传入。导致这个东西及其难拆。utils的方法不传context。前人写的时候图省事,在项目中统一加了一个静态的context,导致几乎所有的utils都没有传入context,这样的后果是这些工具方法直接以来宿主app。之前写的common view 不够独立。既然想写common view,那么就尽量让这个view能够独立,不要耦合其他第三方库,尽量使用android 官方库。- - 4.2.2.2 资源拆分经验

资源的拆分其实很繁琐。尤其是如果字符串、颜色、dimens等资源分布在代码的每个角落,一个一个的拆解,非常麻烦。其实不用这么做。因为安卓在构建的时候会合并收缩资源。最后,res/values下的所有文件(应该注意styles.xml)将只放入intermediate/RES/merged/../valos.xml,无用的文件将被自动删除。最后我们可以用lint自动删除。所以不要在这个地方待太久。就像我刚才说的,styles.xml要注意。那么需要注意什么呢?这个东西是这样写的:

当我们写属性名时,我们必须添加前缀限定符。如果不添加,当你的lib打包成aar被其他应用使用时,会出现属性名重复的冲突。为什么?因为名称BezelImageView没有出现在intermediate/res/merged/../value.xml根本不要以为是属性的限定符!

-4.2.3集成型与组合型(可选)

我们讨论了资源的分割,比如utils,然后我们讨论了第二点,基类的处理。我们看到电影细节页面是建立在一堆基类之上的。每一层的基类都做了一些事情。(当时是为了页面的快速发展而写的。)如果我们想要分离电影细节页面,我们需要将这些基类打包到一个aar中,并将它们放入基本库中,供所有页面使用。但是我们之前的基类是耦合了很多猫眼的东西,比如下拉刷新,页面状态等等,如果我需要写一个页面,我需要继承很多片段。当然这种改变也是可以移植的。但是,对于未来的代码迭代(修改和添加服务)来说,肯定不是好事。因为灵活性差。比如评论app需要某个页面的一部分,而不是整个页面,那么改变原来的就不太方便了。我希望这些页面都是视图,而不是片段。而且不是这种传承方式,而是一种结合方式。也就是说,如果我想要一个带有下拉刷新的列表视图,那么我可以直接构建这样一个视图,如果我需要任何配置,可以设置它,并且可以使用它。您可以将这个视图放入任何视图中,将其分割并与其他视图组合。即:

moviercpagefulltorefreshstatusblock是一个视图,可用于组合任何页面上的视图。

-4.2.3.1组件的插件和插件设计

其实我的做法更大胆或者说“懒”。希望在我的MovieR page purll store refresh status block成功构建后,可以在页面上运行显示,自动加载数据。就像小时候玩积木一样,组件和组件都是即插即用的。至于这个块如何加载数据,对用户来说并不重要。用户只需要得到这个块,然后在构建的时候设置自己需要什么。放到页面上就可以运行了。你可以以此为例:

我们可以看到这一页。我只是建立了两个视图,并把它们放在这个页面上。我不在乎数据加载。数据加载在这个块内完成。然后,如前所述,这个页面可以被应用程序激活并运行。外挂和傻瓜式思维,可能是我“懒”了吧~ ~

这个架构怎么实现?接下来,我们粗略的看一下这个框架的总体实现思路(具体来说,我们看一下我写的这篇关于android官方mvp框架优化的文章:lifecycle-mvp,像前端一样组合写页面)。其实这个框架也是mvp框架的思路,但是也解决了业务场景的一些问题,比如生命周期、可移植性、通信成本、易用性等。既然要讲实现思路,那就从头给自己总结一下,可能对读者有帮助。先说一下mvp框架的含义:

-4.2.3.2 MVP框架含义

一般来说,mvp框架适合安卓场景。m代表模型,提供数据;v是视图,提供视图相关的方法供演示者调用;p是presenter,它提供了一种逻辑方法来触发页面中的动作。

-4 . 2 . 3 . 3的官方mvp框架的缺点

网上有很多mvp框架,官方也推荐mvp框架。一般的区别是,合同用于承载视图和演示者的接口定义。用片段实现视图接口。然而官方用片段实现观有其无奈之处。为什么无奈?对于视图层的接口,用fragment来实现,主要是因为fragment有生命周期。但是碎片太蠢太重。想象一下,我有一个页面,里面有四五条内容。为了以后移动、移除、移植每一段内容,希望每一段内容都做成mvp形式,块与块之间不耦合。那么官方的mvp框架就不适用了。因为你不能在一页上写五个片段。不建议在android活动中写那么多片段,片段的典型使用场景是ViewPager。

-4.2.3.4通常是灵活的

为了灵活起见,五段内容的视图层不再用片段实现,而只是普通视图,每个视图监听事件的响应仍然在view中进行(调用自己的presenter方法)。对于整个页面的初始化加载或者下拉刷新加载,这五条内容共享一个片段,这五条内容对应的presenter方法加载在这个片段的onStart()和下拉刷新的监听回调中。然后在片段的onCreateView()中填充5段内容的视图。这五项内容之间可能需要通信和数据交换,这是通过演示者分段进行的。

-4.2.3.5有生命周期的MVP:生命周期-MVP

这样做绝对没有问题,以上做法在我们的项目中也是存在的。但是通过几个版本的迭代,我发现了一些问题:主持人太乱,分散。片段需要容纳所有演示者,并在onStart()时加载()数据。每个视图还需要有自己的演示者。并且视图和演示者需要相互设置()。您还需要在activty或fragment的onDetroy()方法中管理演示者。总的来说,人们感到困惑。尤其是你的组件需要别人使用,或者组件的使用需要其他应用,别人会得到你的组件。你要关心两件事,即观点和主持人。他必须知道这两件事情中的方法,他需要把它们关联起来,在activity/fragment的生命周期中调用一些方法。嗯。这个过程肯定有很大的沟通成本~

这就是为什么我想到前面提到的构建方法来实例化组件,然后使用寻呼机来组合组件。特点是(具体来说,你可以看看安卓官方的mvp框架优化:生命周期-mvp,像前端一样组合写页面):

使用lifecycle-component这个组件提供生命周期。presenter被view层内部持有,不向外暴露。build创建view实例时,提供TypeFactory,用于业务的扩展。业务代码分层。用这种mvp的变种框架改写项目的原代/写新业务,就可以使页面更容易移植、拓展,页面内的模块也可以移动改变。当然,这种框架是建立在我们的业务基础之上,框架还是需要因项目而已,没有最好,只有更适合~4.3 接口的抽离

模块化的准备工作前面已经介绍过了,接下来我们需要做什么?根据前面介绍的原项目的耦合结构,我们知道我们之前的项目直接依赖于各种服务的具体实现。我们接下来要做的是用接口剥离这些具体的服务实现:

-4.3.1使用servieloader解耦-对服务实现类的非显式调用

-4.3.1.1官方服务加载程序

从图中可以看出,我们的实现类被相应的接口所取代。但就这一步本身而言,并不太难:找到之前调用服务的地方,然后用接口调用。无非是一些服务用的比较多,换起来比较麻烦。但现在我们需要考虑一个问题:如何才能提供服务?首先想到的是我们留下一个参数传入。但是这种方式会导致以后使用lib的时候通信开销太大:你需要告诉别人我需要传入的参数在哪里,传入什么类型的参数。否则,您的lib将不起作用。我不希望别人在使用你的lib时检查你的代码是什么,如何传递参数。我希望lib在别人使用的时候尽可能的透明。你不需要知道lib里面写了什么,你只需要在外面配置一个txt文本就可以运行lib了!那么我们该怎么办呢?

其实java很早就提供了这个类似的功能:把提供者配置文件放在资源目录META-INF/services中,当app运行时,遇到service loader . load(xxxinterface . class)时,会在META-INF/services的配置文件中查找这个接口对应的实现类的完整路径名,然后使用反射生成一个不带参数的实例。

我们的一般用法也是基于java提供的这个函数:

-4.3.1.2官方服务加载器的改造

-4.3.1.2.1官方服务装载机的缺陷

从前面的解释来看,java提供的官方serviceloader至少有三个方面需要改进。

serviceloader没有缓存功能。因为对于服务来说,大部分我们都需要使用单例模式,而不会频繁的生成新的实例。serviceloader使用无参的构造方法进行构建实例。这点不用多说,肯定需要改进。谁的服务构建的时候不需要传入参数呢?serviceloader没有预检查等问题。因为在运行时,需要在配置文件中去寻找接口对应的实现类名。那么肯定会遇到接口名写错了,类名写错了,配置方式写错了,找不到接口实现类等,这些错误在编译器是发现不了的。同时,使用serviceloader是一种非显式的调用服务实现类方式,如果不在proguard中保护这些实现类,那么肯定会被shrink掉。除了proguard问题外,配置文件写在资源目录META-INF/services下对于一些手机(三星)也有兼容问题。最后,考虑servic配置文件手动注册的缺点,serviceloader需要提供自动注册功能。

第一点在处理以上三种情况时很容易解决。只提供一个缓存,我就不多说了。

- 4.3.1.2.2 serviceloader构造示例

其次,我们解决了这个问题:我们让所有使用serviceloader加载服务的接口都实现了Iprovider接口。Iprovider接口提供了一个init(上下文上下文)方法。这样,所有的服务实现类都需要实现init(Context)方法,并在原构造方法中做初始化逻辑。因此,当我们调用serviceloader来加载服务时,它类似于这样:

imageloaderiimageloader = movieserviceloader . getservice(context,imageloader . class);

在MovieServiceLoader中,生成的实例将调用init(上下文)方法一次。就这样,我们解决了第二个问题。这里可能有朋友有一些疑问(比如美团平台童鞋讨论过这件事):为什么只传上下文参数。服务实现类需要其他参数怎么办?就我们的服务和而言,我认为我们只需要在上下文中传递,基本上我们可以通过上下文获得安卓的大部分参数。而对于服务,既然是服务,那么说它不会依赖于你的项目的某个具体的组成部分也是合理的。所以我觉得传入上下文就够了,不要传入格式不定的对象参数:

MovieServiceLoader.getService(对象...params,imageloader . class);

这样当然可以解决所有问题。然而,这种设计思想违背了接口和实现之间的隔离概念。例如,我想使用图像加载服务,所以我只需要调用它

imageLoader = movieserviceloader . getservice(context,imageLoader . class);

没事的。别让我知道你的具体服务是毕加索还是格莱德。我也不想知道。如果用第二种方案,我需要知道你具体的服务需要什么参数,然后传进去吗?感觉好不友好。使用Iprovider的另一个好处是,我们只需要在MovieServiceLoader仓库的proguard中添加它

够了。在其他地方,当使用或创建新的服务接口时,不需要考虑proguard。

- 4.3.1.2.3服务加载器预处理-渐变插件

第三个要解决的问题是serviceloader的预检验。解决方法是编写一个gradle插件。插件的一般流程是:

我们在build的某个阶段拿到所有编译后的class文件(夹)和jar包。使用javassit确定哪些类被@autoService修饰,配置文件中如果不存在,在其添加。查看serviceConfig配置文件里面的格式是不是正确。通过javassit来确定serviceConfig配置文件里面的类是不是在项目中存在,接口类是不是实现了Iprovider接口。- - - 4.3.1.2.4 需要用到的知识:build流程,javassit,groovy

这里不想说太多,但是考虑到这三点,很多读者可能并不熟悉。直接去网上谷歌,光是这些东西,你可能需要学点东西。然后我写下我的一些经历(为了针对性,我就不详细展开了)。读者可以参考参考,有些可以事半功倍。

因为需要得到编译后的类文件和jar包,所以需要知道build的一般流程,每个任务的输入输出是什么,是以文件夹的形式还是jar包的形式。比如你获取所有类的时候,可以在组装XXX的时候从dex任务的输入文件夹/jar包中获取所有类(dex任务任务已经完成)。同理,也可以使用javac task的输入。但是不允许javac任务输出,因为javac任务输出的中间/类文件夹只包含项目中的类文件,不包含aar对应的中间/分解-aar文件中的类文件。转换也是一种实现。给出了转换的输入输出文件路径,输入类都是类。

除了构建过程,您还可以使用groovy来编写插件逻辑。然而,如果你真的不想使用groovy,你也可以使用java,Java是相互兼容的,但是groovy的许多特性,比如循环,是不能使用的。这里有一点经验:写goovy,ide不能很好的提示错误,比如你用了一个变量或者一个方法,如果方法错了,变量就是未定义的。不会给你找不到的暗示。所以,还是写信给比较好。java,然后转到。太棒了。

最后,你需要了解一些javassit的知识,这是一个处理类文件的工具。它非常强大,类似于java,它的大部分使用将基于ctClass。所以这个类应该比较熟悉。这里有一点经验:有时候需要CTClass>:对于类转换,记得用静态变量来存储这个类对象,否则会报告classloader多次加载同一个路径的异常。

好了,用serviceloader解耦的原理、改进和好处都说完了。

-4.3.2服务加载器解耦与路由模式解耦

网上大部分关于模块化的博客都是使用路由解耦的。什么是路由模式的解耦?

-4.3.2.1路由模式的解耦描述

让我们看看一般的路由框架

那么这个路由框架是如何工作的呢?这里的操作是一个服务,而提供者是一个映射集,它保存了lib中的所有键值对(操作名:操作实例)。在宿主应用程序中注册每个库的提供者。这样,当模块A请求moduleB的服务时,它传递(代码来源):

即通过提供提供者名称、动作名称、参数名称和值,在注册的映射中找到对应的动作实例,然后调用其对应的方法。核心是使用字符串匹配对应的实例进行解耦。

-4.3.2.1.1去耦路由模式的优势

这种方法最大的优点是,在创建新的服务时,不需要编写接口,所有的接口都用字符串标记进行匹配,两个模型之间不需要耦合任何东西,甚至接口声明也不需要耦合。如果一个lib中有很多服务需要外界调用,调用的次数不多,或者如果我只对服务进行解耦,那么这种路由方式非常好,因为不需要写接口。

-4.3.2.1.2讨论路由在服务解耦中的不适用性

但是为什么不选择这种脱钩方式呢?因为这样,对于安卓的整体服务脱钩,我还是提出了以下关注点(仅代表我自己的观点,可能比较粗糙,不是说别人的项目不够优秀):

对于大面积的解耦,肯定大部分是app界别的服务进行解耦。特点是大量使用,这时候我写几个接口,下沉到base库,无伤大雅。这样我在使用的时候,serviceloader好处就突出来了:使用服务的时候,我不需要关心实现类的类名,包名是什么,需要传入什么参数,调用的方法的名字是什么。如果使用路由方式接口,我需要关心的事情就多了,如果我需要关心这么多东西,它就不应该叫服务了。如果另一个lib在你不知情的情况下改了名字怎么办?并且在代码移植到其他app或独立运行时,配置方式也不够友好。serviceloader只需要写个配置txt文件放在apk中即可,并且每一个lib的服务写到自己的serviceCinfig即可,不需要宿主app关心。使用路由方式,即使action可以自动注册,也需要在application处理一些注册的事情。路由这种服务框架和serviceloader,本质来说,并不能进行真正意义上的模块间的通信。说的通俗点,路由框架能做的是:b lib可以在不依赖a lib项目的情况下,b可以new 出来a中一个类的实例(或提前new好),然后调用那个实例的方法。这并非通信,只是能够调用其他仓库的方法。而通信指的是监听状态,回调。serviceloader同样也做不到真正意义上的通信。模块间通信只能通过非显式的监听机制才能进行,比如eventbus,广播,contentprovider等来进行。为什么要说这一点呢?因为我看到很多模块化的博客都在说使用路由框架进行模块间通信。但就前面提到的这种路由框架,确实做不到真正意义上的模块间通信。

好了,serviceloader解耦vs route解耦在这里。

4.4关于解耦的其他工作-4.4.1工作评估

前面的大部分工作是关于通过使用serviceloader来分离服务。那么除此之外还需要做什么呢?这里我就概括一下,然后逐一说明:

-4.4.2服务实施的撤销

注意第一点的后半部分:如果想让所有模块独立打包运行,需要把所有服务实现都拉出来。如果不想独立运行,只想解耦,那么可以留在宿主app。虽然说这样的话很容易。但是没有一个服务实现要花很多时间去实现,因为一个服务可能会耦合很多东西,不小心很难拆解。读者应该记住一个数字。但是如果你能把它拉走,试着把它拉走,而不仅仅是lib的独立运作。对于之后的服务更换也是大有裨益的。比如网络加载库,之前用的是retrofi+okhttp,后来升级为改装+长连接。替换只是更改服务配置文件中的一句话。如果打算抽离,要注意接口的定义,不要耦合某个特定库的类,综合考虑,合理设计。例如INet库,接口定义为:

publiciinterceinetserviceextendsiprovider {

& ltT>。t create(FinalClass & lt;T>。服务,字符串获取数据策略,字符串缓存时间);

}

虽然改装是一个很棒的库,但是接口没有耦合这个库。也许有一天会被取代。

-4.4.3撤销数据库

第二点说起来很痛苦。拉走数据库真的很麻烦。不知道是哪个版本开始的。猫眼再加上绿岛。这个数据库本身很优秀,但是太大了装不下!如果我想给别人一个lib,我必须把这个lib和一个大型的第三方数据库连接起来吗?!!因为之前没有考虑模块化,基本上所有的网络数据和敏感数据都被grrendao保存了。所以每次在解耦的时候看到刀会话,都觉得自己像只老虎。网络数据存储在文件中,对业务代码透明。敏感数据存储在数据库中,但通过接口隔离,建议数据库使用官方数据库sqlite或room。

-4.4.4告别黄油刀

第三点意味着,如果你想独立地模块化业务代码,你必须像butterknife框架一样告别视图注入功能。安卓ADT14启动后,库的R资源不再是最终的,所以不能在库中使用R.id.xx,需要改用findViewById();也不能用switch(R.id.xx),需要用if...否则。

第四点是第一点的后续工作。工作不多。

-4.4.5页面跳转-4.4.5.1页面跳转需要做什么

页面跳转在app中也是很重要的一件事,因为它是一个模块化的门户,涉及到页面和其他应用的沟通,我的版本和页面。虽然看似简单,但如果设计不合理,模块化门户的代码优雅性、崩溃量、页面退化、操作协作等方面都会受到影响。

对于页面之间的跳转,我们的一般做法是:

如果这个类页面没有隐式跳转功能: 那么直接在其他页面首先 获取intent(getContext(),TargetActivity.class(,然后intent添加参数。 最后starActivity(getContext(),intent)。 在目标activty的onCreate()里面getIntent().getString(xx_key,defaultValue)等获取参数; 如果xx_key对应的value不合法或者解析错误,比如movieId=0,或者等于“”。那么应该跳转到一个其他页面或者跳转失败。如果这个页面配置了隐式跳转功能: 那么在其他页面你首先得创建一个createXxxActivityIntent()的utils方法,在里面传入落地页的path,参数key,参数value。 在manifest中声明。 在目标activty的onCreate()里面getIntent().getData().parseBoolean(xx_key,defaultValue)…等获取参数 如果xx_key对应的value不合法或者解析错误,比如movieId=0,或者等于“”。那么应该跳转到一个其他页面或者跳转失败。- - 4.4.5.2 android原生页面跳转存在的问题

来说说这个原生页面跳转的问题吧~

在获取参数的时候,需要写一大推的intent.get(xx),如果这个页面既含有隐式跳转,又含有显示跳转,那么肯定上面那个过程都需要,这样在onCreate()里面就会非常的乱。要进行if else如果想进行隐式跳转,那么都需要在manifest进行注册intent-filter。一是麻烦,二是我需要在另外一个地方去配置某一个activity的东西,管理不方便。需要另外写一个utils获取隐式intent。没有降级策略,如果运营配错了,那么只能到错误页面,而无法进行一个补救措施,比如进入i版页面。开发人员或者后台配置错误参数的时候,我们需要写兜底逻辑。每一个页面解析都需要写一段相同的逻辑。如果一个页面需要登录用户才可以打开的权限,那么我们经常会写if(isLogin()){//跳转页面}else{//跳转到登录页面} ,每次操作都要写这些个相同的逻辑。

如果你觉得这方面没有那么多要求,对于页面之间的跳转,为了不耦合其他模块的类,所有页面都可以采用隐式跳转机制。这已经基本满足情况了。但我还是想说一下阿里推出的开源框架Arouter。它具有拦截功能,使跳转失败可以降级(如呈现I版页面),使页面具有登录用户可以打开的权限;获取参数的统一方式等。挺好的。基本解决了以上问题。具体就不展开了。具体来说,你可以看到开源的最佳实践:“Android平台页面路由框架ARouter”

4.5模块/页面之间的通信-4.5.0使用视图模型在页面之间共享数据

这一段是新增加的。我觉得这里比较合适。ViewModel是谷歌引入的生命周期组件中的一个新类。官方文件声明使用ViewModel可以解决页面旋转等配置变化时的数据保存问题。想了想,觉得也能起到解耦页面内数据共享的问题。

举个我之前遇到的例子:pm让我做一个页面完成后的嵌入点。嵌入点需要页面的movieId信息,但是需要嵌入点的块中没有movieId。而且我的街区级别很深。如果我想得到movieId,我需要把它从活动页面级转移到我的块,这就不可避免地导致了中间层的耦合和方法的创建。当时觉得真的是大事。当时,需要像eventbus这样的事件监控形式。我只需要把数据放在总线上,然后就可以很容易地在这个页面的任何地方获得它。总结一下:说白了,当需要在块/片段页面之间使用对方的数据/视图时,不需要在它们之间进行硬性引用,只需要活动的上下文参数就可以获取对方的数据/视图,从而交换数据,访问视图。但是页面的上下文是系统类型,容易获取,没有耦合。

具体使用请参考我之前写的一篇文章,用ViewModel分享页面中的数据:ActivityDataBus

-4.5.1为什么要去掉eventbus,使用广播

如果到了这个阶段,一个页面已经被拉出来,剩下的就是与其他模块和其他页面的交互。

如前所述,serviceloader和routing方法都不能做这些事情。首先想到的是使用eventbus来做这些事情。使用Eventbus的前提是需要定义一些事件事件。例如:

但是如果把业务代码模块化,就有一个尴尬的问题:Event在哪里?因为很多库需要监听这个Event,所以只能将Event沉入基本库。结果基本库越来越大,分不开了。在这一点上,《微信安卓模块化架构重构实践》也提到了这件事,并且创造性地使用了一种叫做的方式。api”来解决这个问题。原理是将公共接口沉入基础库,供其他模块在编译时使用,而这段代码的维护仍在非基础库中。这个基础库就不扩展了,代码维护的责任制更清晰了,挺好的。可惜最近没多少时间写这个gradle插件了。不知道哪位读者有时间和兴趣来实现这个插件。意义还是很大的,基础库的代码不会越来越膨胀。Eventbus不仅扩展了基础库,还存在一个无法在应用之间进行通信的问题。我们用广播代替eventbus。android引入的LocalBroadcast实现机制只是一个循环处理器,并维护一个全局映射。性能类似于事件总线,使用字符串而不是事件模型来匹配事件。如果我们使用一个接口来包装BroadcastManager,那么我们可以在应用程序内部使用域内广播,对于模块化的lib,我们可以使用域外广播来在应用程序之间进行通信。

-4.5.2不要乱播

如果在项目中大量使用eventbus,会看到一个类中有很多onEventMainThread()方法,写起来很爽,读起来很痛苦。如果项目中有许多地方可以发送此事件,则有许多地方可以接收此事件。当你拆分代码的时候,你不敢轻举妄动,怕哪些事件没有收到。广播类似于eventbus。如果一个项目中有很多相同事件的发送和接收,项目的可读性和可维护性就会变得相当差。这种情况在敏感数据的同步中尤为突出:

实际上,对于敏感数据的同步,不需要发送广播或eventbus进行同步。同步可以通过在数据库的帮助下本地化所需的数据来实现。总的想法是,我们从网络上获得的所有数据都与数据库同步。当用敏感数据填充视图时,所有使用的数据都来自数据库。当页面返回时,如果页面没有触发填充敏感数据视图的逻辑,则在onResume()手动调用,即:

那么模块/页面之间的通信一般就完成了,这里没有太多的工作要做:

4.6 lib独立运行

-4.6.1为什么需要与主机app流程沟通

此时,业务模块可以作为库放在宿主应用程序中,也可以作为应用程序独立运行。作为图书馆,很好理解。就像文章前面的问答模块一样,给宿主app添加几个activty的shells,然后在lib中添加页面,再在manifest中注册,也就是:

当然,如果需要做一些actionBar交互,需要在主机activty中编写相应的逻辑。整个app的框架图如下:

当需要调试业务lib时,我们需要让lib独立运行,如前一篇文章中的问答业务模块演示所示。这时候,就有问题了。我们的lib独立运行时,账号数据从哪里来,如何获取与app的地理位置和城市相关的数据?读者可能会说这些不是服务?服务,不应该像网络加载和图片加载一样用serviceloader加载吗?原则上是这样的,但是账户等一些信息的服务实现类并不是那么容易从主机app中拉出的,因为服务实现类需要在应用中进行初始化,还有很多其他的事情需要考虑。所以真实的账户信息不是那么容易通过之前的方式获取的,那怎么办?最简单的方法就是创建假数据,比如创建自己账号的信息,作为服务实现类使用。但这种情况下,账号信息只能属于一个人,修改账号信息不可行,账号无法注销。所以要想新办法。

-4.6.2与主机应用程序流程的沟通流程

最后发现,如果我们自主运行的lib能够监控主机app的账号、位置、城市、登录类型、设备等信息并进行同步,那么自主运行的lib中的所有信息都是真实动态的。主机app注销时,lib中没有登录状态。具体操作是:

在宿主app中,我们提供一些contentProvider,各方法提供的内容就是宿主app真实的的账户等数据。当对宿主app账户等信息改变时,通知contentProvider的监听者,比如:

publicationeventmainthread(LoginEvent LoginEvent){

getContext()。getContentResolver()。notifyChange(Uri . parse(" content://com . Mao Yan . Android . RuneNV/login session "),null);

}

在独立app中,其扮演contentProvider的监听者:

mcontentresolver . registercontentobserver(Uri . parse(" content://com . Mao Yan . Android . runen v/device session "),false,newContentObserver(null) {

@覆盖

publicationchange(BooleanSelfChange){

super . OnChange(SelfChange);

reloadeenvironment();

}

});

这样一来,lib中的account等数据与宿主app的数据是一致的。我们使用服务接口包层,使使用模式与之前的服务使用模式一致。

总体示意图如下:

主机app注销时,lib中没有登录状态。让我们看一下演示:

最后,根据惯例,当一个模块要独立运行时,需要做的是评估:

5最后一句话

整个过程终于结束了。希望读者看完之后对模块化有一个整体的认识,大致了解需要做什么,每一步需要多少时间。模块化不是为了炫耀技能,展示自己有多厉害。如果只是这样,就不用做了。因为模块化繁琐、枯燥、费时,你做了很多工作,但表面功能上,大佬们可能看不到。还不如花点时间,介绍个第三方库,看起来花哨一点。很大一部分工作量是为以前设计不足的代码逻辑买单。我做这个活动是为了商业服务,因为猫眼电影需要很多客户服务。结果表明,业务解耦和业务模块化是不可避免的。模块化后,代码可以轻松移植到其他终端,app中页面的调整变得简单。最后,在模块化的整个过程中,有一些经验和感悟可以分享给大家。原因很简单,更重要的是实现了:

6参考文献

微信Android模块化架构重构实践开源最佳实践:Android平台页面路由框架ARouterAndroid组件化之通信ServiceLoaderAutoServiceAndroid-Architecture-Components关于Android业务组件化的一些思考

原作者:陈,美团猫眼Android开发工程师,擅长组件化、项目解耦、跟随技术前沿。欢迎点击[阅读原文]关注他。

1.《美团猫眼电影 美团猫眼电影Android模块化实战》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。

2.《美团猫眼电影 美团猫眼电影Android模块化实战》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。

3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/junshi/651221.html

上一篇

防弹背心 防弹衣为何都是背心式的?

下一篇

麦秸画 麦秆画的制作流程

页面紧急访问 404,页面无法访问,真情得以寻回

  • 页面紧急访问 404,页面无法访问,真情得以寻回
  • 页面紧急访问 404,页面无法访问,真情得以寻回
  • 页面紧急访问 404,页面无法访问,真情得以寻回
cfvip礼包领取 穿越火线CFVIP专区活动地址 CFVIP可每月来此页面领取特权礼包

cfvip礼包领取 穿越火线CFVIP专区活动地址 CFVIP可每月来此页面领取特权礼包

活动介绍:CFVIP,穿越火线CFVIP区的活动地址,每个月都可以来这个页面领取优惠套餐 活动时间:5月10日-12月31日 消防贵宾区穿越火线活动规则: 在活动页面上,启动CFVIP,您可...

安卓数据线 安卓手机Type-C数据线简介,接口定义,详细图文教程

  • 安卓数据线 安卓手机Type-C数据线简介,接口定义,详细图文教程
  • 安卓数据线 安卓手机Type-C数据线简介,接口定义,详细图文教程
  • 安卓数据线 安卓手机Type-C数据线简介,接口定义,详细图文教程

政治必修四第一单元知识框架 高中政治必修4单元知识框架图

  • 政治必修四第一单元知识框架 高中政治必修4单元知识框架图
  • 政治必修四第一单元知识框架 高中政治必修4单元知识框架图
  • 政治必修四第一单元知识框架 高中政治必修4单元知识框架图
pcie接口 不可不知的SSD接口知识

pcie接口 不可不知的SSD接口知识

将近两年随着技术的进步和成本的下降,固态硬盘已经逐渐成为新安装或主流笔记本电脑的标准去年全球市场研究机构趋势力吉邦咨询鉴于2020年科技产业的发展,整理发布十大科技动态包括与非门闪存工艺技术将首次...

手机app页面制作 手机APP界面如何制作

我们打开一个APP,首先看到的是APP的首页界面,可以告诉我们app的主要功能和特点,所以App首页界面的体验会影响用户对产品的后续体验。所以每个APP应用开发团队都会精心设计每个APP的首...

钢混结构和框架结构的区别 钢筋混凝土的寿命 和框架结构的区别介绍

钢混结构和框架结构的区别 钢筋混凝土的寿命 和框架结构的区别介绍

作为一种基础的建材产品,钢筋混凝土往往用于许多工程项目的施工领域中,比如现在市面上大部分的楼房采用的都是钢筋混凝土通过专业的工艺制作而成的产品,除此之外还可以得知钢筋混凝土楼房的寿命也和材质选择和...

flask框架 Flask框架使用

flask框架 Flask框架使用

当我们构建一个接口测试平台或者管理一些脚本的时候,我们通常希望有一个Web页面来维护。今天,我们将介绍一个用python编写的轻量级web应用程序框架,它很容易与自己的开发服务器和调试器一起使用。...