网易Android工程模板化实践

本文由网易杭州前端技术部首发。

背景

我们网易前端技术部 - 移动技术组作为公司的移动端基础技术部门,主要为其他部门提供解决方案、技术支持和产品孵化。在几年的积累过程中,我们拥有一些自己的框架和 SDK,如轻应用框架、热更新 SDK、网络请求库、本地存储库、页面管理等,服务过网易新闻、云音乐、考拉、易信等亿级产品,先后孵化过青果摄像头、二次元Gacha、严选等重要产品。

在多年的Android开发中,对于 Android 端产品开发,我们有如下几点体会:

  1. 产品孵化排期紧张

  2. 基础模块的需求具有相似性

  3. 基础模块的选型和工具类具有可重用性

  4. 网络请求的代码具有机械性

对于各个基础模块,我们团队封装了自己的 SDK,如网络库、本地存储库、页面管理库、图片库等。使用我们的Activity模板生成的初始工程,就已经包含了我们提供的基础模块,产品团队的开发不需要再花费重复的时间做技术调研、选型、SDK封装集成等工作,而只需要关心自己的业务逻辑编写。我们期望产品团队只需 1 分钟就能得到自己的初始工程,并能马上投入业务逻辑开发,既能缩短开发周期,也能保证工程代码质量。

Android 模板简介

Android Studio 提供遵循 Android 设计和开发最佳实践的代码模板,以帮助我们快速并正确地创造出漂亮的、功能齐全的应用程序代码模板。Android Studio 中提供的模板列表在不断增加,按照它们添加的组件类型(如Activity或XML文件)可对模板进行如下分组:

template menu

可通过文件->新建菜单或在项目窗口中右键单击调出上述模板菜单。

Android Studio 模板位置:

Windows 的路径在 ${android studio 安装路径}/plugins/android/lib/templates/

MacOS 的路径在 ${Android Studio.app 存放路径}/Contents/plugins/android/lib/templates/

该文件夹具体内容如下:

  • activities:Activity模板相关,如 EmptyActivity 文件夹用于创建一个空页面的模板,GoogleMapsActivity 文件夹对应创建一个地图页面的模板等

  • gradle:放置了 gradle 模板,用于在新建工程的根目录下生成 gradle 文件夹,支持用户不用安装 gradle 就能使用 gradlew 命令

  • gradle-project:工程模板相关,用于构建 module,Android Project,Java Library 等

  • other:构建文件模板等

模板最常见的用途之一是向现有应用程序模块添加新的 Activity。 activities 文件夹正是 Android Studio 默认提供的 Activity 模板,涵盖了手机和平板电脑应用中常用的 Activiy 模板。用户也可以参照已有模板自定义符合特定需求的 Activity 模板。

1. Activity 模板的文件结构

下面我们分析最简单的一个模板 EmptyActivity,我们首先查看下 EmtpyActivity (空白页面模板) 里面的内容

EmptyActivity Structrue

Android Studio 使用的是 FreeMarker 模板引擎,所以文件后缀都是 .ftl

  • globals.xml.ftl: 全局变量文件,保存一些全局变量,当中可以引用其他文件的全局变量

  • recipe.xml.ftl: 配置要引用的模板路径以及文件的生成规则

  • template.xml: 模板的配置信息,包括模板的显示图标,界面的表现,全局变量文件和执行文件的指定等

  • template_blank_activity.png: 显示的缩略图

  • SimpleActivity.java.ftl: Activity 模板文件

2. 代码生成流程

目前我们已经基本了解了一个Activity模板的文件结构了,以及每个文件大致包含的东西,简单总结如下:

  • template 中parameter标签,主要用于提供参数

  • global.xml.ftl 主要用于提供参数

  • recipe.xml.ftl 主要用于生成我们实际需要的代码,资源文件等

    例如,利用参数 + MainActivity.java.ftl -> MainActivity.java,其实就是利用参数将ftl中的变量进行替换

代码生成过程如下图所示:

13_code_generation_process.jpg

图片摘自 Tutorial How To Create Custom Android Code Templates

HTTemplate Activity 模板实现

我们编写一个Activity模板叫作:HTTemplate,内容如下:

HTTemplate Structrue

1. template.xml

指定模板名、描述、最低支持 sdk 版本、类别等,输入界面要求指定包名和 Application 类名

2. globals.xml.ftl

引用公共文件内容

<?xml version="1.0"?>
<globals>
    <global id="hasNoActionBar" type="boolean" value="false" />
    <#include "../common/common_globals.xml.ftl" />
</globals>

3. recipe.xml.ftl

