Android组件化开发

作者: JamFF | 来源:发表于2019-03-24 13:36 被阅读196次

    最近公司在做一款新的车机 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之基本使用

    相关文章

      网友评论

        本文标题:Android组件化开发

        本文链接:https://www.haomeiwen.com/subject/xzbfvqtx.html