分類
Android Uncategorized

Room overview

什麼是 Room?

Room 是 Android 官方所提供的 ORM,可快速方便的存取 SQLite。
基本上 Room 為 SQLite 提供了一個抽象層,該層透過使用 SQLite 實現更強大的資料庫存取功能。
處理大量結構化數據的應用程序可以從本地持久化保存數據中獲得好處。最常見的範例是當設備無法使用網絡時緩存相關的數據,讓用戶仍然可以在離線時瀏覽該內容。之後在設備重新連上網路時,任何更改的內容都會同步到服務器。

強烈建議使用 Room 而不直接使用 SQLite。

使用 Room 在本地端資料庫儲存資料
使用 Room entities 定義資料
在資料庫中建立 View
使用 Room DAO 存取資料
 

分類
Android Uncategorized

Two-way data binding (雙向數據綁定)

前言

使用單向數據綁定可以在屬性上設置數值,並可設定對該屬性的變化作出反應的監聽器。如下

<CheckBox
    android:id="@+id/rememberMeCheckBox"
    android:checked="@{viewmodel.rememberMe}"
    android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
/>

雙向數據綁定提供了此過程的捷徑。如下

<CheckBox
    android:id="@+id/rememberMeCheckBox"
    android:checked="@={viewmodel.rememberMe}"
/>

注意在 @={} 表示法中的 = 符號表示了同時接受資料變化以及監聽更新。
為了對數據的變化做出反應,可以讓佈局變量成為 Observable 的實現,通常是 BaseObservable,並使用 @Bindable 註釋,如下

public class LoginViewModel extends BaseObservable {
    // private Model data = ...
    @Bindable
    public Boolean getRememberMe() {
        return data.rememberMe;
    }
    public void setRememberMe(Boolean value) {
        // Avoids infinite loops.
        if (data.rememberMe != value) {
            data.rememberMe = value;
            // React to the change.
            saveData();
            // Notify observers of a new value.
            notifyPropertyChanged(BR.remember_me);
        }
    }
}

由於 bindable 屬性的 getter 方法稱為 getRememberMe(),因此屬性的相應 setter 方法會自動使用名稱 setRememberMe()。
關於使用 BaseObservable 和 @Bindable 的更多訊息,參考 Work with observable data objects.

Two-way data binding using custom attributes

該平台為最常見的雙向屬性和監聽器提供雙向數據綁定的實現,使用者可以將當作應用程序的一部分。
若使用者想在自定義屬性上使用雙向資料綁定則必須了解如何使用 @InverseBindingAdapter 和 @InverseBindingMethod 註解。
例如,如果要在名稱為 MyView 的自定義視圖中對 “time” 屬性啟用雙向數據綁定,必須完成以下步驟
1.註釋設置初始值的方法,並在使用 @BindingAdapter 更改時進行更新

@BindingAdapter("time")
public static void setTime(MyView view, Time newValue) {
    // Important to break potential infinite loops.
    if (view.time != newValue) {
        view.time = newValue;
    }
}

2.使用 @InverseBindingAdapter 註釋從視圖中讀取數值的方法

@InverseBindingAdapter("time")
public static Time getTime(MyView view) {
    return view.getTime();
}

此時,data binding library 知道數據變化時要執行的操作(呼叫使用 @BindingAdapter 註釋的方法)以及視圖屬性變化時調用的內容(它呼叫 InverseBindingListener)。
為此,需要在視圖上設置一個監聽器。它可以是與自定義視圖關聯的自定義監聽器,也可以是通用事件,例如失去焦點或文本更改。
將 @BindingAdapter 註釋添加到為該屬性的變化設置監聽器的方法,如下

@BindingAdapter("app:timeAttrChanged")
public static void setListeners(
        MyView view, final InverseBindingListener attrChange) {
    // Set a listener for click, focus, touch, etc.
}

這個 Listener 包含了 InverseBindingListener 作為參數,可以使用 InverseBindingListener 告知 data binding library 該屬性已更改。接著,系統可以開始使用 @InverseBindingAdapter 呼叫註釋方法,依此類推。
注意每個雙向綁定都會生成一個合成事件屬性( synthetic event attribute)。此屬性與基本屬性具有相同的名稱,但具有後綴 “AttrChanged”。
合成事件屬性允許 data binding library 建立使用 @BindingAdapter 註釋的方法,以將事件監聽器連結到 View 的相應實體。
在實作中,該監聽器包括一些特殊的邏輯,包括用於單向數據綁定的監聽器。相關範例,參考 TextViewBindingAdapter
 

Converters

如果綁定到 View 對象的變數需要在顯示之前進行格式化,轉換或更改,則可以使用 Converter 對象。
例如,使用顯示日期的 EditText 對象

<EditText
    android:id="@+id/birth_date"
    android:text="@={Converter.dateToString(viewmodel.birthDate)}"
/>

viewmodel.birthDate 屬性包含 Long 類型的值,因此需要使用轉換器對其進行格式化。
因為使用雙向表達式,所以還需要一個反向轉換器(inverse converter)讓 data binding library 知道如何將字符串轉換回數據類型,在本例中為 Long。
此過程通過將 @InverseMethod 註釋添加到其中一個轉換器並使此註釋引用反向轉換器來完成。如下

public class Converter {
    @InverseMethod("stringToDate")
    public static String dateToString(EditText view, long oldValue,
            long value) {
        // Converts long to String.
    }
    public static long stringToDate(EditText view, String oldValue,
            String value) {
        // Converts String to long.
    }
}

 

Infinite loops using two-way data binding

使用雙向數據綁定時,小心不要引入無限迴圈。
當用戶更改屬性時,將呼叫使用 @InverseBindingAdapter 註釋的方法,並將數值分配給 屬性。反過來,這將呼叫使用 @BindingAdapter 註釋的方法,這將觸發對使用 @InverseBindingAdapter 註釋的方法的另一個調用,依此類推。
因此,通過比較使用 @BindingAdapter 註釋的方法中的新値和舊值來打破可能的無限循環非常重要。
 

Two-way attributes

下表為平台預設的雙向綁定屬性表。