<?xml version="1.0"?>
<recipe>

    <!-- nei.json -->
    <instantiate from="root/nei.json.ftl"
             to="${escapeXmlAttribute(topOut)}/nei.json" />

    <!-- manifest -->
    <merge from="root/AndroidManifest.xml.ftl"
             to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />

    <merge from="root/AndroidManifestPermissions.xml"
             to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />

    <!-- 全部资源 -->
    <copy from="root/res"
             to="${escapeXmlAttribute(resOut)}" />

    <!-- libs 库 -->
    <copy from="root/libs"
             to="${escapeXmlAttribute(projectOut)}/libs" />

    <!-- gradle -->
    <merge from="root/project_build.gradle.ftl"
             to="${escapeXmlAttribute(topOut)}/build.gradle" />

    <merge from="root/app_build.gradle.ftl"
             to="${escapeXmlAttribute(projectOut)}/build.gradle" />

    <!-- README -->
    <instantiate from="root/README.md.ftl"
             to="${escapeXmlAttribute(topOut)}/README.md" />

    <!-- proguard-rules.pro.ftl -->
    <copy from="root/proguard-rules.pro.ftl"
             to="${escapeXmlAttribute(projectOut)}/proguard-rules.pro.template" />

    <!-- java 代码 -->
    <!-- application 文件夹 -->
    <instantiate from="root/src/app_package/application/AppProfile.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/application/AppProfile.java" />


    ...

    <!-- attrs.xml -->
    <merge from="root/res/values/attrs.xml"
             to="${escapeXmlAttribute(resOut)}/values/attrs.xml" />
             
    ...

</recipe>

省略部分代码,主要的工作是

  • merge AndroidManifest.xml 文件

  • copy 或者 merge 资源文件

  • copy 或 instantiate java 代码

  • merge build.gradle 文件

  • merge settings.gradle 文件

  • copy lib 文件夹里面的全部内容

  • copy proguard-rules.pro 文件

4. root 文件夹

root Structure

放置相关模板源文件,包括一些Activity、自定义View、通用工具类等。将其中以.ftl为后缀的源代码,按照 FreeMarker 语法进行替换。例如,使用了包名的地方,需要替换成 ${packageName}:

package ${packageName}.application;
import ${packageName}.R;

在gradle文件中配置私有maven库的地址、增加公用的依赖库、关闭lint的严格检查、配置APK多渠道打包等。

5. 模板图标

添加 Activity 模板图标,并在 template.xml 中添加引用

<thumbs>
    <thumb>template_thumb.png</thumb>
</thumbs>

6. 使用 Activity 模板生成初始工程

将上述 HTTemplate 文件夹拷贝至 Android Studio 的 Activity 模板目录下:

Windows 的路径在

${android studio 安装路径}/plugins/android/lib/templates/activities

MacOS 的路径在

${Android Studio.app 存放路径}/Contents/plugins/android/lib/templates/activities

重启 Android Studio,在新建工程过程中可以看到,出现了我们自定义的 Activity 模板项:

新建项目流程之选择Activity

直接运行生成的项目,效果如下:

工程运行效果图

到这里,使用我们的 Activity 模板生成的初始工程,就已经包含了我们提供的网络库、本地存储库、页面管理库、图片库等基础模块,开发人员接下来只需专注于业务逻辑的开发。

遇到的问题及解决方案

为分析问题的原因,我们找到了 Android 模板相关源码:

Mac 平台:

${android studio安装路径}/Contents/plugins/android/lib/android.jar

Windows 平台:

${android studio安装路径}/plugins/android/lib/android.jar

以下问题的解答将涉及到部分源码。

1. ${} 通配符冲突

当工程模板实例化时,${} 会被 FreeMarker 语法处理,导致错误。

解决办法:定义 FreeMarker 转义字符如下

$ ==> ${"$"}

2. Java 代码实例化问题

模板中 java 代码较多,我们统一放在 root/src/ 文件夹下,里面有部分文件含有 FreeMarker 标签,有部分只是纯粹的 java 代码。而使用 instantiate 命令对整个文件夹进行实例化操作,并不会触发 FreeMarker 语法执行。

解决办法:因 java 文件比较多,手写 recipe.xml 标签命令繁琐且容易出错。我们通过程序递归遍历 root/src/ 下的全部代码文件,并生成相应的 instantiate 或 copy 命令。

3. copy 和 instantiate 问题

(1) gradle.properties 文件执行 copy 或者 instantiate 操作无效

分析结果:查看 DefaultRecipeExecutor.copy 与 DefaultRecipeExecutor.instantiate 源码处理逻辑,得知执行 copy 和 instantiate 命令时,如果 from 指定一个非文件夹,且目标文件存在,则不执行拷贝。而在执行我们的 Activity 模板之前,已经执行了 gradle-projects/NewAndroidProject 工程模板,并生成了 gradle.properties 文件,因此执行 copy 或 instantiate 都因目标文件已经存在而不再执行。

(2) copy 和 instantiate 对文件夹操作的区别

  • 相同点:如果 from 指定一个文件夹,都是执行 copyTemplateResource 方法,二者没有区别;如果 from 指定一个非文件夹,且目标文件存在,则不执行文件操作。

  • 不同点:copy 命令不使用 FreemarkerUtils 对 FreeMarker 语法进行处理,而 instantiate 命令先执行 FreemarkerUtils 的静态方法 processFreemarkerTemplate 来处理 FreeMarker 语法,之后再执行文件拷贝操作。

4. merge 问题

(1) proguard-rules.pro、gradle.properties 文件执行 merge 操作失败

