Sean's Note

2016年6月22日 星期三

Unit Tests for Beginners

Unit Tests 和 UI Tests 不同,Unit Tests 著重於測試程式中的一小部分的邏輯,例如 Class 或是 Class 中的 methods。而 Unit Tests 又可分為:

  • Local tests
    本地端的測試,程式碼只會運行在 JVM 上,所以執行效率最佳。多用於測試與 Android framework 沒有相依性的程式碼,或是其相依能被 mock objects 所取代。(Robolectric 也可以解決這個問題,見 Ref 3)

  • Instrumented tests
    跑在實體機器或模擬器上的測試。這些測試需要取得裝置上的一些資訊,如 Context 物件,或有其他不容易被 mock objects 所取代的相依類別。

建立 Local Unit Test Class

在 Android 中,建立單元測試需要用到 JUnit 這個已在 Java 世界中被廣泛被使用的 Unit Testing Framework,目前最新的版本是 JUnit 4,JUnit 4 不像前一版 JUnit 3 需要讓 test classes繼承 junit.framework.TestCase,更不限制 method 的命名要以 "test" 為前綴字。

測試的程式碼需要放在路徑 src/test/java 之下。

範例如下:
import org.junit.Test;
import java.util.regex.Pattern;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class EmailValidatorTest {

    @Test
    public void emailValidator_CorrectEmailSimple_ReturnsTrue() {
        assertThat(EmailValidator.isValidEmail("name@email.com"), is(true));
    }
    ...
}

在 gradle.build 裡宣告:

dependencies {
    // Required -- JUnit 4 framework
    testCompile 'junit:junit:4.12'
    // Optional -- Mockito framework
    testCompile 'org.mockito:mockito-core:1.+'
}


建立 Instrumented Test Class

需要用到 JUnit 4 test runner,而且測試的程式碼需要放在路徑 src/androidTest/java 之下。
此外 AndroidJUnitRunner 已經用來取代舊的 InstrumentationTestRunner 和 MultiDexTestRunner(自己繼承 AndroidJUnitRunner 來實作 AndroidJUnitMultiDexRunner )。若需要 Context 的話,可以透過 InstrumentationRegistry.getContext() 來取得物件(不用再繼承 InstrumentationTestCase 了)。

範例如下:
import android.os.Parcel;
import android.support.test.runner.AndroidJUnit4;
import android.util.Pair;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class LogHistoryAndroidUnitTest {

    public static final String TEST_STRING = "This is a string";
    public static final long TEST_LONG = 12345678L;
    private LogHistory mLogHistory;

    @Before
    public void createLogHistory() {
        mLogHistory = new LogHistory();
    }

    @Test
    public void logHistory_ParcelableWriteRead() {
        // Set up the Parcelable object to send and receive.
        mLogHistory.addEntry(TEST_STRING, TEST_LONG);

        // Write the data.
        Parcel parcel = Parcel.obtain();
        mLogHistory.writeToParcel(parcel, mLogHistory.describeContents());

        // After you're done with writing, you need to reset the parcel for reading.
        parcel.setDataPosition(0);

        // Read the data.
        LogHistory createdFromParcel = LogHistory.CREATOR.createFromParcel(parcel);
        List> createdFromParcelData = createdFromParcel.getData();

        // Verify that the received data is correct.
        assertThat(createdFromParcelData.size(), is(1));
        assertThat(createdFromParcelData.get(0).first, is(TEST_STRING));
        assertThat(createdFromParcelData.get(0).second, is(TEST_LONG));
    }
}

在 gradle.build 裡宣告: (Note: 這裡可能會遇到 support-annotations 版本衝突的問題)
dependencies {
    androidTestCompile 'com.android.support:support-annotations:23.0.1'
    androidTestCompile 'com.android.support.test:runner:0.4.1'
    androidTestCompile 'com.android.support.test:rules:0.4.1'
    // Optional -- Hamcrest library
    androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
    // Optional -- UI testing with Espresso
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
    // Optional -- UI testing with UI Automator
    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'
}

Ref:

[Android Library][Open-source] Butternife

Butternife 這個超方便的 library,透過 annotation 的方法來綁定 fields 和 methods,
免去了大家大量使用 findViewById 的方法。(許多知名的 APP 也都有在用這個 Library)

本來 bind 一個 view 要寫這樣:
TextView title;
title= (TextView) activity.findViewById(R.id.title);

用了 Butternife 後只要寫這樣:
@BindView(R.id.title) TextView title;

除此之外,Butternife 還有更多的功能,用法可以參考 Butternife 的官網:
http://jakewharton.github.io/butterknife

