Android组件化开发

最近公司在做一款新的车机 Launcher,需要将一个类似QQ音乐、喜马拉雅的音频模块放入其中,整体作为一个 Launcher,虽然产品一再确定,后面不会进行拆分,但是小心为上,将 Launcher 和 音频软件分为两个 App 开发,两个团队开发互不影响,最后通过组件化,作为 module 引入到空壳App中。

整体思路

这里写个组件化二维码扫描的 Demo ,①空壳 App,②公共 Library,③第一个 App 类似于上面说的 Launcher,④第二个 App 类似于上面说的音频 App。



其中③、④均是可单独运行的 module,都依赖于②,当运行①时,需要将③、④转换为 library 去依赖。

那么需要解决的第一个问题就是,如何在 app 和 library 直接切换。

切换 application 和 library 属性

切换 module 的 application 和 library,需要在 gradle.properties 里面进行配置,因为这里面的变量都是全局的,全部 gradle 都可以取到。


gradle.properties

在最后一行增加一个标记变量 isModule=true,我这里用 true 表示 application,false 表示 library。

# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
isModule=false

下面就可以修改 module 的 build.gradle 判断操作了。

if (isModule.toBoolean()) {// ① 切换
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
apply plugin: 'com.jakewharton.butterknife'

def config = rootProject.ext// 定义变量
android {
    compileSdkVersion config.android.compileSdkVersion
    defaultConfig {
        if (isModule.toBoolean()) {// ② library没有applicationId
            applicationId "com.ff.modulea"
        }
        minSdkVersion config.android.minSdkVersion
        targetSdkVersion config.android.targetSdkVersion
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    sourceSets {
        main {
            // ③ 加载不同位置的AndroidManifest
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }

    compileOptions {
        // ButterKnife 需要Java 8
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    lintOptions {
        // 禁用Google Search
        disable 'GoogleAppIndexingWarning'
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    annotationProcessor "com.jakewharton:butterknife-compiler:$config.dependencies.butterknife"
    api project(':baselib')
}

主要看上面①、②、③ 处代码,主要说下③,可以通过修改 SourceSets 中的属性,修改 AndroidManifest 默认的加载路径,更多SourceSets介绍与使用

先看下作为 library 时的 AndroidManifest,上图①处。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.ff.moduleb">

    <application>
        <activity
            android:name="com.ff.moduleb.CaptureActivity"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:screenOrientation="portrait" />
    </application>

</manifest>

再对比下作为 application 的 AndroidManifest,上图②处。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.ff.moduleb">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".CaptureActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>

为什么需要加载不同的 AndroidManifest ?
一是,因为 library 清单文件不需要指明 application 内容;二是,并不是每个 Activity 都是 App 第一个启动的 Activity。

还需要注意一点,由于组件化,可能会导致每个 module 之间依赖的远程仓库版本不一致,出现异常情况,所以这里使用 config.gradle 统一配置版本。

ext {

    android = [
            compileSdkVersion: 28,
            minSdkVersion    : 19,
            targetSdkVersion : 28
    ]

    dependencies = [
            arouter_api     : "1.4.1",
            arouter_compiler: "1.2.2",
            butterknife     : "9.0.0",
            zxing           : "3.3.3"
    ]

    supportVersion = "28.0.0"
}

只需要在项目的 build.gradle 中引入即可使用,更多详细介绍

// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from: "config.gradle"// 引入config.gradle
buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.2'
        classpath 'com.jakewharton:butterknife-gradle-plugin:9.0.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

空壳 App

里面没有任何 java 代码。



只有一个 AndroidManifest,指明 application,我们会把 BaseApplication 放在 baselib 中。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.ff.module">

    <application
        android:name="com.ff.baselib.base.BaseApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme" />

</manifest>

唯一依赖是 baselib 库中的 BaseApplication。

apply plugin: 'com.android.application'

def config = rootProject.ext
android {
    compileSdkVersion config.android.compileSdkVersion
    defaultConfig {
        applicationId "com.ff.ui"
        minSdkVersion config.android.minSdkVersion
        targetSdkVersion config.android.targetSdkVersion
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        // ButterKnife 需要Java 8
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    lintOptions {
        // 禁用Google Search
        disable 'GoogleAppIndexingWarning'
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    if (isModule.toBoolean()) {
        // 依赖baselib中的BaseApplication
        implementation project(':baselib')
    } else {
        // modulea和moduleb中依赖了baselib
        implementation project(':modulea')
        implementation project(':moduleb')
    }
}

公用的 Library

这里面可以放基类、工具类、常量、权限声明、图片网络框架等等。

我这里放入了一些常量,BaseApplication,BaseActivity,二维码扫描的 jar 包。
至于 ButterKnife 需要如何引入,可以看下 ButterKnife最新版本使用的深坑

apply plugin: 'com.android.library'
// 虽然在library中使用butterknife,但仅在BaseActivity中bind,
// 不需要寻找控件,也就不需要生成R2,所以无需添加plugin
// apply plugin: 'com.jakewharton.butterknife'

def config = rootProject.ext// 定义变量
android {
    compileSdkVersion config.android.compileSdkVersion
    defaultConfig {
        minSdkVersion config.android.minSdkVersion
        targetSdkVersion config.android.targetSdkVersion
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    api "com.android.support:appcompat-v7:$config.supportVersion"
    api "com.jakewharton:butterknife:$config.dependencies.butterknife"
    // 仅在BaseActivity中bind,不需要寻找控件,也就不需要生成java文件,无需使用annotationProcessor
    // annotationProcessor "com.alibaba:arouter-compiler:$config.dependencies.arouter_compiler"
    api files('libs/zxing.jar')
}

权限声明,可以都放在这个 AndroidManifest 中。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.ff.baselib">

    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.FLASHLIGHT" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

</manifest>

组件 module

上面已经设置好,切换 module 的 application 和 library 属性,这里就没有什么其他工作了。

注意不同 module 间的 layout 文件不要重名,不然会出现找不到的现象,类名可以重复,因为每个 module 的包名是不一样的,要是每个 module 的包名都一样我就无语了。

组件间跳转

比如我们这里需要 modulea 中的 MainActivity 需要跳转到 moduleb 中到 CaptureActivity 这个就需要使用路由框架了,这里推荐阿里开源的路由框架 ARouter,使用很便捷。

添加依赖和配置

官方示例代码:

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {// ①
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

dependencies {
    // 替换成最新版本, 需要注意的是api
    // 要与compiler匹配使用,均使用最新版可以保证兼容
    compile 'com.alibaba:arouter-api:x.x.x'// ②
    annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'// ②
    ...
}
  1. 首先,要注意的是①、③处代码,需要同时出现,不然会报错:
ARouter::Compiler >>> No module name, for more information, look at gradle log.
  1. 在新版本中需要使用 api 代替 compiler:
dependencies {
    api 'com.alibaba:arouter-api:x.x.x'
    ...
}
  1. com.alibaba:arouter-api 中的 v4 包是 25 的,与我引入的 v7 包冲突(一般 v7 都包含 v4),所以需要使用 exclude 移除 com.alibaba:arouter-api 里面的 v4 包:
dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    api "com.android.support:appcompat-v7:$config.supportVersion"
    // arouter-api中包含了v4包,与上面v7包中的v4冲突了
    api("com.alibaba:arouter-api:$config.dependencies.arouter_api") {
        // 默认情况下v7中是包含V4包的,exclude的意思是去除v4包,这样就可以解决冲突了
        exclude module: 'support-v4'// 根据组件名排除
        exclude group: 'com.android.support'// 根据包名排除
    }
}

关于 exclude 可以看 com.android.support版本冲突的解决办法

添加注解

官方示例代码:

// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}

初始化SDK

官方示例代码:

if (isDebug()) {           // 这两行必须写在init之前,否则这些配置在init过程中将无效
    ARouter.openLog();     // 打印日志
    ARouter.openDebug();   // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化

发起路由操作

官方示例代码:

// 1. 应用内简单的跳转(通过URL跳转在'进阶用法'中)
ARouter.getInstance().build("/test/activity").navigation();

// 2. 跳转并携带参数
ARouter.getInstance().build("/test/1")
            .withLong("key1", 666L)
            .withString("key3", "888")
            .withObject("key4", new Test("Jack", "Rose"))
            .navigation();

具体的使用,包括 startActivityForResult 的实现,就不在这里粘出来了,可以下载demo看下。
github项目地址

其他注意点

组件化开发中,资源文件不要重名,建议使用组建名作为前缀。

参考资料

Android组件化方案
Android组件化初探
解决v4,v7包冲突问题
探索Android路由框架-ARouter之基本使用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容