2. 代码规范

2.1 Java语言规范

2.1.1 绝对不忽略异常

以正确的方式处理异常。例如:

public void setUserId(String id) {
    try {
        mUserId = Integer.parseInt(id);
    } catch (NumberFormatException e) { }
}

这里将会给开发者和用户带来不清楚的消息。如果发生错误并捕获到异常时,这种写法会使得开发者很难debug并且会给用户带来困惑,如有必要提示用户并且在console记录信息让我们去debug。应如下:

public void setCount(String count) {
    try {
        count = Integer.parseInt(id);
    } catch (NumberFormatException e) {
        count = 0;
        Log.e(TAG, "There was an error parsing the count " + e);
        DialogFactory.showErrorMessage(R.string.error_message_parsing_count);
    }
}

适当处理如下的错误:

  • 显示一个消息给用户提示他们出现了一个错误。
  • 设置时有默认的选项
  • 抛出一个合适的异常

2.1.2 不要捕捉普通异常

捕捉异常时不应该有下面的情况:

public void openCustomTab(Context context, Uri uri) {
    Intent intent = buildIntent(context, uri);
    try {
        context.startActivity(intent);
    } catch (Exception e) {
        Log.e(TAG, "There was an error opening the custom tab " + e);
    }
}

为什么?

通常不要这样做,捕捉或者抛出(最好不要抛出,因为它有可能包是错误)普通异常是不恰当的,这样是非常危险的,你有可能捕捉到系统级别的错误(像RuntimeExceptions和ClassCastException),而你的代码就覆盖了他们的提示。意味着有时有新类型的异常发生在这段代码,编译器不会提示你去处理新类型的异常,多数情况下不应该用同样的方式处理不同的异常。 - 遵从Android代码规范

2.1.3 异常分组

在相同位置有不同的异常发生,为了增加代码可读性和避免重复代码,你应该如下这样做:

public void openCustomTab(Context context, @Nullable Uri uri) {
    Intent intent = buildIntent(context, uri);
    try {
        context.startActivity(intent);
    } catch (ActivityNotFoundException e) {
        Log.e(TAG, "There was an error opening the custom tab " + e);
    } catch (NullPointerException e) {
        Log.e(TAG, "There was an error opening the custom tab " + e);
    } catch (SomeOtherException e) {
        // Show some dialog
    }
}

也可以这样做:

public void openCustomTab(Context context, @Nullable Uri uri) {
    Intent intent = buildIntent(context, uri);
    try {
        context.startActivity(intent);
    } catch (ActivityNotFoundException e | NullPointerException e) {
        Log.e(TAG, "There was an error opening the custom tab " + e);
    } catch (SomeOtherException e) {
        // Show some dialog
    }
}

2.1.4 使用try-catch来抛出异常

使用try-catch语句改善发生异常代码的可读性。当异常出现是,更容易调试和处理错误。

2.1.6 禁止全部导入

当导入库时,不应该导入全部包,比如:

import android.support.v7.widget.*;

相反,应该这样:

import android.support.v7.widget.RecyclerView;

2.1.7 不要保留没使用的库

有时删除一些代码而有的库不再使用,那么应该删除导入这些库的代码。

2.2 Java代码规范

2.2.1 字段定义和命名

所有属性应在该文件的顶部被声明, 遵循下面的规则:

  • 不是静态私有属性不要使用m开始,正确示例:
userSignedIn, userNameText, acceptButton

错误示范:

mUserSignedIn, mUserNameText, mAcceptButton
  • 静态私有属性使用需要使用s开始,正确示例:
someStaticField, userNameText

错误示范:

sSomeStaticField, sUserNameText
  • 其他属性使用小写字母开头:
int numOfChildren;
String username;
  • 静态的终态(final)属性(即常量),所有字母要大写。
private static final int PAGE_COUNT = 0;

属性名称不应定义模糊,比如:

int e; //列表中的元素的数目

使用他的作用作为名称,而不是留下注释来说明。这样更好

int numberOfElements;

2.2.1.2 View属性名

当属性涉及views时,名称的最后一个单词应该是view,比如:

