Android项目编码规范

本文翻译自 GitHub 上一篇个人认为比较好的 Android 项目规范文档: https://github.com/ribot/android-guidelines/blob/master/project_and_code_guidelines.md

每个项目都应该有编码规范,俗话说的好:”无规矩不成方圆”。没有编码规范的项目,恩,谁维护谁知道…反正我是深有体会。废话少说,本篇博文将会从几大模块介绍作为一个 Android 开发人员,应该遵循的。

文件命名

类命名

继承自某一组件的 java 类的命名方式因该以该组件为后缀,比如 SignInActivity, SignInFragment, ImageUploaderService, ChangePasswordDialog.

资源文件

资源文件应以下划线 _ 来分割。

Drawable文件

应遵守下面的规范:

种类 前缀 命名规范
Action bar ab_ ab_stacked.9.png
Button btn_ btn_send_pressed.9.png
Dialog dialog_ dialog_top.9.png
Divider divider_ divider_horizontal.9.png
Icon ic_ ic_star.png
Menu menu_ menu_submenu_bg.9.png
Notification notification_ notification_bg.9.png
Tabs tab_ tab_pressed.9.png

icons 相关命名(摘自Android iconography guidelines):

种类 前缀 命名规范
Icons ic_ ic_star.png
Launcher icons ic_launcher_ ic_launcher_calendar.png
Menu icons and Action Bar icons ic_menu_ ic_menu_archive.png
Status bar icons ic_stat_notify_ ic_stat_notify_msg.png
Tab icons ic_tab_ ic_tab_recent.png
Dialog icons ic_dialog_ ic_dialog_info.png

selector 相关:

种类 后缀 命名规范
Normal _normal btn_order_normal.9.png
Pressed _pressed btn_order_pressed.9.png
Focused _focused btn_order_focused.9.png
Disabled _normal btn_order_normal.9.png
Selected _selected btn_order_selected.9.png

Layout 文件

布局文件应该与 Android 组件文件名匹配。比如说你为 SignInActivity 创建布局文件,布局文件应该是这样: activity_sign_in.xml

组件 文件名 布局文件命名方式
Activity UserProfileActivity activity_user_profile.xml
Fragment SignUpFragment fragment_sign_up.xml
Dialog ChangePasswordDialog dialog_change_password.xml
AdapterView item item_person.xml
Partial layout partial_stats_bar.xml

Note: 需要注意的是,当你为 adapter 创建布局文件时,应该以 item_ 为前缀;另一个就是包含在布局文件的子布局文件,应以 partial_ 为前缀。

Menu 文件与布局文件命名风格一样,比如说你准备为 UserActivity 创建 menu 文件,命名方式应是这样:activity_user.xml

你也许会疑惑,为何不以 menu_ 为前缀?大可不必这样做,因为这些文件都被包含在 menu 文件夹下了。

Values 文件

Values 文件的命名应该是以复数的形式来命名,例如 strings.xml, styles.xml, colors.xml, dimens.xml, attrs.xml

代码规范

Java 语言规则

不要无视异常

比如下面这段代码:

1
2
3
4
5
void setServerPort(String value) {
try {
serverPort = Integer.parseInt(value);
} catch (NumberFormatException e) { }
}

也许你在想,依据当时的业务场景,这种异常根本不会存在,不处理也无可厚非。明天将会发生什么,谁也不知道,你的这种行为也只是为后人挖了个坑,总有一天,有人会掉进去。严谨的代码是必须处理这些异常的,至于具体如何处理,因情况而定。

更具体参见这里

不要捕获父类的异常

你不应该写出下面的代码:

1
2
3
4
5
6
7
8
try {
someComplicatedIOFunction(); // may throw IOException
someComplicatedParsingFunction(); // may throw ParsingException
someComplicatedSecurityFunction(); // may throw SecurityException
// phew, made it all the way
} catch (Exception e) { // I'll just catch all exceptions
handleError(); // with one generic handler!
}

至于为何要这么做,以及更详尽的信息,参见这里

Don’t use finalizers