Class Attribute(s) Binding adapter
AdapterView android:selectedItemPosition
android:selection
AdapterViewBindingAdapter
CalendarView android:date CalendarViewBindingAdapter
CompoundButton android:checked CompoundButtonBindingAdapter
DatePicker android:year
android:month
android:day
DatePickerBindingAdapter
NumberPicker android:value NumberPickerBindingAdapter
RadioButton android:checkedButton RadioGroupBindingAdapter
RatingBar android:rating RatingBarBindingAdapter
SeekBar android:progress SeekBarBindingAdapter
TabHost android:currentTab TabHostBindingAdapter
TextView android:text TextViewBindingAdapter
TimePicker android:hour
android:minute
TimePickerBindingAdapter
分類
Android Uncategorized

Bind layout views to Architecture Components (綁定佈局到架構元件)

前言

AndroidX 函式庫包含架構元件(Architecture Components),可以使用它來設計健壯,可測試和可維護的應用程序。
Data Binding Library 可與架構元件無縫協作,進一步簡化 UI 的開發。
應用程序中的 layout 可以綁定到架構元件中的數據,這些數據可以幫助管理 UI 控制器生命週期並通知數據中的更改。
以下介紹如何將架構元件合併到應用程序,以進一步使用 Data Binding Library 的好處。
 

Use LiveData to notify the UI about data changes

可以使用 LiveData 物件作為數據綁定來源,以自動通知UI有關數據更改的訊息。更多的資訊可以參考 LiveData Overview
與實現 Observable 的物件(例如 observable fields)不同,LiveData 物件了解訂閱對象的生命週期。相關的好處可以參考 The advantages of using LiveData
在 Android Studio 3.1 或之後的版本可以使用 LiveData 取代 observable fields。
要將 LiveData 與綁定類別一起使用,需要指定生命週期所有者(lifecycle owner)以定義 LiveData 對象的範圍。如下,在綁定類別初始化之後指定了 Activity 本身為生命週期所有者。

class ViewModelActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Inflate view and obtain an instance of the binding class.
        UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);
        // Specify the current activity as the lifecycle owner.
        binding.setLifecycleOwner(this);
    }
}

可以使用 ViewModel元件(如 Use ViewModel to manage UI-related data)將數據綁定到佈局。在 ViewModel 中,可以使用 LiveData 對象轉換數據或合併多個數據來源。
如下示範如何在 ViewModel 中轉換數據。

class ScheduleViewModel extends ViewModel {
    LiveData username;
    public ScheduleViewModel() {
        String result = Repository.userName;
        userName = Transformations.map(result, result -> result.value);
}

 

Use VeiwModel to manage UI-related data

Data binding library 和 ViewModel 可以無縫協作,以顯示數據並對其更改做出反應。
將 ViewModel 元件與 data binding library 一起使用,可以將 UI 邏輯從佈局移動到元件中,讓元件更易於測試。
Data binding library 可確保在需要時綁定和取消綁定數據來源。
更多的訊息可以參考 ViewModel Overview
要將 ViewModel 與 data binding library 一起使用,必須實體化繼承自 ViewModel 的元件,取得綁定類別的實體,並將 ViewModel 元件分配給綁定類別中的屬性。如下

class ViewModelActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Obtain the ViewModel component.
        UserModel userModel = ViewModelProviders.of(getActivity())
                                                  .get(UserModel.class);
        // Inflate view and obtain an instance of the binding class.
        UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);
        // Assign the component to a property in the binding class.
        binding.viewmodel = userModel;
    }
}

在 layout 中,使用表達式將 ViewModel 元件的屬性和方法分配給相對應的視圖。

<CheckBox
    android:id="@+id/rememberMeCheckBox"
    android:checked="@{viewmodel.rememberMe}"
    android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />

 

Use an Observable ViewModel for more control over binding adapters

可以使用實現 Observable 的 ViewModel 來通知其他應用程序元件有關數據更改的信息,類似於使用 LiveData 對象的方式。
在某些情況下,可能更適合使用 ViewModel 元件來實現 Observable 介面而不是使用 LiveData 對象,即使沒有 LiveData 的生命週期管理功能。
使用實現 Observable 的 ViewModel 元件可以更好地控制應用程序中的綁定適配器。此模式可以在數據變化時更容易控制通知,允許指定自定義方法以在雙向數據綁定中設置屬性的值。
要實現可觀察的 ViewModel 元件,必須創建一個繼承 ViewModel 並實現 Observable 介面的類別。
當觀察者使用 addOnPropertyChangedCallback() 和 removeOnPropertyChangedCallback()方法訂閱或取消訂閱通知時,使用者可以提供自定義邏輯。
還可以提供在 notifyPropertyChanged()方法中屬性更改時執行的自定義邏輯。
以下示範如何實現可觀察的 ViewModel

/**
 * A ViewModel that is also an Observable,
 * to be used with the Data Binding Library.
 */
class ObservableViewModel extends ViewModel implements Observable {
    private PropertyChangeRegistry callbacks = new PropertyChangeRegistry();
    @Override
    protected void addOnPropertyChangedCallback(
            Observable.OnPropertyChangedCallback callback) {
        callbacks.add(callback);
    }
    @Override
    protected void removeOnPropertyChangedCallback(
            Observable.OnPropertyChangedCallback callback) {
        callbacks.remove(callback);
    }
    /**
     * Notifies observers that all properties of this instance have changed.
     */
    void notifyChange() {
        callbacks.notifyCallbacks(this, 0, null);
    }
    /**
     * Notifies observers that a specific property has changed. The getter for the
     * property that changes should be marked with the @Bindable annotation to
     * generate a field in the BR class to be used as the fieldId parameter.
     *
     * @param fieldId The generated BR id for the Bindable field.
     */
    void notifyPropertyChanged(int fieldId) {
        callbacks.notifyCallbacks(this, fieldId, null);
    }
}

 

分類
Android Uncategorized

Binding adapters (綁定轉接器)

前言

Binding Adapter 主要用來呼叫適當方式來設定數值。一個例子為透過 setText 方法來設定屬性值,另一個例子為透過呼叫 setOnClickListener 方法來設定事件監聽器。
Data Binding Library 讓使用者指定調用方法來設定數值,客製化綁定邏輯,並使用適配器指定返回對象的類型。