如果連 @BindView(R.id.XXX) 都懶得寫,可去下載 Android Studio 的這套 plugin:
ButterKnifeZelezny
https://github.com/avast/android-butterknife-zelezny

安裝完後,只要對 R.layout.XXX 按右鍵 -> Generate... -> Generate Butternife Injections 就可以
選擇要自動產生的 Bind codes 啦!

圖片來源: https://github.com/avast/android-butterknife-zelezny

2016年6月20日 星期一

Premultiplied pixels in Android bitmaps

簡單來說,Android 裡的 Bitmap 都是 premultiplied 過的,premultiplied 就是為了
加速 blending 兩張圖片的運算,預先將 ARGB 的 Alpha 資訊直接記錄在 RGB 中,
這樣的過程就是  premultiplied。
可以透過 Bitmap.isPremultiplied 來做檢查。
詳細可參考下面的連結。

Ref:
  1. The curious case of Android premultiplied alpha
    https://pspdfkit.com/blog/2016/a-curious-case-of-android-alpha/
  2. 透明像素-Premultiplied Alpha的秘密
    http://blog.csdn.net/zinking3/article/details/2260405

2016年6月16日 星期四

[Android Library][Open-source] EventBus

最近看了 EventBus,真是簡單又好用,其架構基於 publisher/subscriber 的設計模式。
透過 EventBus 可以鬆綁類別與類別之間的關係。
圖片來源: http://greenrobot.org/eventbus/

幾個特點如下:

  • 簡化了元件(Activities, Fragments, Threads ...)之間的溝通。
  • 有最佳化過,所以執行效率佳。
  • 不佔用 APK size (<50k jar)。
  • 已經被使用在大量知名的 APP 上,如: WhatApp, Clean Master, Yahoo Mail 等等。 
  • 提供了 subscriber 的優先權等功能。

如何使用和範例可以參考官方的連結:
http://greenrobot.org/eventbus/documentation/how-to-get-started/

各種情況下,需要分別採用的 ThreadMode:
  • POSTING
  • MAIN
  • BACKGROUND
  • ASYNC
http://greenrobot.org/eventbus/documentation/delivery-threads-threadmode/


2016年6月14日 星期二

How to check AAR's dependencies?

以最近很火紅的 FireBase 為例,想要使用 Analytics 的功能,需要引用
com.google.firebase:firebase-analytics:9.0.2
等等? 有人說不是應該是 com.google.firebase:firebase-core:9.0.2 嗎?
其實兩者都對,com.google.firebase:firebase-core:9.0.2 的 dependency 就是
com.google.firebase:firebase-analytics:9.0.2,要怎麼看 AAR 的 dependencies 呢?
首先,Android Studio 把預載好的 AARs 放在以下的目錄:
C:\Users\{UserName}\AppData\Local\Android\sdk\extras\google\m2repository

所以我們找到了 firebase-core:9.0.2 的目錄:
C:\Users\{UserName}\AppData\Local\Android\sdk\extras\google\m2repository\com\google\firebase\firebase-core

資料夾裡有個神秘的檔案 "firebase-core-9.0.2.pom",打開看看:
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns="http://maven.apache.org/POM/4.0.0" 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.google.firebase</groupId>
  <artifactId>firebase-core</artifactId>
  <version>9.0.2</version>
  <packaging>aar</packaging>
  <dependencies>
    <dependency>
      <groupId>com.google.firebase</groupId>
      <artifactId>firebase-analytics</artifactId>
      <version>9.0.2</version>
      <scope>compile</scope>
      <type>aar</type>
    </dependency>
  </dependencies>
</project>

答案揭曉,原來 com.google.firebase:firebase-core:9.0.2 根本就是 dependent on com.google.firebase:firebase-analytics:9.0.2 嘛!

2016年6月6日 星期一

[Gradle] How to generate jar file with Android Studio

Android Studio 在 build 的時候,會在
{library project}\build\intermediates\bundles\release\ 下產生 classes.jar。
我們只要把它複製到該放的路徑下就好。

可以用 Gradle Task 來做:

task deleteJar(type: Delete) {
    delete 'libs/clgpuimage.jar'
}

task createJar(type: Copy) {
    from('build/intermediates/bundles/release/')
    into('libs/')
    include('classes.jar') // Only include "classes.jar" in the from directory.
    rename ('classes.jar', 'clgpuimage.jar')
}

createJar.dependsOn(deleteJar, build)

最後,對這個 library project 執行 "createJar" 的 task 即可。