View Name
TextView usernameView
Button acceptLoginView
ImageView profileAvatarView
RelativeLayout profileLayout

这样我们可以轻松地确认属性属于谁。例如,有一个字段中命名为用户,他是一个非常模糊的名称。给它的命名为usernameView,userAvatarView或userProfieLayout有助于弄清楚属性属于谁。

在以前通常使用view的类型作为结尾(acceptLoginButton),但当views发生改变时,通常会忘记回到java类中去修改属性名称。

2.2.2 避免与容器类型相同

当创建一个集合变量时,我们命名应该避免使用容器类型。比如,我们有一个包含用户列表ArrayList

应该这样命名:

List<String> userIds = new ArrayList<>();

而不应该:

List<String> userIdList = new ArrayList<>();

当我们更改容器时,我们通常会忘记更改名称,就像view的命名,他是不必要的。正确的名称包含他的属性信息就足够了。

2.2.3 避免相似的名称

命名相似名称的变量、方法或类,会使其他开发人员阅读代码时产生混淆,比如:

hasUserSelectedSingleProfilePreviously
hasUserSelectedSignedProfilePreviously

乍看之下可能很难找到他们之间的区别。一个更清晰的命名方式可以使开发人员更容易操作你的代码。

2.2.4 连续数字命名

当Android Studio给我们自动创建代码时,他会使用连续数字命名,这是参数命名是可怕的!比如,这样不太好:

public void doSomething(String s1, String s2, String s3)

没有阅读注释很难了解这些参数,相反:

public void doSomething(String userName, String userEmail, String userId)

这样就很容易理解,现在我们根据代码就能够清晰的了解参数的意义🙂

2.2.5 易识别的命名

当命名属性、方法和类时应该:

  • 容易阅读:有效的命名能够阅读同时就能理解他表达的意思,减少去理解他的意义消耗的时间。

  • 容易发音:容易朗读的命名,可以避免当你和他人对话时你试图发音一个命名的尴尬。

  • 容易寻找:没有什么比试图在一个类中寻找一个被拼写错了(或严重糟糕)的方法或变量更糟糕了。如果我们试图找到搜索用户的方法,那么当搜索search时他就应该出现。

  • 不要使用匈牙利表示法:匈牙利表示法违背上述提出的三点,所以绝对不能用!

2.2.6 缩写的单词必须大写

所有的包含缩写的类、变量名等都应该使用大写来处理。比如:

Good bad
setUserId setUserID
String uri String URI
int id int ID
parseHtml parseHTML
generateXmlFile generateXMLFile

2.2.7 避免特殊整理变量申明

所有的变量申明不应该使用特殊方式来对齐。比如:

正常的代码:

private int userId = 8;
private int count = 0;
private String username = "hitherejoe";

避免这样:

private String username = "hitherejoe";
private int userId      = 8;
private int count       = 0;

这种声明方式在阅读时可能难以理解空白的字符串。

2.2.8 缩进

对于代码块,缩进应该使用4个空格:

if (userSignedIn) {
    count = 1;
}

而当包裹多行,缩进应该使用8个空格:

String userAboutText =
            "This is some text about the user and it is pretty long, can you see!"

2.2.9 If-Statements

2.2.9.1 大括号规范

大括号通常应该和他之前的代码同行,避免这样:

class SomeClass
{
    private void someFunction()
    {
        if (isSomething)
        {
        }
        else if (!isSomethingElse)
        {
        }
        else
        {
        }
    }
}

这样更好:

class SomeClass {
    private void someFunction() {
        if (isSomething) {
        } else if (!isSomethingElse) {
        } else {
        }
    }
}

特别增加一行完全没有必要,也不会更容易阅读代码。

2.2.9.2 单行if语句

有时使用单行if语句是可以的,比如:

if (user == null) return false;

然而,这只能是在简单的操作时,而这样的代码会更适合用大括号:

if (user == null) throw new IllegalArgumentExeption("Oops, user object is required.");

这样更好:

if (user == null) {
    throw new IllegalArgumentExeption("Oops, user object is required.");
}

2.2.9.3 条件嵌套