Setting attribute values

每當綁定值發生更改時,綁定類別必須使用綁定表達式在視圖上調用 setter 方法。使用者可以允許 data binding library 自動決定哪個方法,顯式的聲明方法或提供自定義邏輯來選擇方法。
 

Automatic method selection

對於一個命名為 example 的屬性,data binding library 會嘗試尋找名稱為 setExample(arg) 的方法,其方法接受兼容的參數。
只有屬性的名稱和型別會被用於搜尋,屬性的名稱空間則不會被考慮。
例如,對於 android:text=”@{user.name}” 表達式,data binding library 會尋找一個 setText(arg) 方法,該方法的參數(arg)的型別為 user.getName 方法所回傳的。例如,若 user.getName 方法回傳的型態為 String,則 data binding library 會尋找 setText 方法,其方法接受的參數型態為 String。若 user.getName 方法回傳的型態為 int,則 data binding library 會尋找 setText 方法,其方法接受的參數型態為 int。
表達式必須回傳正確的型態,如有必要可以進行轉型的動作。即使沒有屬性符合給定的名稱,數據綁定仍然工作。之後,可以使用數據綁定為任何 setter 建立屬性。
例如,類別 DrawerLayout 沒有任何屬性,但有很多 setter。
下方的佈局自動使用 setScrimColor(int) 和 setDrawerListener(DrawerListener) 方法作為 app:scrimColor 和 app:drawerListener 屬性的 setter

<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}">

 

Specify a custom method name

有些屬性的 setter 並不符合名稱,在這種情況下一個屬性可以藉由 BindingMethods 註解去連結一個 setter。
註釋與類別一起使用,可以包含多個 BindingMethod 註釋,每個註釋方法一個註釋。綁定方法可以添加到應用程序中任何類別的註釋。如下方範例
android:tint 屬性和 setImageTintList(ColorStateList) 方法結合,而不是和 setTint 方法結合。

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

大多數情況下不需要重命名 setter。已使用名稱約定實現的屬性可自動查找相對應的方法。
 

Provide custom logic

有些屬性需要客製化綁定邏輯。例如 android:paddingLeft 屬性並沒有其相對應的 setter,代替的是 setPadding(left, top, right, bottom) 方法。
一個 static binding adapter 方法加上 BindingAdapter 註釋允許使用者自定義如何調用屬性的 setter。
Android framework 類別的屬性已經有 BindingAdapter 註釋。如下為 paddingLeft 屬性的 binding adapter

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
  view.setPadding(padding,
                  view.getPaddingTop(),
                  view.getPaddingRight(),
                  view.getPaddingBottom());
}

參數的型態相當重要,第一個參數決定與屬性相關連的視圖型別。第二個參數確定給定屬性的綁定表達式所接受的型別。
Binding adapter 相當有用於其他客製化的類型。如一個客製化的 loader 可以從 worker thread 被呼叫用來讀取圖像。
當發生衝突時,使用者自定義的 binding adapter 會覆蓋過原先的 adapter。
也可以有多個 adapter 用來接收多個屬性。如下

@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
  Picasso.get().load(url).error(error).into(view);
}

如下,可以在 layout 使用 adapter。
注意 @drawable/venueError 參考到 App 中的資源。冒號包括住@{}的寫法為合法的綁定表達式。

<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

注意 data binding library 會忽略自定義命名空間以進行匹配。
當 imageUrl 和 error 都被 ImageView 物件使用時,Adapter 將會被呼叫。
若想讓屬性其中之一被使用時就呼叫 adapter,可以設定 requireAll 為 false,如下

@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
  if (url == null) {
    imageView.setImageDrawable(placeholder);
  } else {
    MyImageLoader.loadInto(imageView, url, placeholder);
  }
}

Binding Adapter method 可以選擇在其處理程序中使用舊值。使用舊值和新值的方法應首先聲明屬性的所有舊值。如下

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
  if (oldPadding != newPadding) {
      view.setPadding(newPadding,
                      view.getPaddingTop(),
                      view.getPaddingRight(),
                      view.getPaddingBottom());
   }
}

 
Event handler 只能與帶有一個抽象方法的接口或抽象類別一起使用。如下

@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    if (oldValue != null) {
      view.removeOnLayoutChangeListener(oldValue);
    }
    if (newValue != null) {
      view.addOnLayoutChangeListener(newValue);
    }
  }
}

 
在 layout 中使用這個 event handler,如下

<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

 
當偵聽器具有多個方法時,必須將其拆分為多個偵聽器。
例如 View.OnAttachStateChangeListener 有 2 個方法為 onViewAttachedToWindow(View) 和 onViewDetachedFromWindow(View)。就有 2 個介面去區分它們的 attribute 和 handler。

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
  void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
  void onViewAttachedToWindow(View v);
}

 
因為更改一個偵聽器也會影響另一個偵聽器,所以需要一個適用於任一屬性或適用於兩者的適配器。
可以透過設定 requireAll 為 false 表示並不是每個屬性都必須指定給表達式。

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (attach != null) {
                        attach.onViewAttachedToWindow(v);
                    }
                }
                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (detach != null) {
                        detach.onViewDetachedFromWindow(v);
                    }
                }
            };
        }
        OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener,
                R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}

上面的示例比正常情況稍微複雜一些,因為 View 使用 addOnAttachStateChangeListener()和 removeOnAttachStateChangeListener()方法而不是 OnAttachStateChangeListener 的 setter 方法。
android.databinding.adapters.ListenerUtil 類別有助於跟踪以前的偵聽器,以便可以在綁定適配器中刪除它們。
藉由在 OnViewDetachedFromWindow 和 OnViewAttachedToWindow 方法加入 @TargetApi(VERSION_CODES.HONEYCOMB_MR1),data binding library 只會在裝置運行為 Android3.1 或以上的版本產生 listener。
 

Object conversions

Automatic object conversion

從綁定表達式返回 Object 時,data binding library 會選擇用於設置屬性值的方法。Object 會被強制轉換為所選方法的參數類型。
在使用 ObservableMap 存儲數據的應用程序,此行為很方便,如以下示例所示

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content" />

