本人第一次翻译英文技术文章,深感翻译之不易。断断续续好几次,坚持要将这篇文章翻译完。水平有限,有不足之处还请提出.
原文地址:Best practices in Android development
目录:
- 使用Gradle和它推荐的项目结构。
- 将密码和敏感数据放在gradle.properties中。
- 使用Jackson库来解析JSON数据。
- 不要自己写HTTP client,使用Volley或者OKHttp库。
- 由于65k的方法限制,避免使用Guava库并使用尽可能少的库。
- 用Fragment来显示UI界面。
- Activity只用于管理Fragment。
- layout xml文件也是代码,好好管理它们。
- 使用style来避免在layout xml中的重复属性。
- 使用多个style文件来避免一个单独而巨大的style文件。
- 保持你的colors.xml简短干净,只定义调色板颜色。
- 同样保持你的dimens.xml干净,定义一般的常量。
- 不要做深层级布局结构的ViewGroup。
- 避免客户端处理WebView的内容,并注意内存泄露。
- 使用Robolectric进行单元测试,使用Robotium进行连接设备(UI)的测试。
- 使用Genymotion作为你的模拟器。
- 一直使用ProGuard或者DexGuard。
- 对简单的长久保存,使用SharedPreference。否则使用ContentProvider来保存。
- 使用Stetho来调试你的应用。
Android SDK
把你的Android SDK文件夹放在你的主目录或者其他独立于应用的位置。当安装了一些带有SDK的IDE时,可能会将SDK放到IDE同一目录下。当你需要升级(或者重装)、或者改变IDE时SDK可能会损坏。也避免将SDK放在另一个系统级的目录下,因为如果你的IDE运行在你的用户界别而非root界别的权限时,这可能需要sudo权限。
构建系统
你的构建系统首选应该是Gradle,Ant构建系统有很多限制而且也很啰嗦。而Gradle的简单之处在于:
- 为你的APP构建不同的风格或变化。
- 创建简单的类脚本任务 。
- 管理和下载依赖库
- 自定义keystore
- 其他好处
Google还在积极地修改Android的Gradle插件使其成为全新的标准构建系统。
项目结构
当前有两种流行的项目结构:比较古老的Ant(Eclipse ADT的项目结构)和新出现的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
主要的不同之处在于新的项目结构明确分离了‘source sets’(main,androidTest),这是一个来自Gradle的概念。比如,你可以添加资源配置‘paid’和‘free’到src
中,添加后你的app将会有支付和免费风味的资源代码。
有一个顶层级别app目录用于区分你的app和其他被你的APP引用的library项目(比如上面的library-foobar)。setting.gradle
会对这些library项目保持引用,并使app/build.gradle
可以引用这些library。
Gradle配置
一般结构:详看Google’s guide on Gradle for Android
小的任务,替代shell, Python, Perl等等这些脚本,你可以在Gradle里完成任务。详见Gradle’s documentation
密码:在你的app目录下的build.gradle中你要为发布构建来定义signingConfigs,下面是你需要避免的。
别像下面代码那样做,这将在版本控制系统中将密码显示出来。
signingConfigs {
release {
storeFile file("myapp.keystore")
storePassword "password123"
keyAlias "thekey"
keyPassword "password789"
}
}
应该创建一个不会被添加到版本控制系统中的gradle.properties文件
KEYSTORE_PASSWORD=password123
KEY_PASSWORD=password789
以上文件将会被Gradle自动导入,所以你可以在build.gradle中使用它。如下所示:
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 dependency解析来替代导入jar文件。如果在你的项目中包含有jar文件,它们会有一些特殊固定的版本,比如2.1.1.下载jar并处理它的更新是很麻烦的。Maven很好地解决了这一问题,并鼓励在Android 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这种静态版本可以帮助我们创建一个更加稳定、可预见和可重复的开发环境。
为non-release builds使用不同的包名。使用applicationIdSuffix
调试构建类型,它能够在同一台设备上安装debug和release版本的apk(如果你需要,对于自定义构建类型也可以这么做)。在APP上线后,这将做对APP的生命周期也是特别有用的。
android {
buildTypes {
debug {
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
}
release {
// ...
}
}
}
使用不同的图标来区分装在同一台设备上的不同构建方式的APP——比如对图标使用不同的颜色或者在图标上覆盖一个标签。Gradle让这一切变得简单。在默认的项目结构下,将debug的图标放在app/src/bebug/res目录下而发布版本图标放在app/src/release/res目录下。你也可以根据构建类型改变APP的命名以及版本号。
IDE和文本编辑器
无论使用什么编辑器,项目结构必须美观。用什么编辑器是个人喜好,根据项目结构和构建系统找到适合你的编辑器是你的责任。
当下最值得推荐的IDE是Android Studio,因为这是Google开发的、与Gradle紧密合作的、默认使用了新的项目结构的、趋于稳定的并且是为Android开发量身定制的IDE。
使用Eclipse ADT作为Android开发不再是个好的选择了。因为Google在2015年末已经停止了ADT的支持并呼吁用户尽早转移到Android Studio上来。你仍可以使用Eclipse,但是由于它的使用老的项目结构并使用Ant来构建,你需要配置它后才能用Gradle工作。如果配置失败则需要使用命令行来build。
你甚至可以用一个像Vim、Sublime Text或者Emacs这类普通的文本编辑器。那样,你将需要在命令行中使用Gradle和adb。
无论你用什么,Gradle和新的项目结构仍然是构建应用的官方方法。避免添加你的特殊编辑配置文件到版本控制系统。例如,不要添加一个Ant的build。xml文件.尤其不要忘记保持build.gradle的更新和功能如果你在Ant中修改了配置。优待其他开发者,别强迫他们去改变他们更喜欢的工具。
Libraries
Jackson是一个能将JSON与对象相互转换的Java库。对于解决这个问题,Gson是一个普遍的选择。然而我们发现Jackson有更好的性能自从它支持了可供选择的处理JSON方式:流、树形内存模型和传统的JSON-POJO数据绑定。记住上面所说的,尽管Jackson比Gson更大,在你的项目中你或许也喜欢用Gson来避免65k的方法限制。其他解析JSON的替代库:Json-smart和Boon JSON
网络、缓存和图片。有许多向后端服务器发送请求的交互解决方案,你应该考虑使用哪种方式实现你的连接。使用Volley或者Retrofit。Volley还能帮你加载和缓存图片。假如你选择Retrofit,考虑使用Picasso来加载和缓存图片,并使用OkHttp来处理HTTP请求。所有这三个Retrofit、Picasso和OkHttp都是一家公司写的,所以它们能很好的相互配合。OkHttp也能被用在Volley的网络连接上。
RxJava是相应式编程的库,或者说是处理异步线程的库。它是一个强大而有前景的库。由于它很难掌握所以他经常会变得很混乱。我们建议在使用这个库来设计整个应用之前需要谨慎行事。我们有许多用RxJava做完的项目,如果你需要帮助和这些人中的一个聊聊吧:Olli Salonen, Andre Medeiros, Mark Voit, Antti Lammi, Vera Izrailit, Juha Ristolainen.我们已经写了一个博客贴在这里。[1],[2],[3],[4].
如果你之前对Rx毫无经验,从API响应开始使用它,或者从简单的UI事件处理开始使用它:比如在一个搜索区域点击事件或者输入事件。如果你对的Rx技术有足够自信并想要在整个项目结构中都使用它,然后在所有棘手的部分都用Java代码来写。记得其他不熟悉RxJava的程序员可能在在做项目时变得十分困难。尽力帮助他们理解你的代码并一起用Rx工作。
Retrolambda是一个让Android和其他JDK8之前的插件使用Lambda表达式语法的Java库。他让你的代码保持高严密性和高可读性。如果你对一个使用了RxJava的例子使用了一个功能性的样式,使用这个库,安装JDK8,并在Android Studio的Project Structure对话框中设置JDK8作为你的SDK地址,并设置JAVA8_HOME和JAVA7_HOME环境变量。然后在你项目的根目录的build.gradle中添加:
dependencies {
classpath 'me.tatarka:gradle-retrolambda:2.4.1'
}
然后在另一个module的build.grald中添加:
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新手,建议从下面的两条建议开始做起:
- 任何借口都只有一个方法“lambda friendly”并且可以折叠成更严谨的语法。
- 如果对参数等有疑惑,写一个普通的匿名内部类,然后让Android Studio帮你将它折叠刀一个lambda钟。
当心dex方法限制,避免使用太多library库。当Android应用被打包成一个dex文件,有一个65536个引用方法的硬性限制。如果你超过了这个限制你会看到一个致命的错误。因此,使用最少量的library并使用dex-method-counts工具来判断哪个library是在这个限制下可用的。尤其不要使用Guava库,因为它包含了13000个方法。
Activity和Fragment
开发社区和Futurice的开发人员都没有对如何用Activity和Fragment构建出最佳的Android结构达成共识。Square甚至还有一个大多用View搭建应用结构的库,绕过了对Fragment的需求,但这仍然不是一个开发社区中广泛使用和值得推荐的做法。
由于Android API的历史,你可以认为Fragment是屏幕中的一个UI模块。话句话说,Fragment通常与UI有关。Activity可以被认为是控制器。由于Activity和Fragment的生命周期和状态管理让他们变得尤为重要。然而,你也许会看到他们角色的变化:Activity可能被当做UI界面(提供屏幕间的转换)而Fragment可能被当做控制器。我们建议谨慎行事并做出明智的决定因为选择只使用Activity、只使用Fragment或者只使用View的界面结构都会有所缺陷。所以应该同时使用这些UI界面。这里有一些需要注意的建议:
- 避免使用嵌套Fragment因为这会引起matryoshka bugs,只有当嵌套Fragment是有意义的(比如在当做屏幕的Fragment中有个水平滑动的带有Fragment的Viewpager)或者当嵌套Fragment是一个好主意时才使用它。
- 避免在Activity中有太多代码。如果可以,保持他们主要作用是作为一个轻量级的容器、用于控制Activity在应用中的生命周期或实现其他重要的Android 接口API。最好用单个Fragment的Activity代替单个Activity——将UI代码写在Fragment中。这使得Activity变得可复用的。这使得我们可以改变Activity在一个标签式布局或者多个Fragment模块布局中。不要写一个没有响应Fragment的Activity,除非你觉得这是一个明智的决定。
- 不要滥用Android-level API比如在你的app的内部工作中过于依赖Intent。你可能会影响到Android系统或者其他应用出现一些bug或卡顿现象。例如,假如你的app使用intent来做package之间的交互,如果你的app是开启启动的,你可能会在用户体验中引起几秒钟的卡顿。
Java packages architecture
Java结构对于Android应用类似于Model-View-Controller。在Android中,Fragment和Activity实际上都是controller类。另一方面说,它们也是用户界面的一部分,因此,它们也是views。
所以很难区分Fragment(或者Activity)到底是controller还是views。最好将Fragment放在它们的fragments
包中,Activity可以放在顶层包目录中就像我们建议的那样。如果你打算创建超过2到3个Activity,也可以将这些Activity放在一个activities
包中。
否则,这个结构可能看着像一个MVC结构,创建一个models
包,它包含有通过API请求获得、通过JSON解析器计算出的数据集合。然后创建一个views
包,它包含有你的自定义View、Notification、action bar views、widget等。Adapter是一个特例,包含data数据和view界面。然而,它普遍需要通过getView()
方法来导出一些view。所以你可以将adapters
放在views
包的子目录中。
有些Controller类会在整个应用中使用或者与Android系统紧密相连。这些类可以放在managers包中。其他杂七杂八的数据处理类,如“DataUtils”,放在utils
包中。负责与后台交互的类放在network
包中。
总而言之,项目结构要与后台和用户紧密联系在一起。
com.futurice.project
├─ network
├─ models
├─ managers
├─ utils
├─ fragments
└─ views
├─ adapters
├─ actionbar
├─ widgets
└─ notifications
Resources
命名,将资源类型作为前缀,就像type_foo_bar.xml
。比如:fragment_contact_details.xml
,view_primary_button.xml
,activity_main.xml
。
组织好layout.xml,如果你不确定如何定义layout.xml的格式,下面的规则可能有所帮助。
- 每行一个属性,用4个空格缩进。
android:id
总是放在第一个属性android:layout_****
属性放在上方style
属性放在下面- 结束标记
/>
单独一行放置便于点击和添加属性。 - 相比
android:text
中写内容,考虑对Android Studio尽可能使用layout-designtime-attrributes
(就是@string/text
).
<?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>
属性android:layout_****
应该在layout XML中定义。然而其他属性android:****
应该放在style XML中。也有特例,但是在一般工作中适用。这个建议就是保持只有layout属性(positioniong,margin,sizing)和content属性写在layout文件中,而保持所有的界面细节(colors,padding,font)在style文件中。
特例有以下几点:
android:id
明显应该放在layout文件中- 对于
LinearLayout
的android:orientation
一般放在layout文件中 android:text
应该放在layout文件中因为它要定义文本内容。- 有时候创建一个通用的style定义
android:layout_width
和android:layout_height
是有意义的,但是他们默认还是应该出现在layout文件中。
使用Style样式。几乎每个项目都会适当的使用style,因为界面中出现一些重复界面是很常见的。至少你应该会有一个通用的用于应用中大多数文字的style,比如:
<style name="ContentText">
<item name="android:textSize">@dimen/font_normal</item>
<item name="android:textColor">@color/basic_black</item>
</style>
用在TextView中
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/price"
style="@style/ContentText"
/>
你或许要在Button上做同样的事,但我们可以做的不止于此。预先移动到相关组中并重复的android:****
属性有一个相同的style。
用其style文件分割一个大的style文件。你不必只有一个style.xml
文件。Android SDK支持其他外部文件,文件名style
没有什么神奇的地方,只要是在文件中有<style>
这个XML标签的都可以。因此你可以有各种style文件styles.xml
,styles_home.xml
,styles_item_details.xml
。不想有些对构建系统有特殊意义的resource文件夹,在res/values
中的文件名是可以随意命名。
colors.xml
是一块调色板。在你的color.xml
中应该只有一组由颜色名字命名的RGBA值而没有其他的。不要在color.xml
中为不同类型的按钮定义默认的RGBA值。
不要这样做:
<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”,如此这个样式应该只存在于一个button style中,而不是在colors.xml
。
应该这么做:
<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审美,有必要减少使用的颜色种类。
像color.xml一样对待dimens.xml。你需要定义间距和字体大小的”调色板”,定义的原则基本上和color是一样的,下面就是一个好的dimens的例子:
<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>
你应该在布局时对margin和padding使用spacing_****
命名的通用尺寸,而不要像对待strings那样逐个将dimen值敲进去,这使得dimen更具一致性,而且使dimen易于组织和改变样式和布局。
strings.xml
用类似namespaces的关键字类命名你的string,别怕有一个两个重复的关键字。预言师复杂的,所以namespaces必须联系上下文。
Bad
<string name="network_error">Network error</string>
<string name="call_failed">Call failed</string>
<string name="map_failed">Map loading failed</string>
Good
<string name="error_message_network">Network error</string>
<string name="error_message_call">Call failed</string>
<string name="error_message_map">Map loading failed</string>
不要用全大写来写string值,坚持一种文本写法(比如首字母大写)。如果你需要string显示为全大写,使用TextView的textAllCaps
属性。
Bad
<string name="error_message_call">CALL FAILED</string>
Good
<string name="error_message_call">Call failed</string>
避免深入的View结构。有时你或许会为了安排一个界面添加一个新的LinearLayout。这种情况可能发生如下:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<RelativeLayout
...
>
<LinearLayout
...
>
<LinearLayout
...
>
<LinearLayout
...
>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>