分析结果:根据 DefaultRecipeExecutor.merge 源码的逻辑,我们得知当 to 文件不存在,则执行 copy 或 instantiate 命令;如果 to 文件存在且可读,则仅对 xml 或 gradle 才能执行 merge 操作。

解决办法:

  • 暂时生成 proguard-rules.pro.template 文件

  • 将定义在 gradle.properties 中的常量移动到 project_build.gradle.ftl 的 ext{ } 内

(2) settings.gradle 文件合并,指定 module 路径错误

执行前:

include ':hteventbus', ':htrefreshrecyclerview', ':htrecycleview', ':hthttp'

project(':hteventbus').projectDir = new File('module/hteventbus')
project(':hthttp').projectDir = new File('module/hthttp')
project(':htrefreshrecyclerview').projectDir = new File('module/htrefreshrecyclerview')
project(':htrecycleview').projectDir = new File('module/htrecycleview')

执行后报错:

RuntimeException: java.lang.RuntimeException: When merging settings.gradle files, only include directives can be merged.

分析结果:查看 RecipeMergeUtils.mergeGradleSettingsFile 源码,得知当 settings.gradle 文件合并时,只允许每行开头是 include 命令,其他情况抛出异常。

解决办法:去掉非 include 的操作代码,改用远程依赖引用这些 module,即在 dependencies{ } 中添加相应的依赖。

(3) build.gradle 文件合并,apply 语句合并错误

执行前:

apply plugin: 'com.neenbedankt.android-apt'

执行后:

apply plugin: 'com.neenbedankt.android-apt' plugin: 'com.android.application'

分析结果:查看 GradleFileMerger 中的 mergeGradleFiles 方法,实际执行的是 mergePsi 方法,根据 mergePsi 合并逻辑,apply 不是 call 语句,且 apply 的第一个子元素不是 dependencies,因此添加 plugin: 'com.neenbedankt.android-apt' 到 toRoot 的 apply 子元素前面。

解决办法:根据上面的分析,看起来 apply 的这个合成结果是 Android 模板的 bug,我们目前只能采用手工添加 apply 语句的方法。

(4) build.gradle 文件合并,dependencies{ } 内的 apt 语句消失

执行前:

dependencies {
    compile "com.netease.hearttouch:ht-universalrouter-dispatch:$HEARTTOUCH_HTROUTER_DISPATCH_VERSION"
    apt "com.netease.hearttouch:ht-universalrouter-dispatch-process:$HEARTTOUCH_HTROUTER_DISPATCH_PROCESS_VERSION"
    ...
}

执行后:

dependencies {
    compile "com.netease.hearttouch:ht-universalrouter-dispatch:$HEARTTOUCH_HTROUTER_DISPATCH_VERSION"
    ...
}

分析结果:查看 GradleFileMerger.mergeDependencies 源码,得知当 dependencies 合并时,仅处理 dependencies 中的 compile 子元素,其他如 apt、provided 命令都会被忽略掉。

解决办法:由于源码并未提供非 compile 子元素的合并方案,我们目前只能采用手工添加 apt 语句的方法。

5. 小结和后续工作

到此,基本上完成了我们原先期望实现的初始工程:

  1. 提供 ht-template 支持生成我们的模板工程

  2. 提供 Android Studio 插件 (NEIPlugin)

  • 支持 ht-template 的下载安装

  • nei-toolkit 和 Node.js 的下载安装

  • nei-toolkit 和 Node.js 的使用,生成网络请求代码

这里还是有一些因为 Android 模板自身的限制而无法完成的内容点:

  1. 无法在 settings.gradle 指定 module 路径

  2. 无法合并 proguard-rules.pro 文件,暂时生成 proguard-rules.pro.template 文件

  3. 由于 build.gradle 对 apply 命令合并会出错和无法合并 dependencies 中的 apt 命令,所以无法在 build.gradle 中集成 ht-universalrouter

再次,除了网络请求的代码编写是机械性的,基于我们的 Activity 模板生成的初始工程,在其他方面也存在代码编写的机械性:初始页面代码生成、RecycleView 中的各个 ViewHolder 类、本地数据读取保存等,而这些工作将会是我们的后续工作。

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

推荐阅读更多精彩内容

  • 视频链接 0 背景 我们网易前端技术部 - 移动技术组作为公司的移动端基础技术部门,主要为其他部门提供解决方案、技...
    zyl06阅读 2,502评论 0 17
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,050评论 25 707
  • 由于项目用上了 mvp 架构,基本上一个页面就至少需要新创建6个类,分别是 model view presente...
    大空ts翼阅读 1,933评论 0 4
  • 快速入门。 学习是为了改善自己的生活,并愿意学习任何事情。 社会上到处是速成班,教你如何快速成功。对于速成的概念,...
    谭皓匀阅读 139评论 0 1
  • 站在天桥上,身旁人来人往, 公众场合一向注意形象的我,竟然也不自觉做起了白莲花式的“捂嘴动作”,另一只拿电话的手都...
    Harderrr阅读 95评论 0 0