可能的情况下,如果条件能合并,就应该避免复杂的嵌套。比如:

Do:

if (userSignedIn && userId != null) {
}

Try to avoid:

if (userSignedIn) {
    if (userId != null) {
    }
}

这使得语句更容易阅读并且嵌套除去了额外的行。

2.2.9.4 三元运算

在合适的地方,使用三元运算简化操作,比如,这样更容易阅读:

userStatusImage = signedIn ? R.drawable.ic_tick : R.drawable.ic_cross;

而且使用更少的行比下面这样:

if (signedIn) {
    userStatusImage = R.drawable.ic_tick;
} else {
    userStatusImage = R.drawable.ic_cross;
}

2.2.10 预定义的注释

2.2.10.1 注释实例

Android代码风格:

@Override: @Override 注释必须是方法声明覆盖了实现的接口或父类的方法。举个例子,如果你使用@inheritdocs Javadoc标签,从一个类派生(不是实现),你也必须标注该方法@Overrides了父类的方法

@SuppressWarnings: @SuppressWarnings 注释应该只在它是不出现警告的情况下使用。如果警告你确定它不是问题,就应该使用@SuppressWarning。所以确保所有警告的代码没有问题。

注释尽可能多。比如,当你一个属性可能是空时可以用@Nullable注释,例子:

@Nullable TextView userNameText;
private void getName(@Nullable String name) { }

2.2.10.2 注释风格

所应用方法或类的注释应该在声明中定义,每个注释单独一行:

@Annotation
@AnotherAnnotation
public class SomeClass {
  @SomeAnotation
  public String getMeAString() {
  }
}

当给属性添加注释时,应该确保注释在同一行上且有多余空间。比如:

@Bind(R.id.layout_coordinator) CoordinatorLayout coordinatorLayout;
@Inject MainPresenter mainPresenter;

我们这样做是因为它使语句更易于阅读。例如,@Inject SomeComponent mSomeName意味着这个变量代表此组件

2.2.11 限制变量的作用域

局部变量的范围应保持在最低限度(Effective Java Item 29)。通过这样做,增加代码的可读性和可维护性,减少出错的可能性。每个变量都应该在封闭在所用的代码块中,外部不可以访问。

局部变量应当在其第一次使用时声明,每个局部变量申明时都要初始化。如果你没有足够的信息来初始化变量,你应该推迟申明变量,直到你要使用他。– Android代码风格

2.2.12 未使用的元素

所有未使用的属性导入(imports)方法应该从代码中删除,除非后面注释有它有任何具体的理由。

2.2.13 导入顺序

使用Android Studio时,导入会自动排序。而其他情况时也许不会自动排序,这时我们应该手动排序根据:

  1. Android库的导入
  2. 第三方库的导入
  3. J2EE和J2SE的导入
  4. 当前项目文件中的导入

注意:

  • 导入应该按照字母顺序排列,大写字母应在小写之前(比如Z在a前)。

  • 每种分组之间应该用空行隔开,(android, com, JUnit, net, org, java, javax)

2.2.14 日志(Logging)

日志应该记录在开发时有用的错误信息或其他信息。

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

我们可以在类开始位置设置static finalTAG属性让log使用。例子:

private static final String TAG = MyActivity.class.getName();

在发布的版本中不应该记录verbosedebug级别的日志信息。其他级别的information warning error日志信息还是应该继续记录。

if (BuildConfig.DEBUG) {
    Log.d(TAG, "Here's a log message");
}

注意: Timber在使用时是优于Log方法的。他为我们处理TAG,不是我们去设定TAG。

2.2.15 属性的排序

在类文件的顶部声明的任何属性应该按以下顺序进行排序:

  1. 枚举类型(Enums)
  2. 常量
  3. 依赖注入
  4. 注解式绑定控件
  5. 私有全局变量
  6. 公共全局变量

For example:

public static enum {
    ENUM_ONE, ENUM_TWO
}
public static final String KEY_NAME = "KEY_NAME";
public static final int COUNT_USER = 0;
@Inject SomeAdapter someAdapter;
@BindView(R.id.text_name) TextView nameText;
@BindView(R.id.image_photo) ImageView photoImage;
private int userCount;
private String errorMessage;
public int someCount;
public String someString;