也可以使用 object.key 的方式在 Map 中參考數值,如上例的 @{userMap[“lastName”]} 可以使用 @{userMap.lastName} 來取代。
在表達式中的 userMap 會回傳一個數值,該數值將會被自動轉型為 setText(CharSequence) 方法的參數類型,該方法用來設定 android:text 屬型的値。
如果參數類型不明確,則必須在表達式中強制轉換返回類型。

Custom conversions

在某些情況下,特定類型之間需要自定義轉換。例如 android:background 需要 Drawable,但指定的顏色值是整數。
以下範例顯示需要 Drawable 的屬性,但是提供了一個整數

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

每當需要 Drawable 並返回一個整數時,int 應該轉換為 ColorDrawable。可以使用帶有BindingConversion 註釋的靜態方法完成轉換,如下所示

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
    return new ColorDrawable(color);
}

但是,綁定表達式中提供的值類型必須一致。不能在同一表達式中使用不同的類型,如下

<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

 

分類
Android Uncategorized

Generated binding classes (產生綁定類別)

前言

Data Binding 會產生綁定類別(binding class),用來存取佈局變數(layout’s variable)和視圖(View)。
以下描述了如何建立及客製化綁定類別。
綁定類別將佈局變量與佈局中的視圖連結起來,綁定類別的名稱和 package 可以自行定義。所有的綁定類別都繼承自 ViewDataBinding 類別。
每個 layout file 都會有一個對應的綁定類別。預設,類別的名稱會根據佈局文件的名稱,將其轉換為 Pascal 大小寫並添加 Binding。
若 layout 的名稱為 activity_main.xml,則對應的綁定類別為 ActivityMainBinding。
此類別包含佈局屬性(如user variable)到佈局視圖的所有綁定內容,並知道如何為綁定表達式指定值。
 

Create a binding object

在對佈局進行填充之後,應該快速的創建綁定對象(binding object),以確保在綁定到佈局中具有表達式的視圖之前不會修改視圖層次結構。
將對象綁定到佈局的最常用方法是使用綁定類別上的靜態方法。
可以透過使用綁定類別的 inflate 方法來擴展視圖層次結構並將對象綁定到該層次結構。如下

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    MyLayoutBinding binding = MyLayoutBinding.inflate(getLayoutInflater());
}

 
除了 LayoutInflater 對象之外,還有一個替換的 inflate 方法,它接受 ViewGroup對象,如下

MyLayoutBinding binding = MyLayoutBinding.inflate(getLayoutInflater(), viewGroup, false);

如果使用不同的機制對佈局進行填充,則可以單獨綁定,如下

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

有時候沒辦法事先知道綁定類型。在這種情況下,可以使用 DataBindingUtil 類創建綁定,如下

View rootView = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bind(viewRoot);

若開發者是在 Fragment, ListView, RecyclerView adapter 中使用 data binding item
則可以使用綁定類別或 DataBindingUtil 的 inflate方法。如下

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

 

具有 ID 的視圖

Data Binding 會在綁定類別中建立一個不可變的欄位,該欄位會對應於 layout 檔案中每個具有 id 的 View。如下 Data Binding 會建立 firstName 和 lastName 欄位

<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
        android:id="@+id/firstName"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"
        android:id="@+id/lastName"/>
   </LinearLayout>
</layout>

Data Binding 會在單次傳遞中從視圖層次結構中提取具有 ID 的視圖。這個機制比為佈局中的每個視圖調用 findViewById 方法都要快。ID 對於 Data Binding 並不是必要的,但仍有些實體需要從 code 存取視圖。
 

Variables

Data Binding 會為每個宣告在 layout 中的變數建立存取方法(setter and getter)。如下,將會為 user, image, note 變數建立存取方法。

<data>
   <import type="android.graphics.drawable.Drawable"/>
   <variable name="user" type="com.example.User"/>
   <variable name="image" type="Drawable"/>
   <variable name="note" type="String"/>
</data>


ViewStubs

與普通視圖不同,ViewStub 物件從一個不可見的視圖開始。
當它們被顯示或被明確告知要填充時,它們會通過填充另一個佈局來替換自己的佈局。
由於 ViewStub 基本上會從視圖層次結構中消失,因此綁定對像的視圖也必須消失以允許垃圾回收聲明。
因為視圖是不可變的,所以 ViewStubProxy 對象取代了生成綁定類中的 ViewStub,使您可以在 ViewStub 存在時訪問它,並在 ViewStub 填充時訪問視圖層次結構
當填充另一個佈局時,必須為該佈局建立綁定。因此 ViewStubProxy 必須監聽  ViewStub,並在需要時建立綁定。在同一時間內只能有一個監聽器存在,ViewStubProxy 予許開發者設定 OnInflateListener,該監聽器將在建立綁定之後被呼叫。
 

Immediate Binding

當變量或可觀察對象發生變化時,綁定將會被排程更改在下一幀之前。但是,有時必須立即執行綁定。要強制執行,可以使用 executePendingBindings 方法。
 
 

進階綁定

動態變數

動態變數適用於無法得知特定的綁定類別的時候。如當一個 RecyclerView.Adapter 對非特定的佈局進行操作時並不知道特定的綁定類別,但它還是必須在呼叫 onBindViewHolder 方法時指定綁定値。
在下面的範例中,RecyclerView 綁定的所有佈局都有個 item 變數。而 BindingHolder 物件具有 getBinding 方法,該方法可以回傳 ViewDataBinding 基本類別。

public void onBindViewHolder(BindingHolder holder, int position) {
    final T item = mItems.get(position);
    holder.getBinding().setVariable(BR.item, item);
    holder.getBinding().executePendingBindings();
}

Data Binding 在 module package 會產生一個名為 BR 的類別,該類別包含用於數據綁定的資源的 ID。在上一個範例中 BR.item 是自動產生的。
 

背景執行緒

開發者可以在背景執行緒中更改數據模型(data model),只要它不是集合即可。Data Binding 會判斷每個變數/屬性以避免任何並發問題。
 

客製化綁定類別名稱

