Android最佳实践(译)

来自Futurice 开发者们的Android开发最佳实践,原文地址https://github.com/futurice/android-best-practices
其中的很多道理可能比较浅显,但确实是Android开发中需要注意的。读后感觉有很多有价值的地方,因此翻译出来。

本文摘要

使用Gradle及其推荐的项目结构

将密码等敏感信息保存在gradle.properties文件中

不要自己编写网络框架,使用Volley和Okhttp使用

Jackson库解析JSON数据避免使用Guava,使用轻量级的库以免方法数超出65536上限

使用Fragment来展现UI使用Activity仅用于管理Fragment

布局资源等XML文件也是程序的重要部分,需要很好的组织管理起来

使用主题和样式以避免在布局文件中反复使用同一属性

对样式文件(styles.xml)进行适当拆分,避免使用一个单一冗长的样式文件

保持colors.xml的整洁,只定义颜色变量,避免引入业务相关的命名

保持dimens.xml的整洁,只定义通用的尺寸数值

VIewGroup的布局层次不能过深

使用WebView时需要注意内存泄露问题

使用Robolectric完成单元测试,使用Robotium进行UI测试

使用Genymotion模拟器

发布项目时进行混淆处理(使用ProGuard或者DexGuard)

对于简单数据的持久化处理可以使用SharedPreference,其他情况下使用ContentProvider


Android SDK

将Android SDK放置在与IDE工具无关的位置,这样可以避免在升级和更改IDE工具的时候发生问题。使用Linux系统时,请将IDE工具和Android SDK放置在用户目录下。如果放置在系统根目录下,启动程序时可能需要sudo权限,给操作带来不便。


构建系统(Build System)

请使用gradle。Ant的局限性更大而且使用起来更为繁琐,使用gradle,可以很轻松的完成以下工作:
* 为App构建不同的发布类型
* 创建简单的脚本化的任务
* 管理、下载依赖库
* 自定义的keystores
* 更多


Android Gradle同时也由谷歌官方在持续开发维护,作为官方的构建工具,有什么理由不用呢?

项目结构

有两种可选的项目结构:传统的Ant+Eclipse的项目结构和新近的Gradle+Android Studio的项目结构。应该使用新的项目结构,如果你的项目还在使用旧的项目结构,请逐步将他们迁移到新的项目结构中。

旧的项目结构

old-structure
├─ assets
├─ libs
├─ res
├─ src
│  └─ com/futurice/project
├─ AndroidManifest.xml
├─ build.gradle
├─ project.properties
└─ proguard-rules.pro

新的项目结构

new-structure
├─ library-foobar
├─ app
│  ├─ libs
│  ├─ src
│  │  ├─ androidTest
│  │  │  └─ java
│  │  │     └─ com/futurice/project
│  │  └─ main
│  │     ├─ java
│  │     │  └─ com/futurice/project
│  │     ├─ res
│  │     └─ AndroidManifest.xml
│  ├─ build.gradle
│  └─ proguard-rules.pro
├─ build.gradle
└─ settings.gradle

主要区别在于新的项目结构拆分了源码目录(main、androidTest)。Gradle的一个理念是可以分别管理不同发布版本对应的代码,例如可以将源码目录分为”paid”和”free“两个目录,差异化地管理付费版和免费版app的代码。
将app模块单独划分可以用于区别app模块和其他库模块。settting.gradle文件管理对这些库模块的引用。


Gradle配置

整体结构参考谷歌官方文档
Google’s guide on Gradle for Android

Gradle任务相关
可以使用Gradle替代之间的各种编译脚本(python, perl, shell等等)来构建Android工程。通过Gradle的官方文档Gradle’s documentation来了解更多细节。

密码的相关问题
在工程中的build.gradle文件里需要为构建发布(release)版本定义 signingConfigs节点. 以下是一些需要避免的问题:

下面这种做法是不可取的. 以下代码都会被纳入到版本管理中,因为build.gradle文件需要纳入到版本管理中.

signingConfigs {
    release {
        storeFile file("myapp.keystore")
        storePassword "password123"
        keyAlias "thekey"
        keyPassword "password789"
    }
}

正确的做法应该是这样的, 创建一个 gradle.properties文件,并且不要将该文件纳入到版本管理中:

KEYSTORE_PASSWORD=password123
KEY_PASSWORD=password789

该文件会被gradle自动引入, 于是就可以在 build.gradle文件中这样使用 gradle.properties文件中定义的变量:

signingConfigs {
    release {
        try {
            storeFile file("myapp.keystore")
            storePassword KEYSTORE_PASSWORD
            keyAlias "thekey"
            keyPassword KEY_PASSWORD
        }
        catch (ex) {
            throw new InvalidUserDataException("You should define KEYSTORE_PASSWORD and KEY_PASSWORD in gradle.properties.")
        }
    }
}

通过maven仓库来处理依赖,而不是引入本地jar包
如果直接在项目中引入本地jar包,那么该jar包对应的依赖库版本就被定死了,比如2.1.1。手动下载jar包并且处理版本问题是非常繁琐的。依赖于托管在Maven库中的依赖库,在gradle中添加依赖时附带版本号就能很容易第管理依赖库的版本问题。Gradle也非常鼓励使用这种依赖管理方式,例如:

dependencies {
    compile 'com.squareup.okhttp:okhttp:2.2.0'
    compile 'com.squareup.okhttp:okhttp-urlconnection:2.2.0'
}

添加Maven库依赖时尽量不要使用动态的版本号
避免使用例如 2.1.+这种形式的动态版本号,这会导致构建时使用的依赖版本不可控并且因为依赖库版本不同给项目带来不易发觉且难以追查的问题。最好使用例如 2.1.1这样具体的版本号,以利于构建过程的稳定。

IDE工具和文本编辑器

不管是用什么编辑工具,都需要工具能够很好地适应项目结构
选择什么编辑工具是个人的喜好问题,只要编译工具能很好地和当前项目结构相适应就好。
当前最推荐使用的IDE工具就是Android StudioAndroid Studio,因为它是Google官方的工具,与Gradle可以完美配合,同时使用新的项目结构。
也可以使用EclipseEclipse ADT,但是因为它使用的是Ant工具和旧的项目结构,因此可能需要做相应的配置。使用Vim一类的文本编辑工具也是可以的,类似的还有Sublime Text和Emacs。使用这些工具的时候,就只能使用命令行来运行Gradle和
adb命令了。如果在Eclipse中使用Gradle出现问题,在命令行下执行Gradle命令也是一个可能的解决办法。当然还是建议能够转向使用Android Studio,这是目前最推荐的Android开发工具。
不管是用什么开发工具,请确保使用Gradle构建项目并且使用新的项目结构,并且不要把开发工具本身特有的一些文件添加到版本管理中,例如Ant的build.xml文件。特别不要忘记在Ant的构建配置更改的时候也要同步修改 build.gradle文件。对于团队中的其他开发者,也不要强行让他们更换自己的开发工具。

关于使用的一些依赖库

Jackson是一个JSON序列化-反序列化的java库,我们发现Jackson处理JSON的时候表现更好,因为它支持多种处理JSON的方式:流处理、DOM树处理、以及传统的JSON-POJO数据绑定。但是需要注意,Jackson是一个比GSON更大的库,所以这需要看情况来选择,可能选择GSON更有利于避免方法数达到65536的上限。以下是Jackson的一些相关资料:Json-smart and Boon JSON

网络、缓存和图片. 有很多经过实际检验的处理网络请求的框架,可以给予这些框架实现自己的网络请求功能。推荐使用Volley or Retrofit.Volley同样提供了图片加载和缓存的功能. 如果使用Retrofit,考虑使用Picasso来加载和缓存图片, 使用 OkHttp来提升网络请求的效率.Retrofit, Picasso以及OkHttp 都是由Square开发的, 因此他们相互之间配合的很好. 一些相关的资料:OkHttp can also be used in connection with Volley.

RxJava是一个Reactive编程库,主要用来处理异步事件。他是一个非常稳定可控的编程模型,但是有时候也会造成困惑因为它和通常的java语法有所区别。建议使用该库搭建项目框架时要审慎考量。已经有一些项目在使用RxJava,你可以与以下这些开发者联系: Timo Tuominen, Olli Salonen, Andre Medeiros, Mark Voit, Antti Lammi, Vera Izrailit, Juha Ristolainen. 这里是几篇我们编写的关于RxJava的博客: [1], [2], [3], [4].如果你之前对响应式(Rx)编程缺乏经验,可以先从把它应用到处理API响应开始。然后,逐步把它运用到简单的UI事件处理,例如点击事件或者键盘输入事件。如果你有信心将响应式编程应用到整个项目结构上,请编写一份Javadoc文档来说明其中的关键部分。请考虑团队协作中的问题,因为有些不熟悉响应式编程的开发者可能需要花一些时间才能熟悉这种编程方式。尽力让他们理解响应式编程以及你的代码。

Retrolambda是一个可以将Lamba表达式语法引用到JDK8之前版本的Java库.如果你的代码是函数语言风格(例如RxJava
)的,该库可以让你的代码更加简洁同时可读性更好。使用该库之前需要先安装JDK8,然后在Android Studio的Structure对话框中设置使用的JDK路径为JDK8的路径。同时还需要设置JAVA8_HOMEJAVA7_HOME 环境变量。之后在整个工程的根目录下的build.gradle文件中设置以下依赖:

dependencies {
    classpath 'me.tatarka:gradle-retrolambda:2.4.1'
}

对于工程中每个模块的build.gradle文件,还需要添加如下部分:

apply plugin: 'retrolambda'
android {
    compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
retrolambda {
    jdk System.getenv("JAVA8_HOME")
    oldJdk System.getenv("JAVA7_HOME")
    javaVersion JavaVersion.VERSION_1_7
}

Android Studio提供对Java8 Lamba语法的代码帮助功能,如果需要编写Lamba表达式代码,可以先从以下步骤开始:

  • 任何只有一个方法的接口都是“Lamba友好”的,并且可以折叠成更加精简的语句。
  • 如果你对于参数之类的问题不是很明确,那么可以先使用正常的Java匿名内部类写法然后使用Android Studio来把它转换成Lamba表达式写法。

警惕dex的方法数限制,避免引入过多的库对于Android应用来说,当被打包为dex文件时,有硬性的65536方法数限制[1] [2] [3]。超过方法数限制会导致编译报错。由于这个原因,尽量使用较为轻量的方法数较少的库,并使用该工具dex-method-counts来判断使用哪些库能够让方法数保持在最大上限之下。特别是要避免使用Guava库,因为它包含了13000+个方法。


关于Activity和Fragment

关于在构建Android项目结构时Activity和Fragment的使用方式和组织结构,目前还没有一个统一的观点。Square甚至还有一个完全基于View来构建Android项目的框架a library for building architectures mostly with Views, 完全不需要使用Fragment。但是目前这并不是一种普遍做法,也不是开发者社区所推荐的组织项目结构的方式。
可以简单认为Fragment就是UI的一些碎片。也就是说,Fragment一般来说是跟UI相关的。Activity可以简单认为是控制器,它的关注点在生命周期和状态管理。但是在以下场景下也有例外情况(delivering transitions between screens), 以及 fragments might be used solely as controllers。我们的建议还是小心处理这个问题并且综合考虑可能带来的影响。无论是只基于Activity的,还是只基于Fragment或View的项目结构都可能有各自的缺点。以下也仅是一些建议,而并非是绝对的准则:

  • 不要使用嵌套Fragment , 因为以下问题 matryoshka bugs 会出现. 只有绝对有必要的时候才使用嵌套Fragment,例如ViewPage中的某一页内部还需要一个Fragment,或者是其他已经经过谨慎思考需要使用嵌套Fragment的场景。
  • 避免在Activity中编写过多代码。尽量让Activity作为轻量级的控制器存在,主要用于管理生命周期并且处理如Trasition一类的交互。推荐使用只具有一个单一Fragment的Activity而不是仅使用Activity。
  • 将UI相关代码放置在Activity的Fragment中,这有利于重用Fragment代码。例如在tab切换几个页面或者在应用到平板产品上时(多个Fragment在同一个页面)。避免仅使用Activity而不使用Fragment,除非必须这样做。
  • 不要过度使用Android级别的API(例如Intent)来处理应用内部的通信工作。这可能会影响到Android系统或者是其他应用,造成bug或者卡顿。例如,如果某一个应用再开机后立刻启动并且使用Intent来与其他App进行通信,就会造成数秒的卡顿。

Java包结构

Java包的项目结构基本上类似于MVC模式的项目结构。在Android里,Fragment和Activity是控制器;同时,他们也是用户界面的一部分,所以他们也是视图。

由于这些原因,很难严格界定Fragment和Activity扮演的角色究竟是控制器还是视图。所以比较好的处理方式就是所有Fragment放置在一个包里。如果遵守了之前所提及的一些建议,Activity可以放到顶层的包里。如果Activity数量较多,同样应该把Activity组织到一个包里。

类似MVC类型的项目结构,同样可以建立一个models模型类的包用放置POJO类以便于处理JSON解析和API响应,还需要一个views包放置自定义view。Adapter也是一个灰色地带,兼有数据和视图的属性。Adapter需要通过getView()方法来暴露仕途,所以也可以把Adapter放置到views包下的子包当中。

一些控制器类用于整个应用范围内并且与贴近Android系统,可以将他们放置在managers包里。其他一些工具类,例如”DataUtils”,放置在utils包里。与后台交互相关的类放置在netword包里。

最后,按从贴近后台处理到贴近用户界面的顺序管理包:

com.futurice.project
├─ network
├─ models
├─ managers
├─ utils
├─ fragments
└─ views
   ├─ adapters
   ├─ actionbar
   ├─ widgets
   └─ notifications

Resources资源相关

资源文件命名:最好使用资源类型作为名称前缀。例如fragment_contact_detail.xmlview_primary_button.xml
组织好布局资源文件:以下使一些建议
- 每个属性一行,前置4个空格
- android:id作为第一个属性
- android:layout_xxxx属性放置在前面
- style属性放置在最后
- 使用\>关闭标签
- 不要硬编码类似于android:text这类树形,使用Android Studio的设计专用属性

良好的布局xml文件应该像以下这样:


<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <TextView
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:text="@string/name"
        style="@style/FancyText"
        />

    <include layout="@layout/reusable_part" />

LinearLayout>

例如android:layout_xxxx这种属性应该定义在布局xml里,其他的一些属性应该定义在style xml文件里。当然也有一些例外的情况,不过总体来说应该这样。推荐的原则是将布局(位置、边距、大小)以及内容(文字)类型的属性在布局xml中设置,其他与外观相关的属性(颜色、内边距、字体)应该定义在style xml中。
使用样式:所有的项目都应该使用样式,因为很多视图的外观都需要复用。
将style xml拆分:不要只使用一个styles.xml文件。Android SDK对样式xml文件的命名没有要求,只要是