使用该排序习惯有助于字段声明进行分组,从而方便属性的定位和可读性。

2.2.16 类成员排序

为了改善代码可读性,按一定逻辑的对类成员进行排序是非常重要的。应该按以下规则来排序:

  1. 常量
  2. 属性
  3. 构造函数
  4. 覆盖的方法和回调方法(私有或公共的)
  5. 公共方法
  6. 私有方法
  7. 内部类或内部接口

实例:

public class MainActivity extends Activity {
    private int count;
    public static newInstance() { }
    @Override
    public void onCreate() { }
    public setUsername() { }
    private void setupUsername() { }
    static class AnInnerClass { }
    interface SomeInterface { }
}

所有的生命周期方法应该按照安卓框架类定义的组件的生命周期来排序,比如:

public class MainActivity extends Activity {
    // Field and constructors
    @Override
    public void onCreate() { }
    @Override
    public void onStart() { }
    @Override
    public void onResume() { }
    @Override
    public void onPause() { }
    @Override
    public void onStop() { }
    @Override
    public void onRestart() { }
    @Override
    public void onDestroy() { }
    // public methods, private methods, inner classes and interfaces
}

2.2.17 方法参数的排序

定义方法时,参数排序应该遵循:

public Post loadPost(Context context, int postId);
public void loadPost(Context context, int postId, Callback callback);

Context 参数始终在第一位 Callback 参数始终在最尾。

2.2.18 字符串常量命名和值 (*****)

当使用字符串常量,它们应该被声明为final的静态和使用如下约定:

[Strings table]

注意 翻译不懂?

2.2.19 枚举(Enums) (*****)

需要时才使用枚举,如果在其他方法需要,那么实现的方式不应该用枚举。

不应该这样使用:

public enum SomeEnum {
    ONE, TWO, THREE
}

这样更好:

private static final int VALUE_ONE = 1;
private static final int VALUE_TWO = 2;
private static final int VALUE_THREE = 3;

2.2.20 Fragments和Activities的参数 (*****)

当我们使用Intent或Bundle传递数据时,键名和值必须规范。

Activity

传递数据到其他Activity必须使用KEY,定义遵循:

private static final String KEY_NAME = "com.your.package.name.to.activity.KEY_NAME";

Fragment

传递数据到其他Activity必须使用EXTRA,定义遵循:

private static final String EXTRA_NAME = "EXTRA_NAME";

当创建新的Fragment或Activity实例,向实例传递数据时,我们应该提供一个静态方法来获取新的实例,传递数据的方法参数。 例如:

Activity

public static Intent getStartIntent(Context context, Post post) {
    Intent intent = new Intent(context, CurrentActivity.class);
    intent.putParcelableExtra(EXTRA_POST, post);
    return intent;
}

Fragment

public static PostFragment newInstance(Post post) {
    PostFragment fragment = new PostFragment();
    Bundle args = new Bundle();
    args.putParcelable(ARGUMENT_POST, post);
    fragment.setArguments(args)
    return fragment;
}

2.2.21 行长度限制

代码长度尽量不要超过100个字符,他使得更难阅读代码,为了这样我们应该遵循:

  • 数据提取到一个局部变量
  • 逻辑由外部方法实现
  • 有的代码可以换行分割成几行

注意: 对于注释和import语句不存在100字符的限制

2.2.21.1 换行技巧

对于太长的一行分割成几行的代码,我们应该保持代码的样式一致。

分割运算

有时使用运算符,导致这一行太长,我们应该分割这个运算符。

int count = countOne + countTwo - countThree + countFour * countFive - countSix
        + countOnANewLineBecauseItsTooLong;

当然,你也可以这样分割:

int count =
        countOne + countTwo - countThree + countFour * countFive + countSix;

方法链

使用方法链时,尽量不要在一行使用。

不要这样

Picasso.with(context).load("someUrl").into(imageView);

而该这样:

Picasso.with(context)
        .load("someUrl")
        .into(imageView);

太长的参数