在預設情況下綁定類別的名稱是根據其相關佈局名稱而來,主要規則是將佈局名稱的底線去除並讓首字改為大寫,最後再加上 Binding,而綁定類別的位置會放置於 module package 的 databinding 資料夾下。 如佈局名稱為 contact_item.xml,其綁定類別為 ContactItemBinding。若 module package 為 com.example.my.app 則綁定類別的位置為 com.example.my.app.databinding。
可以透過 data 元素的 class 屬性來改變綁定類別的名稱或位置。
如下面的內容將會產生名為 ContactItem 綁定類別。

<data class="ContactItem">
…
</data>

也可以使用完整名稱來指定綁定類別的位置。如下
將產生 ContactItem 綁定類別,其位置為 com.example

<data class="com.example.ContactItem">
…
</data>

 

分類
Android Uncategorized

Splash Activity

1.編輯 value/styles.xml 加入以下內容

<resources>
...
    <style name="AppTheme.Launcher">
        <item name="android:windowBackground">@drawable/launch_screen</item>
        <!-- Optional, on Android 5+ you can modify the colorPrimaryDark color to match the windowBackground color for further branding-->
        <!-- <item name="colorPrimaryDark">@android:color/white</item> -->
    </style>
...
</resources>

其中 @drawable/launch_screen 目前還沒有,下一步製作。

2.在 drawable 新增 launch_screen.xml,內容如下

<?xml version="1.0" encoding="utf-8"?>
<!-- The android:opacity=”opaque” line — this is critical in preventing a flash of black as your theme transitions. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:opacity="opaque">
  <!-- The background color, preferably the same as your normal theme -->
  <item android:drawable="@android:color/white"/>
  <!-- Your product logo - 144dp color version of your app icon -->
  <item>
    <bitmap
      android:gravity="center"
      android:src="@drawable/your_logo"/>
  </item>
</layer-list>

其中 @drawable/your_logo 就是你想顯示的 logo 圖示

3. SplashActivity

該 Activity 除了在 onCreate 作了特殊處理之外,並沒有其它不同。如下

public class LogoActivity extends Activity {
...
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    setTheme(R.style.AppTheme);
    super.onCreate(savedInstanceState);
  }
...

 

4. 在 AndroidManifest.xml 讓 LogoActivity 套用 AppTheme.Launcher

    <activity
      android:theme="@style/AppTheme.Launcher"
      android:name=".logo.LogoActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>

 

分類
Android

Android Monkey 介紹與使用

描述

Monkey主要是用來做穩定性或是壓力測試,它可以執行在實體裝置或是模擬器上,藉由產生一系列的隨機事件(e.g., touch, click, gestures)來測試應用程序。


基本語法

adb shell monkey [options] <event-count>

[options] 為參數,<event-count>為事件數。


常用範例介紹

1.啟動monkey
直接執行 monkey, 發送event count 數量的事件:

adb shell monkey <event count>

 
2.停止monkey
2-1.搜尋 monkey pid

adb shell ps | grep 'monkey'

2-2.kill monkey by pid

adb shell kill <Step 1.找到的 pid>

Notectrl + c無法停止 monkey
 
3.執行在 com.package.name 上, 發送 event count 數量的事件

adb shell monkey -p com.package.name <event count>

 
4.執行在 com.package.name , 發送 event count 數量的事件,顯示最詳細的訊息

adb shell monkey -v-v-v -p com.package.name <event count>

 
5.執行在 com.package.name , 發送 event count 數量的事件,每個事件等待1秒, 顯示最詳細的訊息,事件隨機分配。

adb shell monkey -v-v-v -p com.package.name --throttle 1000 <event count>

 
6.執行在 com.package.name , 發送 event count 數量的事件,每個事件等待1秒,調整 touch 事件為50%,剩餘百分比隨機分配,顯示最詳細的訊息

adb shell monkey -v-v-v -p com.package.name --pct-touch 50 --throttle 1000 <event count>

 
7.執行在 com.package.name 上, 發送 event count 數量的事件,每個事件等待1秒,調整 touch 事件 50%,motion事件 25%,剩餘百分比隨機分配,顯示最詳細的訊息

adb shell monkey -v-v-v -p com.package.name --pct-touch 50 --pct-motion 25 --throttle 1000 <event count>

 



monkey
參數[options]介紹:

參數總共分4大類,為一般(General)類,事件(Events)類,限制(Constraints)類,除錯(Debugging)類。

一般類

-h : 幫助訊息

adb shell monkey -h
usage: monkey [-p ALLOWED_PACKAGE [-p ALLOWED_PACKAGE] ...]
              [-c MAIN_CATEGORY [-c MAIN_CATEGORY] ...]
              [--ignore-crashes] [--ignore-timeouts]
              [--ignore-security-exceptions]
              [--monitor-native-crashes] [--ignore-native-crashes]
              [--kill-process-after-error] [--hprof]
              [--pct-touch PERCENT] [--pct-motion PERCENT]
              [--pct-trackball PERCENT] [--pct-syskeys PERCENT]
              [--pct-nav PERCENT] [--pct-majornav PERCENT]
              [--pct-appswitch PERCENT] [--pct-flip PERCENT]
              [--pct-anyevent PERCENT] [--pct-pinchzoom PERCENT]
              [--pkg-blacklist-file PACKAGE_BLACKLIST_FILE]
              [--pkg-whitelist-file PACKAGE_WHITELIST_FILE]
              [--wait-dbg] [--dbg-no-events]
              [--setup scriptfile] [-f scriptfile [-f scriptfile] ...]
              [--port port]
              [-s SEED] [-v [-v] ...]
              [--throttle MILLISEC] [--randomize-throttle]
              [--profile-wait MILLISEC]
              [--device-sleep-time MILLISEC]
              [--randomize-script]
              [--script-log]
              [--bugreport]
              [--periodic-bugreport]
              COUNT

 
-v:顯示log訊息
越多 -v,顯示的訊息越多,最多可以有3-v 

