安卓APP完整开发流程(6/12)多项目架构/组件化开发
牛客高级系列专栏:
安卓
嵌入式
正文开始👇
1、创建/引用子模块
所谓的多项目,就是指多个子模块,新创建的工程会自带一个名为app的子模块。上篇文章说到如何引用工程其他子模块,那我们先创建一个子模块出来。
创建子模块之前,明确两个概念,打开app子模块的build.gradle,可以看到:
plugins {
id 'com.android.application'
}
代表当前的子模块将构建为一个应用程序,同样还有:
- "com.android.application":用于构建 Android 应用程序
- "com.android.library":用于构建 Android 库的插件
- "com.android.test":用于构建 Android 测试的插件
因此,我们是可以创建一个工程,里面包括多个application(应用程序)和多个library(库):
- 一个application可以引用多个library;
- 一个library可以被不同的application所引用;
那么,现在给新工程里的app子模块去新建一个library子模块。
依次点击File - New - New Module:
选择Android Library,并设置子模块名称、包名、编程语言等即可。创建后有两个地方需要注意。打开新创建的子模块的build.gradle,可以看到:
plugins {
id 'com.android.library'
}
代表当前的子模块将构建为一个库。同时,在根目录的settings.gradle会多了一句
include ':mylibrary'
这是因为settings.gradle文件定义了应用程序的模块都有哪些,既然创建了一个新的模块,就需要在此记录,当然这个步骤Android Studio会自动完成。
接着,回到app子模块的build.gradle添加:
dependencies {
implementation project(':mylibrary')
...
}
就可以将 "mylibrary" 库模块作为依赖项引入app子模块中。当以app子模块作为主项目进行编译时, 也就是这里:
如果项目里有多个子模块被设置为‘com.android.application’,那这里会有多个选项。所以现在就只有app这一个选项。Gradle 会自动查找本地库模块,并将其编译成一个单独的 .aar 库文件,然后将其打包到 APK 文件中。这样,在以app子模块作为主项目中就可以使用 "mylibrary" 模块中的资源和功能,方便快捷地实现应用程序开发。
2、多项目架构/组件化开发
2.1 什么是组件化开发
将一个庞大的App分成多个模块,每个模块都是一个组件(Module),每个组件之间相互独立,并在组件模式
中每个组件可以单独进行调试,但是在集成模式
中这些组件又能作为一个个子库合并成一个Apk,这就是组件化开发。
如下图,lib_audio和lib_base和lib_common_ui就是3个单独的模块,彼此相互独立,组件模式
中可以作为单独的App进行单独调试。在集成模式
中这3个模块又作为子库,提供特定的功能供imooc_voice这个主模块使用,最后由主模块集成3个子库,打包为一个完整的APk。
PS:在本文,模块(module
)和组件
是同个概念。
2.2 为什么需要组件化开发
当App的业务功能越来越多,开发人员也越来越多,那么就会导致以下问题:
- 编译时间久:代码量级增加,编译时间自然增加,改1行代码就需要重新编译整个App工程;
- 误引入bug:因为代码耦合性高,做一个新需求时,可能不小心破坏了其他地方的逻辑,引入bug;
- 代码冲突/篡改:因为代码耦合性高,经常和其他同事修改同样位置的代码,产生代码冲突。或者相互修改了对方的代码;
- 完整测试:修改一个功能模块,可能影响到整体逻辑,导致需要完整项目的完整测试;
- 重复造轮:明明其他地方有同样功能的代码,却要复制过来再修改后才能使用;
如果你在日常开发中遇到上述问题,证明你的App工程需要进行组件化改造了。
2.3 组件化改造的优势
上述很多问题产生的根本就在于代码耦合
,组件化开发可以有效降低代码之间的耦合性,使整体框架更加明显,甚至可以把某一业务当成单一项目来开发,因此可以灵活地对业务模块进行组装和拆分。并且还能降低团队成员熟悉项目的成本,降低项目的维护难度。
让我们再次看看组件化改造后,上述问题的解决方法:
- 编译时间久 ==》改造后:可加快编译速度:可以单独编译单一模块,即把单一模块当作一个App来进行调试,编译时间大幅度缩减,提高开发效率,当然总的编译时间是不会减少的;
- 误引入bug ==》改造后:提高协作效率,控制产品质量:只在新需求对应的模块下进行修改,最小程序地影响其他模块代码,避免误引入bug,也避免误修改到其他同事的代码,提升协作效率,并控制产品质量;
- 代码冲突/篡改 ==》改造后:责任明确,控制代码权限:各司其职,大家各自修改自己负责的模块代码,减少代码冲突;
- 完整测试 ==》改造后:降低测试成本:修改单一模块,则只需要把该模块当作一个独立App进行提测即可,开发调试时不需要对整个项目进行编译;
- 重复造轮 ==》改造后:功能重用:每个模块都能提供独立的能力,其他模块可以直接拿来用,因此造一个轮子,到处都可以用;
2.4 组件化开发的思路和框架
最理想的组件化开发,就是将传统的一个Module大杂烩,拆分成若干个Module,由主App提供统一的入口,每个拆分后的Module通过相关配置,可以独立运行调试,也可以供主App依赖使用。我们以一个音乐播放的App例子,理想的组件化架构如下:
几个名词解释:
- 集成模式:主要是集成所有的业务组件、提供Application唯一实现、gradle、manifest配置,将所有的组件由“壳工程”进行打包,组成一个完整的APK,
- 组件模式:每个组件可以独立开发,一个组件就是一个APP,可以独立进行安装调试;
- 壳工程:也叫“宿主”工程,负责管理各个组件,以及负责打包完整的APK,其内部包含少量的业务功能甚至没有具体业务功能;
- 业务组件:根据项目需求而独立形成的一个个组件,如一个音乐App,一般都会包含首页、登录、搜索、个人信息页等几个服务。
- 基础业务组件:为了更好的解耦各个组件,可以把上述不同的业务组件里,重复性高的,可复用的代码再抽离出来,作为基础业务组件。比如将各个业务组件用到的通用的UI控件,BaseActivity,BaseHandler等,抽离为“通用UI”基础业务组件。或者将分享功能的代码抽离为“分享”基础业务组件,这时候首页业务和个人信息业务这两个业务组件,都可能用到分享的功能,就不用重复写代码了;
- 基础功能组件:提供开发APP的基础功能,如图片加载、视频播放、保活机制、网络能力等这种单一能力;如果是比较优秀的功能组件,通常会作为公司通用的基础能力,修改频率极低,可以多处复用于公司多个项目集成使用;
- SDK:这里指的是第三方库,如常见的OkHttp、RxJava等;
- 组件依赖关系是上层依赖下层,修改频率是上层高于下层:
- 一个业务组件可能依赖多个基础业务组件和多个功能组件;
- 一个业务组件可能不包括基础业务组件,直接依赖多个功能组件;
- 一个基础业务组件可能依赖多个功能组件;
- 业务组件之间不存在依赖关系;
- 基础业务组件之间不存在依赖关系
- 存在形式:
- 壳工程、业务组件、基础业务组件都是以子模块的形式存在;
- 功能模块可以使用源码直接加载到工程,也可以将源码编译为aar包再放进工程;
- SDK:也就是第三方库,一般都是使用别人做的架包,这种情况一般都是使用远程依赖即可,也方便后续修改版本。
3、组件化开发的实现
为了实现组件化开发,首先需要根据业务,将各个业务进行解耦,并为每个业务创建一个子模块,最后加上壳工程就是完整的组件化开发架构。具体哪种需求适合封装成子模块,以及要以源码形式还是aar包形式集成到项目里,可以参考1.4小节自行定位该模块在项目所处的位置进行决定。现在讨论组件化开发实现的几个需求点:
- 每个子模块可以单独编译,也可以作为库被壳工程集成;
- 全局Context的获取及组件数据初始化;
- library依赖问题;
- 各个模块统一依赖,统一版本号;
- ARouter框架实现各个子模块之间可以跳转/通信;
先附上‘二流小码农’大佬开源的github项目:AndroidAssembly,说明文档:《Android组件化开发,其实就这么简单》,本人学习过程中也从中学到不少。
3.1 每个子模块可以单独编译,也可以作为库被壳工程集成
3.1.1 application和library动态切换
一个模块可以设置为application和library,这是在子模块里的build.gradle去设置的,我们看看这两者的build.gradle有什么区别:
- 设置为application:
plugins {
id 'com.android.application'
}
android {
...
defaultConfig {
applicationId "com.xurui.test"
...
}
}
- 设置为library:
plugins {
id 'com.android.library'
}
android {
...
defaultConfig {
...
}
}
如果希望实现两种状态的动态切换,我们很容易想到用if语句做条件判断,因此可以将上述代码合二为一,gradle使用groovy语言编程,还不了解的同学可见我另外的Gradle专栏:
plugins {
if(is_Module.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
}
android {
...
defaultConfig {
if(is_Module.toBoolean()){
applicationId "com.xurui.test"
}
}
}
我们知道根目录的gradle.properties文件是Gradle构建工具的属性文件(对APP工程目录不了解的同学可见另一个APP开发全流程解析的专栏,将详细介绍每个文件的作用)。所以可以在此设置一个名为is_Module的变量,因为gradle.properties 中的数据类型都是String类型,使用其他数据类型需要自行转换,此时需要通过toBoolean()转为Boolen类型。通过is_Module变量就可以根据配置动态切换每个模块了。比如:
- is_Module变量为false,那么工程处于集成模式,只能编译壳工程,其他子模块作为库包含到壳工程里。
- is_Module变量为true,我们在再定义一个变量,表示当前可以编译为应用程序的子模块的名字,比如工程有A,B,C三个子模块,定义
moduleName=A
此时A模块也可以编译为APP形式运行到手机上。
3.1.2 动态改变清单文件资源指向
除了build.gradle文件外,application和library模式还有另一个区别文件,就是AndroidManifestxml文件,即清单文件;
- application形式:在 Android Studio 中,每个组件都有其对应的 AndroidManifest.xml 文件,用于声明需要的权限、应用程序、Activity、Service、Broadcast 等。当子模块处于application模式时,业务组件的 AndroidManifest.xml 文件应当包含所有 Android 应用程序所具有的属性,特别是声明 Application 和要启动的 Activity,比如:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xurui.code">
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/appTheme"
android:usesCleartextTraffic="true">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
- library形式:当模块处于library形式,代表会被壳工程所集成,此时该子模块就不能有自己的 Application 和 主Activity,不然编译出来的APK就不知道主Activity是哪个了,此时的清单文件可能如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.abner.code">
<application>
<activity
android:name=".MainActivity"
android:screenOrientation="portrait" />
</application>
</manifest>
因为每个子模块的功能是不一样的,所以清单文件内容肯定是不一样的。那么,没有共性,就无法上build.gradle文件一样提取公共代码,通过if语句做判断了。因此,可以一个子模块都保存上述两个清单文件,在build.gradle去判断当前使用哪个清单文件,如在build.gradle这么写:
sourceSets {
main {
if (is_Module.toBoolean()) {
manifest.srcFile 'src/main/mainfest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
其中,main/AndroidManifest.xml是原有的AndroidManifest.xml,我们可以在main目录下创建一个新的名为mainfest的文件夹,将原来的AndroidManifest.xml拷贝一份过去,修改为library形式时的AndroidManifest.xml即可。
3.2 全局Context的获取及组件数据初始化
无论当前子模块是处于application还是library模式,都难免在开发的时候需要获取全局的context,即 ApplicationContext。此时,不可能每个子模块都各定义一个Application类,否则打包起来就会出现问题,毕竟一个APP只能有一个Application。对于这种情况,就可以创建一个BaseApplication类,然后每个子模块都来集成BaseApplication类。比如可以创建一个基础业务组件-通用UI组件,暂时命名为lib_common。该组件可以被其他的子模块所引用。在lib_common创建一个BaseApplication类:
public class BaseApplication extends Application {
private static Application sContext;
@Override
public void onCreate() {
super.onCreate();
sContext = this;
}
public static Context getAppContext(){
return sContext;
}
}
此时就可以在其他子模块,比如app子模块引用lib_common,进一步在MApp文件继承BaseApplication类:
class MApp : BaseApplication() {
override fun onCreate() {
super.onCreate()
...
}
}
3.3 library依赖问题
举个场景,有如下4个模块
- ‘业务模块-用户个人信息模块’,子模块存在;
- ‘基础业务模块-分享’,子模块存在;
- ‘基础业务模块-推送’,子模块存在;
- ‘功能组件-日志组件’,aar架包存在;
其中‘用户个人信息‘模块依赖‘分享’和‘推送’模块,然后‘分享’和‘基推送’都分别依赖了‘日志组件’。那么疑问来了:‘用户个人信息’模块是不是重复依赖了‘日志组件’?
首先如果你代码可以编译成功,那么肯定是没有重复依赖,毕竟如果出现重复依赖,编译都直接报错了。其实编译器编译时,会将‘分享’和‘推送’这两个模块从源码形式先打包为aar包,接着Gradle会自动删除重复的aar包,如此编译出来的APK是不会包含重复的代码的。
3.4 各个模块统一依赖,统一版本号
随着业务的增加,项目里的子模块数量自然会增加。每个子模块对第三方库的依赖最好是用同一个gradle文件进行管理,gradle文件抽取封装之后,便于所有的依赖管理,也便于统一版本号依赖。如下所示,抽取出assemble.gradle文件之后,每个组件下的build.gradle文件里,直接使用统一的文件即可,在代码上也是非常的清晰简单。
app下
其它Module下
具体的实现和使用,详见第二章开头都github项目。
3.5 ARouter框架实现各个子模块之间可以跳转/通信
在组件化开发的时候,组件之间是没有依赖关系,我们不能在使用显式调用来跳转页面了,因为我们组件化的目的之一就是解决模块间的强依赖问题,这时候就需要引入“路由”的概念了。ARouter路由框架是阿里Android技术团队开源的一个高效、稳定、易用的 Android 路由框架,能够帮助开发者实现各组件间的解耦,提供多种跳转方式和参数传递功能,同时还支持自定义拦截器,非常适合用于中大型 Android 开发项目。
先附上:官方中文说明文档
3.5.1 ARouter配置
添加依赖和配置
首先在子模块下面的build.gradle做如下配置:(以下省略不相关代码,仅保留arouter相关的):
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
dependencies {
compile 'com.alibaba:arouter-api:1.5.1'
annotationProcessor 'com.alibaba:arouter-compiler:1.5.1'
...
}
其中:
- AROUTER_MODULE_NAME:是 ARouter 框架中用来描述 Module 的常量字符串,用于识别当前运行的 ARouter 的版本信息以及当前运行的是哪个 Module 的信息;
- com.alibaba:arouter-api和com.alibaba:arouter-compiler:ARouter分为 arouter-api 和 arouter-compiler 两个模块,其中:
-
arouter-api:是 ARouter 的核心 API 模块,包括了 ARouter SDK 的所有核心功能,包括路由跳转、参数传递、拦截器等。开发者需要将其引入到自己的项目中,并在项目中进行相应的配置和使用。
-
arouter-compiler:是 ARouter 的编译期注解处理器,用于在编译期扫描项目中的注解,并生成相应的路由映射表(RouterMap)。开发者需要将其引入到自己的项目中,并在项目中进行相应的配置和使用
如果你仅仅只添加arouter-compiler,你可能会发现,在引入 arouter-compiler 模块的情况下,只要在 Activity 或 Fragment 上添加 @Route 注解,也可以通过 ARouter 跳转到指定页面。但是,这只是在编译期生成了一个映射表,在加载该页面时跳转到了指定的 Activity 类,而没有 ARouter 的运行时支持,无法进行参数传递、拦截器设置等操作。因此在使用 ARouter 时,应该同时添加 arouter-compiler 和 arouter-api 两个模块,并在项目中进行相应的配置和使用,才能够充分发挥 ARouter 的功能和性能优势。
-
如果是使用Kotlin,则可以这么写:
android {
defaultConfig {
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
}
}
dependencies {
compile 'com.alibaba:arouter-api:1.5.1'
kapt "com.alibaba:arouter-compiler:1.5.1"
}
上面有两个工具,这边顺便解释下,kapt 和 annotationProcessor 都是用于注解处理的工具,不同之处在于它们的适用语言和处理方式。
使用 Gradle 插件实现路由表的自动加载
'com.alibaba.arouter'是阿里提供的ARouter注册插件,可以对路由表自动加载,默认通过扫描 dex 的方式 进行加载通过 gradle 插件进行自动注册可以缩短初始化时间解决应用加固导致无法直接访问 dex 文件,初始化失败的问题。这是可选项,建议加上;可以在根build.gradle添加:
apply plugin: 'com.alibaba.arouter'
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "com.alibaba:arouter-register:1.0.2"
}
}
添加混淆规则
如果使用了Proguard混淆,则需要在子模块下面的proguard-rules.pro文件添加:
-keep public class com.alibaba.android.arouter.routes.**{*;}
-keep public class com.alibaba.android.arouter.facade.**{*;}
-keep class * implements com.alibaba.android.arouter.facade.template.ISyringe{*;}
# 如果使用了 byType 的方式获取 Service,需添加下面规则,保护接口
-keep interface * implements com.alibaba.android.arouter.facade.template.IProvider
# 如果使用了 单类注入,即不定义接口实现 IProvider,需添加下面规则,保护实现
# -keep class * implements com.alibaba.android.arouter.facade.template.IProvider
3.5.2 初始化SDK
按官方推荐在Application的onCreate()方法添加:
override fun onCreate() {
super.onCreate()
if (isDebug()) {
ARouter.openLog() // 打印日志
ARouter.openDebug() // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(this) // 尽可能早,打印日志和开启调试模式后才能执行init(),否则这些配置在init过程中将无效
}
3.5.3 实现Activity跳转
举个场景,名为‘user’的子模块有一个名为ClientActivity,名为‘login’的子模块有一个名为 LoginActivity。此时需要在LoginActivity界面上点击一个按钮,就跳转到ClientActivity,并传入一个String类型的数据。下面代码使用kotlin编写:
Step1 LoginActivity设置路径
@Route(path = RouterPath.USER_CLIENT)
class ClientActivity : AppCompatActivity() {
@Autowired(name = "RouterPath.NAME")
var clientName: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ARouter.getInstance().inject(this)
}
}
上述代码做了2件事情:
- 为ClientActivity设置路由路径为RouterPath.USER_CLIENT,有个细节需要注意,USER_CLIENT = "/user/client",这里的命名一般建议如此命名:“/模块名/Activity名";
- 将传入的参数对象进行自动装配:通过 @Autowired 注解在属性上进行声明,并在 onCreate() 方法中调用 ARouter.getInstance().inject(this) 自动注入参数;
建议可以在公共子模块,如‘common’子模块(也就是其他子模块都会依赖这个‘common’子模块),另外起一个文件,RouterPath.java专门记录路由信息,这样‘user’子模块和‘login’子模块都可以访问到RouterPath类:
public class RouterPath {
public static final String USER_CLIENT = "/user/client";
public static final String NAME = "name";
}
Step2 LoginActivity触发跳转
只要在LoginActivity执行:
Router.build(RouterPath.USER_CLIENT)
.withString(RouterPath.NAME, “xurui”)
.navigation()
此时就可以跳转到LoginActivity界面,并且LoginActivity - clientName变量会自动装配内容为“xurui”。
3.5.4 服务跳转
首先定义一个服务接口来暴露服务,然后在服务提供方中实现该接口,并使用 @Route 注解将其注册到 ARouter 中;在服务使用方中,通过 ARouter.getInstance().navigation() 方法,传入服务接口的 Class 对象即可获取到服务对象。
Step1 暴露服务
定义一个服务接口 IClientService:
public interface IClientService extends IProvider {
boolean isLogin();
}
IProvider 是一个 ARouter 提供的服务注册接口,用于将服务接口和实现类绑定在一起,从而实现服务提供方和服务使用方之间的解耦。在 ARouter 中,服务提供方通过实现 IProvider 接口,并在接口上使用 @Route 注解将其注册到 ARouter 中。所以服务提供方可以这么写:
@Route(path = RouterPath.USER_CLIENT_SERVICE)
class ClientService : IClientService {
override fun isLogin(): Boolean {
return true
}
}
Step2 发现服务
接着服务使用方则通过调用 ARouter.getInstance().navigation() 方法获取到服务实现类对象。所以服务使用方通过下述一行代码就可以获取用户是否登录:
Boolean isLogin = ARouter.getInstance().navigation(IClientService::class.java).isLogin
通过字符串映射跳转:
除了通过‘navigation(IClientService::class.java)’的方式,还可以通过字符串映射跳转,这是一种更加灵活的方式,它可以在运行时动态指定服务的实现类或者实现对象,并将其与一个字符串映射绑定。
例如,在服务提供方中,将服务实现类或者实现对象与一个字符串映射绑定:
ARouter.getInstance().build(RouterPath.USER_CLIENT_SERVICE)
.withString("className", "com.test.ClientService")
.navigation()
在服务使用方中,通过 ARouter.getInstance().build() 方法先构建一个 ARouter 对象,然后使用 withString() 方法将服务实现类或者实现对象与一个字符串映射绑定,最后调用 navigation() 方法获取到服务对象并调用服务方法:
val className = ARouter.getInstance().build(RouterPath.USER_CLIENT_SERVICE)
.getString("className", "")
val service = Class.forName(className).newInstance() as IClientService
Boolean isLogin = service.isLogin()
Step3 预处理服务
实现 PretreatmentService 接口,并加上一个Path内容任意的注解即可实现预处理服务:
@Route(path = RouterPath.USER_CLIENT_SERVICE)
public class PretreatmentServiceImpl implements PretreatmentService {
@Override
public boolean onPretreatment(Context context, Postcard postcard) {
// 跳转前预处理,如果需要自行处理跳转,该方法返回 false 即可
}
@Override
public void init(Context context) {
}
3.5.5 其他常见API
ARouter最常见的就是Activity跳转和发现服务,当然ARouter还有其他的方法,可以参考探索Android路由框架-ARouter之基本使用,以下是官方案例::
// 构建标准的路由请求
ARouter.getInstance().build("/home/main").navigation();
// 构建标准的路由请求,并指定分组
ARouter.getInstance().build("/home/main", "ap").navigation();
// 构建标准的路由请求,通过Uri直接解析
Uri uri;
ARouter.getInstance().build(uri).navigation();
// 构建标准的路由请求,startActivityForResult
// navigation的第一个参数必须是Activity,第二个参数则是RequestCode
ARouter.getInstance().build("/home/main", "ap").navigation(this, 5);
// 直接传递Bundle
Bundle params = new Bundle();
ARouter.getInstance()
.build("/home/main")
.with(params)
.navigation();
// 指定Flag
ARouter.getInstance()
.build("/home/main")
.withFlags();
.navigation();
// 获取Fragment
Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();
// 对象传递
ARouter.getInstance()
.withObject("key", new TestObj("Jack", "Rose"))
.navigation();
// 觉得接口不够多,可以直接拿出Bundle赋值
ARouter.getInstance()
.build("/home/main")
.getExtra();
// 转场动画(常规方式)
ARouter.getInstance()
.build("/test/activity2")
.withTransition(R.anim.slide_in_bottom, R.anim.slide_out_bottom)
.navigation(this);
// 转场动画(API16+)
ActivityOptionsCompat compat = ActivityOptionsCompat.
makeScaleUpAnimation(v, v.getWidth() / 2, v.getHeight() / 2, 0, 0);
// ps. makeSceneTransitionAnimation 使用共享元素的时候,需要在navigation方法中传入当前Activity
ARouter.getInstance()
.build("/test/activity2")
.withOptionsCompat(compat)
.navigation();
// 使用绿色通道(跳过所有的拦截器)
ARouter.getInstance().build("/home/main").greenChannel().navigation();
// 使用自己的日志工具打印日志
ARouter.setLogger();
// 使用自己提供的线程池
ARouter.setExecutor();
要成为一名高级安卓APP开发工程师,只有对安卓APP完整开发流程有全面性的了解,才能在技术、产品、市场这三大模块,帮助团队找到更优的解决方案。 本专栏详细介绍安卓APP完整开发流程:配置环境--》创建工程--》工程配置--》编写代码--》引用第三方库--》多项目构建--》多Dex支持--》代码混淆--》签名/打包--》构建定制--》多渠道打包--》线上运维。 安卓系统工程师也可以参考~