在有的方法中包含许多的参数,我们应该在适当情况下换行。比如我们申明方法时有太多的参数时,我们在合适的参数后面换行:

private void someMethod(Context context, String someLongStringName, String text,
                            long thisIsALong, String anotherString) {               
}

当调用方法时,我们应该每个参数都换行:

someMethod(context,
        "thisIsSomeLongTextItsQuiteLongIsntIt",
        "someText",
        01223892365463456,
        "thisIsSomeLongTextItsQuiteLongIsntIt");

2.2.22 方法间隔

在方法之间只应该空一行。

正确:

public String getUserName() {
    // Code
}
public void setUserName(String name) {
    // Code
}
public boolean isUserSignedIn() {
    // Code
}

错误示范:

public String getUserName() {
    // Code
}
public void setUserName(String name) {
    // Code
}
public boolean isUserSignedIn() {
    // Code
}

2.2.23 注释

2.2.23.1 行内注释

在必要时,内部注释被用来向读者提供这一段代码的意义,它只应在代码复杂难以理解的情况下使用。然而,在大多数情况下,代码应该被写的相当简单便于阅读而不需要注释

注意: 行内注释尽量保持不超过100字符

2.2.23.2 JavaDoc风格注释

方法的应该命名为描述方法的功能。JavaDoc风格注释的内容,是使得更容易理解方法的功能,以及传递到方法的所有参数的作用。

/**
 * Authenticates the user against the API given a User id.
 * If successful, this returns a success result
 *
 * @param userId The user id of the user that is to be authenticated.
 */

2.2.23.3 类注释

创建的类注释应该是有意义的、描述性的。在需要的地方使用@link,例如:

/**
  * RecyclerView adapter to display a list of {@link Post}.
  * Currently used with {@link PostRecycler} to show the list of Post items.
  */

关于作者注释,当有很多人使用和修改这个类时,这个注释并不能提供任何有用的信息。

/**
  * Created By Joe 18/06/2016
  */

2.2.24 分割代码

2.2.24.1 Java代码

当使用注释分割代码时,注释应该遵循下面这样的方式:

public void method() { }
public void someOtherMethod() { }
/********* Mvp Method Implementations  ********/
public void anotherMethod() { }
/********* Helper Methods  ********/
public void someMethod() { }

不要这样:

public void method() { }
public void someOtherMethod() { }
// Mvp Method Implementations
public void anotherMethod() { }

这使得分割的方法比较容易在一个类来定位。

2.2.24.2 字符串文件

字符串资源在string.xml文件中定义,且按功能分割成组,比如:

// User Profile Activity
<string name="button_save">Save</string>
<string name="button_cancel">Cancel</string>
// Settings Activity
<string name="message_instructions">...</string>

这不仅有助于保持字符串文件整齐,而且当需要改变内容时更容易找到字符串。

2.2.24.3 RxJava chaining

当Rx链式操作时,都应该在.之前换一行,比如:

return dataManager.getPost()
            .concatMap(new Func1<Post, Observable<? extends Post>>() {
                @Override
                 public Observable<? extends Post> call(Post post) {
                     return mRetrofitService.getPost(post.id);
                 }
            })
            .retry(new Func2<Integer, Throwable, Boolean>() {
                 @Override
                 public Boolean call(Integer numRetries, Throwable throwable) {
                     return throwable instanceof RetrofitError;
                 }
            });

这使得更容易理解的链操作的流程。

2.2.25 Butterknife

2.2.25.1 事件侦听

在需要时,使用Butterknife绑定监听器,比如单击事件的绑定就应该是:

mSubmitButton.setOnClickListener(new View.OnClickListener() {
    public void onClick(View v) {
        // Some code here...
    }
  };

不是这样:

@OnClick(R.id.button_submit)
public void onSubmitButtonClick() { }

2.3 XML样式规则

2.3.1 使用自己的关闭标签

当在一个XML布局视图没有任何子标签时,应使用自结束标签。

这样:

<ImageView
    android:id="@+id/image_user"
    android:layout_width="90dp"
    android:layout_height="90dp" />

不要这样:

<ImageView
    android:id="@+id/image_user"
    android:layout_width="90dp"
    android:layout_height="90dp">
</ImageView>

2.3.2 资源命名

所有的资源名称和ID都应该使用小写和下划线,示例:

text_username, activity_main, fragment_user, error_message_network_connection

这样可以保持一致, 当想更改布局文件时更容易找到这个文件。

2.3.2.1 ID命名

所有的ID命名都应该使用他的元素作为前缀。

Element Prefix
ImageView image_
Fragment fragment_
RelativeLayout layout_
Button button_
TextView text_
View view_

比如:

<TextView
    android:id="@+id/text_username"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

特殊View只出现一次在布局文件中,可以直接给他定义为元素名称。比如toolbar

2.3.2.2 字符串

所有的字符串名称应该与他们正被应用程序引用的一部分作为前缀开头。比如:

Screen String Resource Name
Registration Fragment “Register now” registration_register_now
Sign Up Activity “Cancel” sign_up_cancel
Rate App Dialog “No thanks” rate_app_no_thanks

如果这是不被引用的,我们可以使用下面的规则:

Prefix Description
error_ Used for error messages
title_ Used for dialog titles
action_ Used for option menu actions
msg_ Used for generic message such as in a dialog
label_ Used for activity labels

字符串资源有两个重点:

  • 字符串资源不应该在整个屏幕上重复使用。当想改变屏幕中一个字符串时会出现额外问题。每处使用单独的字符串定义解决未来可能会出现的问题

  • 字符串通常strings.xml中定义,不再类文件中直接使用字符串。

2.3.2.3 样式和主题

当定义样式和主题时,通常使用驼峰命名法来命名。比如:

AppTheme.DarkBackground.NoActionBar
AppTheme.LightBackground.TransparentStatusBar
ProfileButtonStyle
TitleTextStyle

2.3.3 属性排序

属性的排序不仅要求整齐而且还要在布局文件中能快速定位属性。主要规则:

  1. View Id
  2. Style
  3. Layout width and layout height
  4. Other layout_ attributes, sorted alphabetically
  5. Remaining attributes, sorted alphabetically

For example:

<Button
        android:id="@id/button_accept"
        style="@style/ButtonStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentStart="true"
        android:padding="16dp"
        android:text="@string/button_skip_sign_in"
        android:textColor="@color/bluish_gray" />

注意: 此格式可以通过在Android Studio中格式功能进行格式化 - cmd + shift + L

当想更改布局文件属性时,更容易通过XML浏览找到属性.

2.4 测试样式规则

2.4.1 单元测试

任何单元测试类名应该和测试目标类名匹配,结尾加上Test后缀。示例:

Class Test Class
DataManager DataManagerTest
UserProfilePresenter UserProfilePresenterTest
PreferencesHelper PreferencesHelperTest

所有的测试方法都应该使用@Test注释,方法命名应该遵循这个示例:

@Test
public void methodNamePreconditionExpectedResult() { }

根据示例,我们想检查signUp()方法在无效的email地址时的效果,这个测试方法应该是:

@Test
    public void signUpWithInvalidEmailFails() { }

测试应该只测试方法名赋予的测试,如果想添加额外的测试,应该创建新的单元测试。

如果我们当前测试类包含许多不同的方法,那么这个测试类应该分割成多个测试类。这有助于保持测试更易于维护,更容易定位。比如,一个DatabaseHelper类应该分成多个测试类:

DatabaseHelperUserTest
DatabaseHelperPostsTest
DatabaseHelperDraftsTest

2.4.2 Espresso测试

每个Espresso测试类一般针对一个Activity,所以它的名称应与目标Activity的相同,再接着测试。 例如:

Class Test Class
MainActivity MainActivityTest
ProfileActivity ProfileActivityTest
DraftsActivity DraftsActivityTest

当用Espresso API时,为了增加可读性链式方法每个方法都应该单独一行。示例:

onView(withId(R.id.text_title))
        .perform(scrollTo())
        .check(matches(isDisplayed()))

这种风格不仅帮助我们坚持每行少于100个字符,而且也很容易地读取事件发生在那个Espresso测试链上。