adb shell monkey -v-v-v 10
:Monkey: seed=0 count=10
:IncludeCategory: android.intent.category.LAUNCHER
:IncludeCategory: android.intent.category.MONKEY
// Event percentages:
//   0: 15.0%
//   1: 10.0%
//   2: 2.0%
//   3: 15.0%
//   4: -0.0%
//   5: 25.0%
//   6: 15.0%
//   7: 2.0%
//   8: 2.0%
//   9: 1.0%
//   10: 13.0%
:Switch: #Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10200000;component=com.google.android.street/.Street;end
    // Allowing start of Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.google.android.street/.Street } in package com.google.android.street
:Sending Flip keyboardOpen=false
:Sending Touch (ACTION_DOWN): 0:(313.0,367.0)
:Sending Touch (ACTION_UP): 0:(313.19876,367.49512)
Events injected: 10
:Sending rotation degree=0, persist=false
:Dropped: keys=0 pointers=0 trackballs=0 flips=0 rotations=0
## Network stats: elapsed time=247ms (0ms mobile, 247ms wifi, 0ms not connected)
// Monkey finished

2seed=0代表事件使用的隨機數,如果指定相同的隨機數,將會產生相同的事件。
count=10代表事件數量
第3~4行 :IncludeCategory: android.intent.category.LAUNCHER
:IncludeCategory: android.intent.category.MONKEY
代表 monkey 會去執行指定category的類別,預設是LAUNCHER or MONKEY
當在指令中使用 -c 參數時會顯示在這裡,若找不到指定的category會顯示以下訊息。

// Warning: no activities found for category android.intent.category.test
** No activities found to run, monkey aborted.

516行:事件比例,對照如下

// Event percentages:
// 0: 15.0% (--pct-touch )
// 1: 10.0% (--pct-motion)
// 2: 2.0% (--pct-pinchzoom)
// 3: 15.0% (--pct-trackball)
// 4: -0.0% (--pct-rotation)
// 5: 25.0% (--pct-nav)
// 6: 15.0% (--pct-majornav)
// 7: 2.0% (--pct-syskeys)
// 8: 2.0% (--pct-appswitch)
// 9: 1.0% (--pct-flip)
// 10: 13.0%(--pct-anyevent)

1722行:monkey 切換訊息。其中包括 actioncategorycomponent的訊息。action category 請參考 android developer 參考。而 component 即為啟動的 package全名和 activity名稱。
23行之後包含相關的測試訊息觸碰座標,旋轉螢幕等等。


事件類

-s:隨機數,指定相同的隨機數會有相同的事件產生。
–throttle:每個事件之間延遲的時間,以mill second 來計算,預設沒有延遲。
–pct-touch:觸碰事件,觸碰事件為按下放開事件。
–pct-motion:運動事件,運動事件為按下事件加上一連串的隨機事件最後加上放開事件。
–pct-trackball:軌跡球事件,軌跡球事件為一個或多個亂數動作。
–pct-nav:基本導航事件,基本導航事件為裝置的上,下,左,右輸入。
–pct-majornav:主要導航事件,主要導航事件為引發圖形介面的動作(e.g.,返回鍵,或選單鍵)
–pct-syskeys:系統事件,系統按鍵事件為home鍵,返回鍵,撥號鍵,音量鍵等等。
–pct-appswitchactivity啟動事件, activity啟動事件為影響monkey 最大化的平均切換同一個package內的所有activity 的機率。
–pct-anyevent:其他事件,包含以上沒有提到事件。

//執行在 com.example.mypackage 上, 發送 100 數量的事件,調整 touch 事件為10%,motion事件20%,軌跡球事件為 15%,剩餘百分比隨機分配,顯示最詳細的訊息
adb shell monkey -v-v-v -p com.example.mypackage -s 999 --pct-touch 10 --pct-motion 20 --pct-trackball 15 100
限制類

-p:指定 monkey 運作的 package,可以有一個到多個 packagemonkey會啟動在該package內的activity,如果 app 需要存取其他的 packagee.g., 聯絡人)。也必須在此指定。預設是隨機啟動該裝置中任何一個package