We don’t use finalizers. There are no guarantees as to when a finalizer will be called, or even that it will be called at all. In most cases, you can do what you need from a finalizer with good exception handling. If you absolutely need it, define a close() method (or the like) and document exactly when that method needs to be called. See InputStream for an example. In this case it is appropriate but not required to print a short log message from the finalizer, as long as it is not expected to flood the logs。-(Android code style guidelines

完整包引入

你不应该这样做:import foo.*;
你应该这样做:import foo.Bar;

具体信息参见这里

Java style rules

字段定义和命名

变量应被定义在文件的顶部,他们应该遵循下面列出的命名规则:

  • 私有的,非静态的变量应该以 m 开头。
  • 私有的,静态的变量以 s 开头。
  • 其他类型的小写开头,驼峰式命名。
  • static final类型的变量均已大写,下划线分割的方式。

如下:

1
2
3
4
5
6
7
8
public class MyClass {
public static final int SOME_CONSTANT = 42;
public int publicField;
private static MyClass sSingleton;
int mPackagePrivate;
private int mPrivate;
protected int mProtected;
}

驼峰式

正确 错误
XmlHttpRequest XMLHTTPRequest
getCustomerId getCustomerID
String url String URL
long id long ID

空格缩进

代码块内缩进使用 4个空格:

1
2
3
if (x == 1) {
x++;
}

行缩进使用 8个空格:

1
2
Instrument i =
someLongExpression(that, wouldNotFit, on, one, line);

Use standard brace style

Braces go on the same line as the code before them.

1
2
3
4
5
6
7
8
9
10
11
class MyClass {
int func() {
if (something) {
// ...
} else if (somethingElse) {
// ...
} else {
// ...
}
}
}

Braces around the statements are required unless the condition and the body fit on one line.

如果判断的代码只有一行,并且最长行数的代码还少的话,大括号就没有必要了:

1
if (condition) body();

而不应该写成下面这样:

1
2
if (condition)
body(); // bad!

注解

Annotations practices

依据 Android code style guide,一些 Java 中的预定义注解的标准做法如下:

  • @Override: The @Override annotation must be used whenever a method overrides the declaration or implementation from a super-class. For example, if you use the @inheritdocs Javadoc tag, and derive from a class (not an interface), you must also annotate that the method @Overrides the parent class’s method.
  • @SuppressWarnings: The @SuppressWarnings annotation should only be used under circumstances where it is impossible to eliminate a warning. If a warning passes this “impossible to eliminate” test, the @SuppressWarnings annotation must be used, so as to ensure that all warnings reflect actual problems in the code.

更多信息参见这里

注解style

类, 方法和构造器:

当一个类,或者方法,或者构造器中运用到注解,则注解应该下注释的下面,且一行一注解的原则:

1
2
3
4
/* This is the documentation block about the class */
@AnnotationA
@AnnotationB
public class MyAnnotatedClass { }

字段上的注解:

对于字段上的注解应该和该字段处于同一行,除非达到最大行数:

1
@Nullable @Mock DataManager mDataManager;

变量作用域

局部变量的作用域我们应保持到最小。如此,可以提升你代码的可读性和维护性,降低bug出现的可能性。

局部变量应被声明在第一次使用的地方。且每个变量都应初始化。

import语句排序

如果你正在使用的 IDE 是 android studio,你无须担心排序的问题,应该 android studio 已经自动帮你干完了.否则,你需要看看下面排序方式:

  • 1.Android imports
  • 2.Imports from third parties (com, junit, net, org)
  • 3.java and javax
  • 4.Same project imports

另:

  • 每个分组中还需按字母表的排序, 且大写字母在小写字母的前面 (如: Z 在 a 之前).
  • 分组和分组之间应该空一行 (android, com, junit, net, org, java, javax).

更多信息

日志

使用 Log 类提供的方法能够让我们来发现问题所在:

  • Log.v(String tag, String msg) (verbose)
  • Log.d(String tag, String msg) (debug)
  • Log.i(String tag, String msg) (information)
  • Log.w(String tag, String msg) (warning)
  • Log.e(String tag, String msg) (error)

通常情况下, 我们在类的顶部定义一个 static final 的 TAG 变量,该变量为所在类的类名,具体如下:

1
2
3
4
5
6
7
public class MyClass {
private static final String TAG = MyClass.class.getSimpleName();

public myMethod() {
Log.e(TAG, "My error message");
}
}

VERBOSE 和 DEBUG 日志需在 app 发行版本中禁用. 同样还建议你将 INFORMATION, WARNING 和 ERROR 日志禁用掉,当然这个看你自己,如果你觉得这些信息对于发行版本的 app 的 bug 有用处的话,你可以选择不禁用,这个时候你需要确保这些日志不会泄露用户的隐私信息(比如: email, 地址,用户名,密码等)。

仅在调试版本打印日志:

1
if (BuildConfig.DEBUG) Log.d(TAG, "The value of x is " + x);

类成员变量排序

使用正确的顺序将会显著提高你代码的可读性。这里推荐你使用下面的排序:

  • 1.常量(Constants)
  • 2.成员变量(Fields)
  • 3.构造器(Constructors)
  • 4.重写的方法和回调方法(public 或者 private)
  • 5.Public 方法
  • 6.Private 方法
  • 7.内部类和接口

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MainActivity extends Activity {

private String mTitle;
private TextView mTextViewTitle;

public void setTitle(String title) {
mTitle = title;
}

@Override
public void onCreate() {
...
}

private void setUpView() {
...
}

static class AnInnerClass {

}

}

如果你写的类继承自 Activity 和 Fragment 这样的 android 组件,对于重写的方法,最好和组件的生命周期相匹配。比如,你写的 activity 实现了 onCreate(), onDestroy()onPause(), 和 onResume(), 那么正确的排序是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainActivity extends Activity {

//Order matches Activity lifecycle
@Override
public void onCreate() {}

@Override
public void onResume() {}

@Override
public void onPause() {}

@Override
public void onDestroy() {}

}

方法中参数的排序

在 Android 编程中,定义大部分的方法都可能需要一个 context 参数,如果正在定义这样一个方法,context 需要放在第一位。
而若存在回调接口,则需要放在最后一位,比如:

1
2
3
4
5
// Context always goes first
public User loadUser(Context context, int userId);

// Callbacks always go last
public void loadUserAsync(Context context, int userId, UserCallback callback);

字符串

在 Android SDK 中的 SharedPreferences, Bundle, Intent 都大量使用了字符串常量,所以说,即使一个很小的 app,也有可能定义很多字符串常量。

如何你需要定义字符串常量,你需要将其定义为 static final,并加上下面的前缀:

所属 变量前缀
SharedPreferences PREF_
Bundle BUNDLE_
Fragment Arguments ARGUMENT_
Intent Extra EXTRA_
Intent Action ACTION_

需要注意的是 Fragment 中的arguments : Fragment.getArguments(),它同样是一个 Bundle.然而,因为它太常见了,我们需要为其声明一个不一样的前缀,如下:

1
2
3
4
5
6
7
8
// Note the value of the field is the same as the name to avoid duplication issues
static final String PREF_EMAIL = "PREF_EMAIL";
static final String BUNDLE_AGE = "BUNDLE_AGE";
static final String ARGUMENT_USER_ID = "ARGUMENT_USER_ID";

// Intent-related items use full package name as value
static final String EXTRA_SURNAME = "com.myapp.extras.EXTRA_SURNAME";
static final String ACTION_OPEN_USER = "com.myapp.action.ACTION_OPEN_USER";

Fragments和Activities中的参数

当我们通过 Intent 或者 Bundle 向 activity 传递参数时,对于不同参数的 key 应遵循上一节中的规则。
当 activity 或者 Fragment 需要传递参数,它需要提供一个 public static 方法以便接下来该干什么。

Activity中的此方法通常命名为 getStartIntent():

1
2
3
4
5
public static Intent getStartIntent(Context context, User user) {
Intent intent = new Intent(context, ThisActivity.class);
intent.putParcelableExtra(EXTRA_USER, user);
return intent;
}

Fragments 中的此方法通常命名为: newInstance():

1
2
3
4
5
6
7
public static UserFragment newInstance(User user) {
UserFragment fragment = new UserFragment;
Bundle args = new Bundle();
args.putParcelable(ARGUMENT_USER, user);
fragment.setArguments(args)
return fragment;
}

注意1:: 此方法要放置在 onCreate() 方法之前。

注意2:: 如果我们声明了上面的方法,那么参数的 key 应该是 private 的。它们没有必要暴露给其他类。

代码行长度限制

每一行的代码不应超过 100 个字节。如果超过 100 个字节,这里有两种方法来减少代码行的长度:

  • 提取局部变量或方法(优先考虑)
  • 将当行转换为多行

但也有列外,下面这些情况,你可不必换行::

  • 不太可能被分割的行, 比如: 很长的 url 地址.
  • 包声明和包引入语句。

在哪里换行?

对于在哪里换行,其实,也没有一个特别合理的解决方案。但,下面几点还是能够给予我们参考的价值。

运算符时换行

1
2
int longName = anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne
+ theFinalOne;

=号后面

1
2
int longName =
anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne + theFinalOne;

.换行

比如说将下面的代码换行前:

1
Picasso.with(context).load("http://ribot.co.uk/images/sexyjoe.jpg").into(imageView);

换行后:

1
2
3
Picasso.with(context)
.load("http://ribot.co.uk/images/sexyjoe.jpg")
.into(imageView);

长参数换行

当一个方法拥有很长的参数时,我们应该换行:

1
loadPicture(context, "http://ribot.co.uk/images/sexyjoe.jpg", mImageViewProfilePicture, clickListener, "Title of the picture");

换行后:

1
2
3
4
5
loadPicture(context,
"http://ribot.co.uk/images/sexyjoe.jpg",
mImageViewProfilePicture,
clickListener,
"Title of the picture");

RxJava chains styling

Rx chains of operators require line-wrapping. Every operator must go in a new line and the line should be broken before the .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Observable<Location> syncLocations() {
return mDatabaseHelper.getAllLocations()
.concatMap(new Func1<Location, Observable<? extends Location>>() {
@Override
public Observable<? extends Location> call(Location location) {
return mRetrofitService.getLocation(location.id);
}
})
.retry(new Func2<Integer, Throwable, Boolean>() {
@Override
public Boolean call(Integer numRetries, Throwable throwable) {
return throwable instanceof RetrofitError;
}
});
}

XML 规范

使用头标签闭合

当一个 xml 节点中没有任何的内容,使用自闭合即可。

正确的做法:

1
2
3
4
<TextView
android:id="@+id/text_view_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

错误的做法:

1
2
3
4
5
6
<!-- Don't do this! -->
<TextView
android:id="@+id/text_view_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >

</TextView>

资源命名

resource 的 id 应该小写且下划线分割。

ID命名

节点 前缀
TextView text_
ImageView image_
Button button_
Menu menu_

ImageView范例:

1
2
3
4
5
<!-- Don't do this! -->
<ImageView
android:id="@+id/image_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

Menu范例:

1
2
3
4
5
6
<!-- Don't do this! -->
<menu>
<item
android:id="@+id/menu_done"
android:title="Done" />

</menu>

Strings

Strings 的命名应该以其所在的场景含义为前缀来命名。比如 registration_email_hintregistration_name_hint。如果一个 string,你找不到归类,你可以遵守的下面的规范:

前缀 描述
error_ 错误信息
msg_ 常规的信息
title_ 标题,如 dialog 的标题
action_ 动作,如保存或创建

Styles and Themes

根命名以大驼峰命名,其余则以小驼峰命名。

属性排序

  • 1.View Id
  • 2.Style
  • 3.布局长宽
  • 4.其他 layout 属性,按字母排序
  • 5.剩下的属性, 按字母排序

测试规范

单元测试

测试类的命名应该以该类名加上 Test 为后缀来命名。比如,我们创建一个 DatabaseHelper 的测试类,类名应该为 DatabaseHelperTest.

测试方法因该加上 @Test 注解,并且以被测试的方法名开头,后跟一个前提或预期的行为(有时候并不需要,根据测试的场景)来命名。

  • Template: @Test void methodNamePreconditionExpectedBehaviour()
  • Example: @Test void signInWithEmptyEmailFails()

有时候,一个类中包含了大量的方法,这个时候我们可以根据功能模块来分割这个类的测试类,比如说需要测试 DataManager, 这个类有大量的方法,我们就可以将其划分为 DataManagerSignInTest, DataManagerLoadUsersTest,等。

Espresso tests

Espresso 测试通常作用于 Activity,因此命名通常以被测试的 activity 的类名为开头,如: SignInActivityTest.

当你使用到 Espresso 的 API 时,通常以下面的方式换行测试:

1
2
3
onView(withId(R.id.view))
.perform(scrollTo())
.check(matches(isDisplayed()))
文章有帮助到您?不妨打赏博主一碗拉面或一杯咖啡的小费吧 :-D!