这篇文章主要为Futurice公司Android开发者总结的经验教训。遵循这些规范可以避免无谓的重复劳动。如果对iOS或Windows Phone平台的开发感兴趣,请查看iOS最佳实践文档和Windows客户端最佳实践文档。
欢迎反馈,但请先阅读反馈规范。
将Android SDK存放在home目录或者其他跟应用开发无关的位置。一些IDE在安装时包含了SDK,这时SDK可能存放在IDE的安装目录下。而这是很不好的做法,特别是当你需要升级(或者重新安装)或更换IDE时。同时也要避免把SDK存放在系统目录下,否则,当普通用户(不是root)使用IDE时就需要获取sudo权限。
编译系统首选Gradle。相比于Gradle,Ant更加的局限并且更加繁琐。使用Gradle编译系统可以很简单的做到:
Google正积极的开发安卓Gradle插件,作为新的标准编译系统。
主要有两个主流的项目结构:旧的Ant项目结构和Eclipse ADT项目结构,较新的Gradle和Android Studio项目结构。当然选择新的项目结构。如果你的项目正在用旧的项目结构,考虑放弃旧的结构,转移到新的项目结构下吧。
旧项目结构:
1
2
3
4
5
6
7
8
9
10
|
old-structure
├─ assets
├─ libs
├─ res
├─ src
│ └─ com
/futurice/project
├─ AndroidManifest.xml
├─ build.gradle
├─ project.properties
└─ proguard-rules.pro
|
新的项目结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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)。例如,你可以在代码集src文件夹下添加’paid’和’free’文件夹,分别用于存放付费版应用代码和免费版应用的代码。
顶层app文件夹用于将你的应用和其他库(例如:library-foobar)区分开来。Settings.gradle中保存了app/build.gradle需要用到的库的引用。
普通项目结构。遵循Google安卓Gradle规范。
简单任务。可以用Gradle完成一些简单任务,而不用特地去写(shell, Python, Perl等)脚本。具体参考Gradle文档。
密码。你需要在build.gradle中配置应用发行版本的签名配置。以下这些情况是需要避免的:
不要这样做。也许你会在版本控制系统中这样做。
1
2
3
4
5
6
7
8
|
signingConfigs {
release {
storeFile
file
(
"myapp.keystore"
)
storePassword
"password123"
keyAlias
"thekey"
keyPassword
"password789"
}
}
|
换一种方式,新建一个gradle.properties文件,文件内容如下。注意,不要把Gradle.properties添加到版本控制系统中。
KEYSTORE_PASSWORD=password123
KEY_PASSWORD=password789
Gradle会自动导入gradle.properties文件,所以你可以在build.gradle中这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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完全解决了这个问题,并且,Maven可以集成在安卓Gradle编译系统中。你可以指定版本的范围,例如2.2.+,然后Maven就会自动更新到版本范围内的最新版本。例如:
1
2
3
4
5
6
7
8
9
|
dependencies {
compile
'com.netflix.rxjava:rxjava-core:0.19.+'
compile
'com.netflix.rxjava:rxjava-android:0.19.+'
compile
'com.fasterxml.jackson.core:jackson-databind:2.4.+'
compile
'com.fasterxml.jackson.core:jackson-core:2.4.+'
compile
'com.fasterxml.jackson.core:jackson-annotations:2.4.+'
compile
'com.squareup.okhttp:okhttp:2.0.+'
compile
'com.squareup.okhttp:okhttp-urlconnection:2.0.+'
}
|
不管用什么编辑器,它都必须要能够很好的显示项目结构。编译器的选择看个人喜好,但是编辑器必须要能够显示项目结构和编译。
现在最为推荐的IDE时Android Studio,因为Android Studio由Google开发,最为接近Gradle,默认使用新的项目结构,也终于发布了beta版,可以说是为Android开发量身定做的IDE。
当然你也可以使用Eclipse ADT,但是需要重新配置,因为Ecplise ADT默认使用旧的项目结构和使用Ant编译。甚至,可以使用纯文本编辑器,比如Vim, Sublime Text, 或者Emacs。如果使用纯文本编辑器,就需要在命令行中使用Gradle和adb。如果Eclipse集成Gradle后仍旧不能工作,你可以选择在命令行中编译,或者迁移至Android Studio。
不管使用什么IDE和文本编辑器,确保使用Gradle和新的项目结构来编译应用程序,同时避免把编译器的配置文件添加到版本控制系统当中。例如,避免添加Ant的配置文件build.xml。还有需要强调的一点,如果你在Ant中更改了编译配置,不要忘记更新build.gradle,使其能够完成编译。另外,对其他的开发者友好一点,不要强迫他们去改变他们的工具的偏好设置。
Jackson是一个用于将对象转换成JSON或者将JSON转换成对象的Java库。为了解决JSON和对象相互转换的问题,Gson是一个受欢迎的选择。但是我们发现,自从Jackson支持多种JSON处理方式:流,内存中的树模型和传统的JSON-POJO数据绑定,Jackson更加高效。请记住,Jackson是一个比GSON大的库,所以请根据你自己的实际情况做出选择。考虑到65K的方法空间限制,你可能会偏向于选择GSON。其他选择:Json-smart和Boon JSON
网络,缓存和图片。现在已经有许多经过实践证明的向后端服务器请求数据的解决方案。你应该考虑使用这些解决方案来实现自己的客户端。使用Volley或者Retrofit。Volley也提供了加载和缓存图片的帮助类。如果你选择Retrofit,考虑使用Picasso来加载和缓存图片,使用OkHttp来实现高效的HTTP请求。Retrofit,Picasso和OkHttp都由同一个公司实现,所以这三者契合的特别好。OkHttp也可以和Volley配套使用。
RxJava是一个用于响应式编程的库,也即是,处理异步事件的库。RxJava这个范例非常强大,而且前途光明。RxJava非常与众不同,因此使用RxJava时可能会令人迷惑。我们推荐在把RxJava部署到整个应用前先花一些时间了解RxJava。现在已经有一些项目是利用RxJava来完成的,如果你需要帮助,请向这些人询问:Timo Tuominen, Olli Salonen, Andre Medeiros, Mark Voit, Antti Lammi, Vera Izrailit, Juha Ristolainen。另外,我们也写了一些博客:[1], [2], [3], [4].
如果你没有使用Rx的经验,请从应用Rx的响应API开始。或者,从应用Rx的UI事件处理开始,比如点击事件或者在搜索框中的键盘事件。如果你对使用Rx很有信心,想要把Rx应用到整个应用程序当中,请在比较难处理、容易令人迷惑的部分写明Javadocs。记住,其他不熟悉RxJava的程序员维护项目时可能会非常困难。请尽力去帮助他去理解你的代码和Rx。
Retrolambda是一个在Android平台或者其他低于JDK8的平台上处理Lambda表达式语法的Java库。利用这个库,可以保持你的代码的整洁严谨并且具有可读性,特别是当你使用了函数式编程风格(functional style),例如使用了RxJava。使用前,先安装JDK8,在Android Studio项目结构对话框中将它设置为你的SDK路径,设置JAVA8_HOME和JAVA7_HOME环境变量,然后在项目根目录下build.gradle中增加以下内容:
1
2
3
|
dependencies {
classpath
'me.tatarka:gradle-retrolambda:2.4.+'
}
|
然后在每一个模块下的build.gradle中,增加以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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的lambda智能提示。如果你是第一次使用lambda,从以下两条规则开始:
请注意dex方法限制,避免使用过多的库。被打包成dex文件的安卓应用,都有一个硬性的限制:最多能有65536个方法引用[1] [2] [3]。如果你超出了这个限制,在编译的时候你就会看到一个严重的编译错误。因此,使用尽量少的库,并使用dex-method-counts工具来决定在保证不超出限制的前提下,有哪些库可以使用。特别要避免使用Guava库,因为它包含了超过13k个方法。
在Android应用开发中,首选Fragment来显示UI。Fragment是可重用的用户交互界面,并且可以将Fragment组合在一起。我们推荐使用Fragment来显示用户交互界面,而不是使用Activity。以下是一些理由:
虽然我们建议使用fragment,但是我们不建议大量使用嵌套的fragment,因为可能会引起“套娃式bug”(matryoshka bugs)。只在合理的情况下(例如,水平滑动的ViewPager中的fragment嵌套在一个模拟屏幕的fragment中)或者经过深思熟虑时,才使用嵌套的fragment。
从架构层面来讲,你的应用应该有一个顶层的activity,其中activity中包含了大部分的业务相关的fragment。你也可以有其他的辅助activity,只要这些activity和主activity的通信足够简单,能够通过Intent.setData()
或者Intent.setAction()
或者其他简单的方式实现即可。
Android应用程序的Java包结构可以用基本上近似于模型-视图-控制器结构。对于Android,Fragment和Activity实际上就是控制类。同时,这两者也是用户交互界面的一部分,因此,这两者也是视图。
由于上述原因,将fragment(或者activity)严格的归类为控制器或者是视图是非常困难,不合理的。所以,更合理的做法是把fragment存放在专有的fragment包内。如果你遵循了前一部分的建议,那么可以将activity存放在最顶层的包下。如果你计划创建多于2个或3个activity,那么创建一个activities包。
否则(译者注:如果没有fragment和activity),包结构看起来就是一个典型的MVC结构。有一个models包,存放主要用于JSON解析时API返回值的POJO对象;一个views包,存放你自定义的视图,通知,action bar视图和小部件等。Adapter的归类比较模糊,是处于数据和视图之间的位置。但是,一般情况下,adapter需要在getView()函数中引入一些视图,所以可以在views包下建一个adapters包来存放adpater。
一些控制类是整个应用程序都需要使用到的,也更加接近安卓系统底层。这些控制类存放在managers包下。各种数据处理类,例如「DateUtils」,存放在utils包下。负责与后端服务器进行交互的类存放在network包下。
总之,按靠近后端服务器到靠近用户的顺序排列,包结构如下:
1
2
3
4
5
6
7
8
9
10
11
|
com.futurice.project
├─ network
├─ models
├─ managers
├─ utils
├─ fragments
└─ views
├─ adapters
├─ actionbar
├─ widgets
└─ notifications
|
命名。遵循以类型作为前缀的习惯,像type_foo_bar.xml。例如:fragment_contact_details.xml,view_primary_button.xml,activity_main.xml。
管理好布局XML代码。如果你不确定如何按照一定的格式来管理XML,可以参考以下几个习惯:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
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
>
|
最重要的规则是,在布局XML中定义android:layout_****属性,而其他的android:****属性则在样式XML中定义。这个规则有例外的情况,但是大部分情况下是适用的。这个规则保证只有layout属性(positioning, margin, sizing)和内容属性在布局文件中,其他的外观属性(colors, padding, font)则定义在样式文件中。
例外的情况有:
使用样式。在项目中,重复的view的外观(译者注:重复的view属性)是很常见的,因此,基本上每个项目都需要恰当的使用样式。在一个应用程序中,至少应该有一个通用的文本内容的样式。例如:
1
2
3
4
|
<
style
name
=
"ContentText"
>
<
item
name
=
"android:textSize"
>@dimen/font_normal</
item
>
<
item
name
=
"android:textColor"
><
a
href
=
"http://www.jobbole.com/members/color/"
rel
=
"nofollow"
>@color</
a
>/basic_black</
item
>
</
style
>
|
应用到TextView当中如下:
1
2
3
4
5
6
|
<
TextView
android:layout_width
=
"wrap_content"
android:layout_height
=
"wrap_content"
android:text
=
"@string/price"
style
=
"@style/ContentText"
/>
|
你也可能需要给button按钮写一个通用的样式, 不过不要只停留在给文本内容和按钮写通用样式上。继续的深入应用这个思想,把View的相关的重复的属性写成通用的样式。
把大的样式文件分成多个小样式文件。你不一定非得只有一个styles.xml文件。Android SDK支持以非传统方式命名的样式文件。文件名styles并没有特别的作用,起作用的只是文件中的XML标签<style>。因此,一个项目中可以同时有这些样式文件styles.xml
, styles_home.xml
,styles_item_details.xml
, styles_forms.xml
。不像资源目录名那样在编译时有特殊意义,在res/values下的文件名是任意的。
colors.xml是颜色调色板。colors.xml中应该只包含一些颜色名字到RGBA颜色值的映射。不要在colors.xml中为不同的按钮定义不同的颜色。
不要像下面这样做:
1
2
3
4
5
6
7
8
9
|
<
resources
>
<
color
name
=
"button_foreground"
>#FFFFFF</
color
>
<
color
name
=
"button_background"
>#2A91BD</
color
>
<
color
name
=
"comment_background_inactive"
>#5F5F5F</
color
>
<
color
name
=
"comment_background_active"
>#939393</
color
>
<
color
name
=
"comment_foreground"
>#FFFFFF</
color
>
<
color
name
=
"comment_foreground_important"
>#FF9D2F</
color
>
...
<
color
name
=
"comment_shadow"
>#323232</
color
>
|
如果你像上面这种形式来定义颜色,你很快便开始定义重复的RGBA颜色值。这种情况下,需要改变基础色值时,工作将会变得非常复杂。并且,这些颜色定义跟上下文有关,像”button”和”comment”这些,应该在按钮的样式文件中定义,而不是在colors.xml中定义。
你可以这样做:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<
resources
>
<!-- grayscale -->
<
color
name
=
"white"
>#FFFFFF</
color
>
<
color
name
=
"gray_light"
>#DBDBDB</
color
>
<
color
name
=
"gray"
>#939393</
color
>
<
color
name
=
"gray_dark"
>#5F5F5F</
color
>
<
color
name
=
"black"
>#323232</
color
>
<!-- basic colors -->
<
color
name
=
"green"
>#27D34D</
color
>
<
color
name
=
"blue"
>#2A91BD</
color
>
<
color
name
=
"orange"
>#FF9D2F</
color
>
<
color
name
=
"red"
>#FF432F</
color
>
</
resources
>
|
像应用程序的设计者要这份颜色调色板。名字不一定非得是颜色的名字,例如”green”, “blue”等。像”brand_primary”, “brand_secondary”, “brand_negative”这中类型的名字也是完全可以接受的。以这种格式来管理颜色,在改变颜色值的时候会很方便,同时也可以很直观的看到使用了多少个不同的颜色。如果要展现一个漂亮的UI界面,减少颜色种类的使用是很重要的一点。
像管理colors.xml那样来管理dimens.xml。同样,你可以定义间隔,字体大小等属性的”调色板”,理由和管理颜色的理由一样。下面是dimens文件的一个好样例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<
resources
>
<!-- font sizes -->
<
dimen
name
=
"font_larger"
>22sp</
dimen
>
<
dimen
name
=
"font_large"
>18sp</
dimen
>
<
dimen
name
=
"font_normal"
>15sp</
dimen
>
<
dimen
name
=
"font_small"
>12sp</
dimen
>
<!-- typical spacing between two views -->
<
dimen
name
=
"spacing_huge"
>40dp</
dimen
>
<
dimen
name
=
"spacing_large"
>24dp</
dimen
>
<
dimen
name
=
"spacing_normal"
>14dp</
dimen
>
<
dimen
name
=
"spacing_small"
>10dp</
dimen
>
<
dimen
name
=
"spacing_tiny"
>4dp</
dimen
>
<!-- typical sizes of views -->
<
dimen
name
=
"button_height_tall"
>60dp</
dimen
>
<
dimen
name
=
"button_height_normal"
>40dp</
dimen
>
<
dimen
name
=
"button_height_short"
>32dp</
dimen
>
</
resources
>
|
你应该使用(译者注:dimens文件中定义的)spacing_****尺寸来实现视图布局的margin和padding属性,而不是在布局文件中硬编码属性值,这一点很像字符串的一般处理方式。这会使应用保持一致的观感,同时在管理和更改样式和布局时也更加方便。
避免深层级视图。有时候,你想要在原有的视图xml中添加一个新的LinearLayout,以此来实现一个新的视图。那么,很有可能发生下面的情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
<
LinearLayout
android:layout_width
=
"match_parent"
android:layout_height
=
"match_parent"
android:orientation
=
"vertical"
>
<
RelativeLayout
...
>
<
LinearLayout
...
>
<
LinearLayout
...
>
<
LinearLayout
...
>
</
LinearLayout
>
</
LinearLayout
>
</
LinearLayout
>
</
RelativeLayout
>
</
LinearLayout
>
|
即使你没有直接在一个布局文件中看到上述的情况,当你在把一个视图填充(在Java代码中)到另一个视图中时,上述的情况也有可能发生。
这可能引发一系列的问题。可能会有性能问题,因为在这种情况下,处理器需要处理非常复杂的UI树。另外一个更严重的错误是栈溢出错误。
因此,尽可能的减少视图的层级:学习如何使用RelativeLayout,如何优化布局和如如何使用<merge>标签。
谨慎处理与WebView相关的问题。当你必须显示一个网页时,例如一篇新闻,不要在客户端中处理HTML,更好的做法是向后端程序员请求”纯净”的HTML代码。当你把WebView绑定到activity上,而不是绑定到ApplicationContext上时,WebView也可能会泄露内存。不要使用WebView来展现简单文字或者按钮,用TextView和Button来实现。
Android SDK提供的测试框架仍旧不够完善,特别是UI测试。Android Gradle现在利用一个为安卓定制的JUnit帮助工具插件,实现了一个测试框架connectedAndroidTest来执行你创建的JUnit测试。也就是说,在进行测试时,你需要连接设备或者模拟器。请根据官方的测试指南[1] [2]来操作。
只用Robolectric来单元测试,不用于视图UI测试。为了保证开发速度,Robolectric这个测试框架致力于提供不连接设备时的测试,也即是适合于对模型和视图模型的单元测试。但是,在Robolectric的框架下测试UI是不准确,不完全的。在测试和动画,对话框相关的UI元素时,你可能会遇到一些问题。由此,你”坠入了深渊”(测试过程中看不到控制屏幕),这使测试变得非常复杂。
Robotium让写UI测试变得非常容易。在Robotium测试框架下测试UI,你不需要进行连接设备的测试,但是利用Robotium提供的大量的帮助工具,你可以非常方便的分析视图UI和控制屏幕。测试用例也非常简单,以下是一个例子:
1
2
3
4
5
|
solo.sendKey(Solo.MENU);
solo.clickOnText(
"More"
);
// searches for the first occurence of "More" and clicks on it
solo.clickOnText(
"Preferences"
);
solo.clickOnText(
"Edit File Extensions"
);
Assert.assertTrue(solo.searchText(
"rtf"
));
|
如果你以开发安卓应用为职业,那么买一个正版的Genymotion模拟器吧。相比于AVD模拟器,Genymotion模拟器具有更高的帧率。它提供了一些工具来演示你的应用,模拟网络连接质量,GPS定位等。当然,Genymotion也适合于进行连接设备的测试。(译者注:为了全面的测试)你需要买很多(但不是全部)不同的设备,因此花钱买一个正版的Genymotion模拟器会比买很多物理设备便宜很多。
注意:Genymotion模拟器不会实现所有的谷歌服务,例如Google Play商店和地图。如果你需要测试三星独有的API,那还是有必要买一个三星的设备。
一般情况下,ProGuard用于缩减和混淆安卓项目的打包代码。
是否使用Proguard取决于你的项目配置。大部分情况下,当你编译一个发行版本的apk时,你需要配置gradle来运行ProGuard。
1
2
3
4
5
6
7
8
9
10
|
buildTypes {
debug {
runProguard false
}
release {
signingConfig signingConfigs.release
runProguard true
proguardFiles 'proguard-rules.pro'
}
}
|
为了判断要保留哪些代码,忽略或者混淆哪些代码,你必须明确的指出一个或者多个代码入口。 这些代码入口一般为包含有main函数的类,Java小程序(applet),移动信息设备小程序(Midlet),activity等。你在SDK_HOME/tools/proguard/proguard-android.txt
可以找到安卓框架提供的默认配置。每个项目在my-project/app/proguard-rules.pro
中自定义的proguard规则,(译者注:执行proguard时)会被附加到默认配置上。
有一个跟ProGuard相关的常见问题,在应用程序启动时因为ClassNotFoundException
或者NoSuchFieldException
或者类似的异常而崩溃,即使你在编译命令行(例如,assmbleRelease)中成功的完成编译并且没有warning提示。不外乎以下两种情况:
查看app/build/outputs/proguard/release/usage.txt
,看造成崩溃问题的对象是否被移除了。查看app/build/outputs/proguard/release/mapping.txt
,看造成崩溃问题的对象是否被混淆了。
为了防止ProGuard剔除需要用到的类或者类成员,在你的proguard配置中添加一个keep项:
1
|
-keep class com.futurice.project.MyClass { *; }
|
为了防止ProGuard混淆一些类或者类成员,添加一个keepnames项:
1
|
-keepnames class com.futurice.project.MyClass { *; }
|
在这份ProGuard配置模板中有一些例子。在ProGuard文档中有更多的例子。
提示:把每一个发行版本的mapping.txt文件都保存下来。这样,当用户遇到一个bug,提交了一个混淆的调用栈时,便可以根据保存的mapping.txt来调试,找到问题所在。
DexGuard。如果你需要一个不错的工具来优化代码,特别是经过混淆的发行版代码,考虑使用DexGuard。DexGuard是有ProGuard团队做的一个商业软件。利用DexGuard,可以很容易的分割Dex文件,解决了65k方法空间限制的问题。
感谢Antti Lammi, Joni Karppinen, Peter Tackage, Timo Tuominen, Vera Izrailit, Vihtori Mäntylä, Mark Voit, Andre Medeiros, Paul Houghton和其他Futurice开发者分享关于安卓开发的知识。
Futurice Oy Creative Commons Attribution 4.0 International (CC BY 4.0)