//執行在 com.example.mypackage 上, 發送 100 數量的事件,事件隨機分配,顯示最詳細的訊息
adb shell monkey -v-v-v -p com.example.mypackage 100

 
-c: 指定 monkey 運作的 category,通常可用來指定 monkey 啟動的 activitycategory AndroidManifest.xml 中設定(e.g.,預設為啟動Intent.CATEGORY_LAUNCHER or Intent.CATEGORY_MONKEY

//執行在 com.example.mypackage 上, 限制 category 為 LAUNCHER 以及 MONKEY,發送 100 數量的事件,事件隨機分配,顯示最詳細的訊息
adb shell monkey -v-v-v -p com.example.mypackage -c android.intent.category.LAUNCHER -c android.intent.category.MONKEY 100

AndroidManifest.xml example

...
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.mypackage.MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="portrait" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
...

第14行即為 category 指定的方式,因此 monkey 將會執行在com.example.mypakcage.MainActivity 。

除錯類

-hprof:在 monkey 執行事件的前後產生報告,報告大約有 5mb,路徑為data/misc
–ignore-crashesmonkey 預設當測試發生 exception crash 會立即停止,當設定該參數 monkey將不會停止。
–ignore-timeoutsmonkey 預設當測試發生 ANR會立即停止,當設定該參數 monkey將不會停止。
–ignore-security-exceptionsmonkey 預設當測試發生 permission error會立即停止,當設定該參數 monkey將不會停止。
–kill-process-after-errormonkey 預設當測試發生錯誤時不會停止啟動的程序,當設定該參數 monkey將會停止該程序。Note: monkey 成功完成測試後,也不會停止該程序,只會保留在測試結束的最後一個階段。
–montion-native-crashes:監看並報告當測試發生native code 的錯誤。
–wait-dbg:停止執行中的 monkey直到有除錯器連接為止。
–dbg-no-events:設定後,monkey 進入activity 前不會產生事件,一般會和 -v , -p-throttle 一起使用來監視 packages 之間的切換。

分類
Android

把安裝在手機的 apk 複製到本地端

0.開啟目前平台提供的最基本的Terminal tool
windows使用power shell(不要使用git bash),ubuntu使用bash,並移動到android sdk/platform-tools/
1.列出所有安裝的 package
(以下以Twitter app為範例,請確認已安裝Twitter)

adb shell pm list packages

Note:
在列出的 package 中尋找目標 apk 的 package,通常 package 和目標 apk 會有關連
若使用 adb 出現 command not found, 請指定 adb 的絕對路徑, (e.g., /android-sdk/platform-tools/adb), 或參考這篇將 adb 加到環境變數中。

./adb.exe shell pm list packages
output:
...
package:com.google.android.accessibility.soundamplifier
package:com.twitter.android
package:com.android.musicfx
...

 
2.藉由 package 列出目標 apk 的完整路徑

adb shell pm path example.package_name

Note:
example.package_name 就是在第1步列出的目標 apk package

./adb.exe shell pm path com.twitter.android
output:
package:/data/app/com.twitter.android-rsWL7_udEQ4bnqYf6aGKXQ==/base.apk
package:/data/app/com.twitter.android-rsWL7_udEQ4bnqYf6aGKXQ==/split_config.arm64_v8a.apk
package:/data/app/com.twitter.android-rsWL7_udEQ4bnqYf6aGKXQ==/split_config.xxhdpi.apk
package:/data/app/com.twitter.android-rsWL7_udEQ4bnqYf6aGKXQ==/split_config.zh.apk

 
3.複製目標到本地端

adb pull /data/app/example.package_name /local/path

Note:
/data/app/example.package_name 就是在第2步找到的完整路徑
local/path/ 為想放置目標 apk 的本地端(目前操作的機器)路徑,路徑之後可以設定取出的Apk的名稱,如D:\tempAPK\twitter_temp.apk。

./adb.exe pull /data/app/com.twitter.android-rsWL7_udEQ4bnqYf6aGKXQ==/base.apk D:\tempAPK\twitter_temp.apk
output:
/data/app/com.twitter.android-rsWL7_udEQ4bnqYf6aGKXQ==/base.apk: 1 file pulled. 35.1 MB/s (17336308 bytes in 0.470s)

完成後在D:\tempAPK\twitter_temp.apk就是從裝置取出的Twitter apk

分類
Android

Robotium 使用介紹

Robotium 為專門測試 android ui 的 framework,詳細介紹參考官網
本篇介紹如何導入 Robotium,以及建立測試 Activity 的 Robotium 模板,最後是 Robotium 常用函式介紹。


如何導入 Robotium

  1. Robotium 的使用環境和 android test project 非常類似,因此先建立 android test project。
    建立 android test project 詳細步驟請參考這篇
  2. 在第1步建立的 android test project 根目錄中建立 libs 資料夾,並將 Robotium 提供的 jar 檔放入 libs ,android 會自動幫你做好其他事。

建立測試 Activity 的 Robotium 模板

完成導入 Robotium 之後,你應該有個 test case,再將以下的 mSolo 加入。
Solo 為 Robotium 提供的類別,大部分測試功能都由它開始。

public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity>
{
    private Solo mSolo;
    public MainActivityTest(String name) {
        super(MainActivity.class);
        setName(name);
    }
    public MainActivityTest()
    {
        this(MainActivityTest.class.getSimpleName());
    }
    protected void tearDown() throws Exception
    {
        mSolo.finishOpenedActivities();
        super.tearDown();
    }
    protected void setUp() throws Exception
    {
        super.setUp();
        mSolo = new Solo(getInstrumentation(), getActivity());
    }
    public void testPreconditions()
    {
       assertNotNull(mSolo);
    }
}

第24行初始化 mSolo
第17行銷毀 mSolo
以上就是測試 activity 的 Robotium 模板


Robotium 常用函式介紹

使用 Robotium 來寫測試主要分為3個步驟:

  1. 取得 UI Component
  2. 操作 UI Component
  3. 使用斷言(Assert)判斷操作結果是否符合預期

接下來依序介紹在 Robotium 中各個步驟如何進行

取得 UI Component

在 Robotium 主要有2種取得 UI Component 的方式,第1種為根據 UI Component 的 ID 來取得,適合用於 UI Component 具有唯一的 ID 情況。

android.view.View getView(int id)

Returns a View matching the specified resource id.
android.view.View getView(String id)

Returns a View matching the specified resource id.

Example

...
LinearLayout mainView = (LinearLayout) mSolo.getView(R.id.main_view);//R.id.main_view 為 resource id

UI Component 的內容值可以當作識別的方式,但這種方式只支援 Button, EditText, TextView, 適合用於 UI Component 具有內容值的情況。

android.widget.Button getButton(String text)

Returns a Button displaying the specified text.
android.widget.EditText getEditText(String text)

Returns an EditText displaying the specified text.
android.widget.TextView getText(String text)

Returns a TextView displaying the specified text.

Example

...
Button exitButton = mSolo.getButton("Exit");//Exit 為顯示在 button 上的內容

UI Component 的 Tag 可以當作識別的方式,適合用於 UI Component 具有 Tag 的情況下。

android.view.View getView(Object tag)

Returns a View matching the specified tag.

第2種為根據 UI Component 的類型進行過濾再根據索引來取得,這種情況適用於 UI Component 沒有可以識別的 ID 情況。以下第1個方法為回傳所有類型的 views, 第2個回傳指定類型的 views, 第3個回傳指定類型並指定 parent 的 views

ArrayList<android.view.View> getCurrentViews()

Returns an ArrayList of the Views currently displayed in the focused Activity or Dialog.
<T extends android.view.View>
ArrayList<T>
getCurrentViews(Class<T> classToFilterBy)

Returns an ArrayList of Views matching the specified class located in the focused Activity or Dialog.
<T extends android.view.View>
ArrayList<T>
getCurrentViews(Class<T> classToFilterBy, android.view.View parent)

Returns an ArrayList of Views matching the specified class located under the specified parent.

使用上述的方法取得 view 的群集之後,再使用以下方法取得個別的 view。
需要注意的是 index 該 index 為子視圖的順序(index 從零開始由左到右或上到下遞增)

android.widget.Button getButton(int index)

Returns a Button matching the specified index.
android.widget.EditText getEditText(int index)

Returns an EditText matching the specified index.
android.widget.ImageView getImage(int index)

Returns an ImageView matching the specified index.
android.widget.ImageButton getImageButton(int index)

Returns an ImageButton matching the specified index.
android.widget.TextView getText(int index)

Returns a TextView matching the specified index.
<T extends android.view.View>
T
getView(Class<T> viewClass, int index)

Returns a View matching the specified class and index.

Example

...
LinearLayout mainView = (LinearLayout) mSolo.getView(R.id.main_view);
List<Button> buttons = mSolo.getCurrentViews(Button.class, mainView);
for (int i=0; i<buttons.size(); ++i) {
    Button button = mSolo.getButton(i);
}
//以上的 source code 即是取得 mainView 中的所有 button

操作 UI Component

Robotium 提供的操作共有點擊,長按,輸入文字,拖曳,滾動,搜尋,等待。
點擊&長按
點擊和長按的方法大致上各為clickOnXXX, clickLongOnXXX,支援的種類有 View, Button, EditText, ImageView, ImageButton, CheckBox, MenuItem, RadioButton, ToggleButton, Screen座標等等 。
詳細參考官網
Example :

...
mSolo.clickOnButton("Exit");//點擊 Exit button

輸入和清除文字
各為 typeText 以及 enterText,都是對 EditText 輸入文字,2者不同為 type 會一個一個的輸入,而 enter 為直接全部輸入 ,清除則是只對 EditText 清除 (cleanEditText)。
詳細參考官網
Example :

...
EditText numberEditText = mSolo.getEditText("number");//取得內容為 number 的 EditText
mSolo.typeText(numberEditText, "123");//在 EditText 填入 123

拖曳

void drag(float fromX, float toX, float fromY, float toY, int stepCount)

Simulate touching the specified location and dragging it to a new location.
...
mSolo.drag(0.0f, 50.0f, 0.0f, 50.0f, 100);

需要注意的是原點從左上角開始算起。
滾動
滾動為 ScrollXXX,基本的有 ScrollDown, ScrollUp 往上下滾動一段。
ScrollToBottom, ScrollToTop 往頂端底端滾動。
ScrollToSide 滾動至左或右。進階可以指定滾動的距離。
支援的種類有 view, ListView, RecyclerView,
詳細參考官網

mSolo.scrollDown();

搜尋
searchXXX,搜尋的目的為確認 UI Compenent 是否存在於 Screen 上,有的話才進行下一步操作。支援 Button, EditText, TextView, ToggleButton, 詳細參考官網
Example :

...
if(!mSolo.searchButton("Exit")){//尋找 Exit button 是否在螢幕上
    fail("Exit button should on screen");
}

等待
waitForXXX為等待系列,支援 activity, dialog open dialog close, text, view。
需要注意的是 waitFor 函式會在其內預設等待20秒,在20秒之內若等待條件成立的話回傳 true,反之回傳 false,也有提供自定義時間 e.g. mSolo.waitForLogMessage(“”,5000)。
比較特殊等待條件有 waitForLogMessage,這裡的 LogMessage 即為使用 Log 列印出來的內容
e.g. Log.d(TAG,”log message should print”);
就可使用 mSolo.waitForLogMessage(“log message should print”);
另一個是 waitForCondition , 可以自定義複雜條件以滿足特別的需求。
詳細參考官網
另外還有其他操作 e.g. 休息(sleep),擷圖(takeScreenShot),模擬按下實體按鍵, 檢查 UI 狀態(isXXX)。


使用斷言(Assert)判斷操作結果是否符合預期

因為 android test project 預設為使用 junit 來斷言,對於絕大部分的斷言情況來說已經足夠。Robotium 提供的斷言不多,只有4種(assertCurrentAcitvity, assertMemoryNotLow)。
Example :

...
assertTrue(mSolo.searchButton("Exit"));//斷言 Exit button 需要在螢幕上

 
 
 
 

分類
Android

TouchUtils.clickView 對 button 無效

描述:

button 是一般的 android 預設的 button,xml 定義如下

        <Button
            android:id="@+id/showtime_btn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="0.5"
            android:text="Show Time" />

被測程式 source code 使用如下

public class MainActivity extends Activity implements OnClickListener
{
    private Button mShowTimeButton;
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        initUIComponents();
    }
    private void initUIComponents()
    {
       mShowTimeButton = (Button) findViewById(R.id.showtime_btn);
       mShowTimeButton.setOnClickListener(this);
    }
    @Override
    public void onClick(View view)
    {
       int uiId = view.getId();
       switch(uiId){
           case R.id.showtime_btn:
               System.out.println("click show time button");
               break;
       }
    }
}

在直接執行被測程式的情況下, mShowTimeButton 可以點擊且點擊會出現click show time button。
測試程式如下

public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity>
{
    private static final String DEBUG = MainActivityTest.class.getSimpleName();
    private MainActivity mMainActivity;
    private Button mShowTimeButton;
    public MainActivityTest() {
        super("com.example.targettestproject", MainActivity.class);
    }
    protected void setUp() throws Exception
    {
        super.setUp();
        setActivityInitialTouchMode(true);
        mMainActivity = getActivity();
        initUIComponents();
    }
    private void initUIComponents()
    {
       mShowTimeButton = (Button) mMainActivity.findViewById(R.id.showtime_btn);
    }
    protected void tearDown() throws Exception
    {
        super.tearDown();
    }
    public void testPrecondition()
    {
        assertNotNull(mMainActivity);
        assertNotNull(mShowTimeButton);
    }
    @UiThreadTest
    public void testShowTimeByClickButton()
    {
        TouchUtils.clickView(this, mShowTimeButton);
    }
}

第15行經過測試有沒有加入 setActivityInitialTouchMode 沒有影響。
第37行經過測試有沒有加入 @UiThreadTest 沒有影響。
第40行沒有效果。
經過 google 相關搜尋沒有找到類似的問題及解決方案。
 

修正:

使用 @UiThreadTest 以及 Button.performClick() 來取代 TouchUtils.clickView,如下

    ...
    @UiThreadTest
    public void testShowTimeByClickButton()
    {
        mShowTimeButton.performClick();
    }