分類
Uncategorized

Lifecycle-Aware Components

什麼是 Lifecycle-aware Components?

Lifecycle-ware components(生命週期感知元件)包含在 android.arch.lifecycle 套件中,基本上就是當元件(Activity or Service)的生命週期發生變化時,生命週期元件即會自動收到通知並觸發相對應的動作。
 

為什麼需要生命週期感知元件?

在此之前若開發者需要根據元件的生命週期執行相對應的動作時都必須寫在元件的對應方法中。最常見的例子為旋轉螢幕時需要儲存資料,不是寫在 onPause 方法就是寫在 onSaveInstanceState 方法。
但這種寫法最直接的缺點就是增加 Activity 的大小,讓 Activity 變得越來越大。
生命週期感知元件可以根據 Activity 或 Fragment 的當前生命週期狀態自動呼叫其行為。
 

Lifecycle

Lifecycle 是一個類別,擁有關於另一個元件(Activity or Fragment)生命週期狀態的資訊。
Lifecycle 使用 2 個列舉來追蹤其相關連元件的生命週期狀態:
1.Event(事件)
Event 透過 Framework 和 Lifecycle class 發送,Event 會對應於 Activity 或 Fragment 的回調事件(callback events)。
2.State(狀態)
相關連元件的當前狀態。
以下為 Event 和 State 兩者互相轉換的關係圖。

LifecycleOwner

LifecycleOwner 為只有一個方法的介面,該方法必須回傳 Lifecycle。
在 Support Library 26.1.0 已將 Activity 和 Fragment 繼承 LifecycleOwner 並寫好回傳 Lifecycle 的實作部分,也就是說 Activity 和 Fragment 就是 LifecycleOwner。
要自動感知 Activity 的生命週期,首先呼叫 getLifecycle 方法取得 Lifecycle,接著再呼叫 Lifecycle 的 addObserver 方法讓 LifecycleObserver 訂閱 Lifecycle。
基本上就是透過 LifecycleOwner 提供 Lifecycle,接著讓 LifecycleObserver 訂閱
Lifecycle,如此 LifecycleObserver 就能感知 Lifecycle 的變化。
 

LifecycleObserver

LifecycleObserver 的用途就是用來跟蹤繼承 LifecycleOwner 的元件,透過在方法上加上註解的方式,可以自動取得目標元件的相關生命週期變化。
一個最簡單用來感知 Activity 的 LifecycleObserver 如下

import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.Lifecycle.Event;
import android.arch.lifecycle.LifecycleObserver;
import android.arch.lifecycle.OnLifecycleEvent;
import android.content.Context;
import android.util.Log;
public class LifecycleMainActivityObserver implements LifecycleObserver {
  private static final String TAG = LifecycleMainActivityObserver.class.getSimpleName();
  @OnLifecycleEvent(Event.ON_START)
  public void lifecycleStart() {
    Log.d(TAG, "detect lifecycle on start");
  }
  @OnLifecycleEvent(Event.ON_CREATE)
  public void lifecycleCreate() {
    Log.d(TAG, "detect lifecycle on create");
  }
  @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
  public void lifecycleResume() {
    Log.d(TAG, "detect lifecycle on resume");
  }
  @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
  public void lifecyclePause() {
    Log.d(TAG, "detect lifecycle on pause");
  }
  @OnLifecycleEvent(Event.ON_STOP)
  public void lifecycleStop() {
    Log.d(TAG, "detect lifecycle on stop");
  }
  @OnLifecycleEvent(Event.ON_DESTROY)
  public void lifecycleDestroy() {
    Log.d(TAG, "detect lifecycle on destroy");
  }
  @OnLifecycleEvent(Event.ON_ANY)
  public void lifecycleChange() {
    Log.d(TAG, "detect lifecycle change");
  }
}

註解 OnLifecycleEvent(Event.ON_CREATE) 就表示當目標元件處於 create 狀態時,該方法便會被呼叫。
接著只要在目標元件的 onCreate 方法初始化 observer,並呼叫 addObserver() 即可

public class LifecycleMainActivity extends AppCompatActivity {
  private LifecycleMainActivityObserver mLifecycleMainActivityObserver;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.lifecycle_main_activity);
    mLifecycleMainActivityObserver = new LifecycleMainActivityObserver();
    getLifecycle().addObserver(mLifecycleMainActivityObserver);
  }
}

最後 LifecycleMainActivityObserver 的方法便會對應 LifecycleMainActivity 的生命週期改變自動被呼叫。
 

關於生命感知週期元件的最佳實踐

1.保持 UI Controllers(Activity or Fragment)盡量精簡,它們不該存取自己的資料,而應該交由 ViewModel 來執行,並透過 LiveData 將資料的變更反射回 View。
2.嘗試實作數據驅動的 UI,其中 UI Controller 負責在數據更改時更新視圖,或將用戶操作通知給 ViewModel。
3.應該將數據邏輯放到 ViewModel,把 ViewModel 當作 UI 和其他元件的連接器且 ViewModel 不應該去直接獲取數據(如本地端資料庫,遠端網路)。而是透過其它元件來取得數據,再返回給 UI controller。
4.使用 Data Binding 來建立 View 和 UI Controller 之間的簡潔介面,可讓 View 更具聲明性並大量的減少在 Activity 和 Fragment 需要寫的代碼。也可以考慮使用 Butter Knife 來避免樣版代碼以及建立良好的抽象。
5.若 UI 非常複雜可以考慮建立 Presenter 來處理 UI 的變化。
6.不要在 ViewModel 中引用 View 或 Activity context,否則可能造成記憶體洩漏。
 

生命感知週期的使用案例

1.動畫,當 App 位於後台時停止動畫,返回前台時恢復動畫
2.網路連接,當 App 位於後台時暫停網路,返回前台時恢復網路
3.視頻緩衝,當 App 啟動時盡快啟動緩衝,但直到App完全啟動時再播放視頻。可在App銷毀時終止視頻緩衝。
4.位置更新,當 App 位於後台時使用粗粒度位置更新,返回前台時使用細粒度位置更新。
 

分類
Uncategorized

Android Architecture Components

什麼是 Android Architecture Components

Android Architecture Components(AAC) 是函式庫的集合,可以幫助開發者寫出穩健,可測試,維護性高的 App。
 
AAC 包含了以下函式庫

Data Binding Lifecycles LiveData Navigation Paging
將可觀察資料綁定到 UI 元件 管理 Activity 和 Fragment 生命週期 當資料改變時通知視圖(View) 處理 App 導航功能 循序的從數據來源讀取資料
Room ViewModel WorkManager
流暢存取 SQLite 以生命週期方式管理 UI 相關資料 管理後台任務

 
也可以看到 AAC 涵蓋了 App 的整個架構主體。
基本上有以下 4 個要點

1. lifecycle-aware components 管理 Activity 或 Fragment 的生命週期。

可以避免記憶體洩漏(memory leaks),減少處理配置變更(configuration changes)的成本,更簡易的讀取資料到 UI。

2. LiveData 建立資料物件,當資料改變時會自動通知視圖(Views)。

可以套用 Observer Pattern 的觀念,把 LiveDate 當作 Subject,視圖當作 observer。
視圖會訂閱 LiveData,因此當 LiveData的資料發生變化時,視圖會自動收到通知。

3. ViewModel 保存 UI 相關的資料,讓 App 旋轉螢幕時不會銷毀資料。

ViewModel 就是擔任 MVVM Pattern 的 VM 角色,透過隔離視圖邏輯和數據層邏輯,方便撰寫測試,專注業務邏輯。

4. Room 能更快速的存取資料庫(SQLite)並可搭配 RxJava 協同使用。

Room 為 ORM 的一種,和其它流行的框架類似能夠大量簡化 SQL 語法。
 

分類
Uncategorized

Android Jetpack (噴射背包)

什麼是 Android Jetpack?

Android Jetpack 是一系列軟體元件的組合,透過這些元件可以幫助開發者寫出最佳實踐,避免樣板代碼並簡化複雜任務,讓開發者專注需要關心的代碼。
Jetpack 包含在 androidx.* 的套件名稱,沒有和任何平台 API 綁定,它向後相容並可頻繁更新,代表開發者可以隨時使用最新最好的版本。
 

使用 Jetpack 的好處

1.加速開發

Jetpack 元件可單獨或組合使用,同時利用 Kotlin 提供的特性來加速開發

2.消除樣板代碼

Jetpack 特別用來管理複雜的行為,如後台任務,導航,生命週期。讓開發者專注需要關心的部分

3.建立高品質,穩固的 App

Jetpack 可以減少崩潰,減少記憶體洩漏並提供向後兼容性
 

Android Jetpack Components(元件組)

Jetpack 主要有 4 個分類,各為 Fundation,Architecture,Behavior,UI,每個分類都包含不同數量的元件,各個元件提供特定功能並可單獨或協同使用。

1.Foundation

提供向後兼容,支援 Kotlin,測試相關功能

AppCompat Android KTX Multidex Test
為舊版 App 提供向後兼容性 支援 Kotlin 語言 支援多 DEX 檔案 Android 測試框架,提供單元測試和 UI測試

 

2.Architecture (Android Architecture Components)

提供建立穩固,可測試,可維護性高的 App

Data Binding Lifecycles LiveData Navigation Paging
將可觀察資料綁定到 UI 元件 管理 Activity 和 Fragment 生命週期 當資料改變時通知視圖(View) 處理 App 導航功能 循序的從數據來源讀取資料
Room ViewModel WorkManager
ORM的一種,可流暢存取 SQLite 以生命週期方式管理 UI 相關資料 管理後台任務

 

3.Behavior

讓 App 和標準 Android 服務集成,如 Notification,Permission,Sharing,Assistant

DownloadManager Media & playback Notifications Permissions Sharing Slices
排程和管理大量下載 播放媒體(包含Google Cast) 提供通知功能並可向後兼容 檢查和要求 App 權限 用於 Action bar 的共享操作 可在 App 之外顯示資料的 UI

 

4. UI

提供 Widgets 讓 App 使用簡單且有趣

Animation & transitions Auto Emoji Fragment Layout
用來移動或轉換 widget 幫助開發 Android Auto 元件 可在舊版本上使用最新的Emoji 功能 可元件化的基本單位 關於 App 版面配置
Palette TV Wear OS By Google
從調色板中提取有用訊息 幫助開發 Android TV 元件 幫助開發 Android Wear 元件

 
 
 

分類
Uncategorized

ViewModel 基本紀錄(Android Architecture Component)

Overview

ViewModel 為 Android Architecture Component 其中一部分,主要的用途為以自動管理生命週期方式來儲存與管理 UI 相關數據。好處為當配置更改(螢幕旋轉)之後能夠讓數據繼續存在
Android 管理 UI 控制器(Activity, Fragment)的生命週期,在管理的過程中可能會決定銷毀或重新創建 UI 控制器,以反應某些完全不受控制的使用者操作或設備事件。
如果系統銷毀或重新創建 UI 控制器,則存儲在其中的任何 UI 相關數據都將消失。
例如,App 可能會在其中一個 Activity 中包含使用者清單。當配置更改後重新創建 Activity 時,新的 Activity 必須重新取得使用者清單。
對於簡單的資料,Activity 可以使用 onSaveInstanceState() 方法並從 onCreate() 中恢復數據,但是這種方法僅適用於可以序列化然後反序列化的少量數據,而不適用於大量數據。
另一個問題是 UI 控制器經常需要進行花費一些時間才能返回的異步調用。
UI 控制器需要管理這些調用並確保系統在銷毀後清理它們以避免潛在的內存洩漏,這種管理動作需要大量維護動作,並且當配置更改重新創建物件的情況下會浪費資源,因為物件可能必須重新發出已經進行的調用
Activity 和 Fragment 之類的 UI 控制器主要用於顯示 UI 數據並反應使用者操作或處理系統通信,例如許可請求。
若是要求 UI 控制器也負責從資料庫或從網路加載數據,這會大量增加UI控制器的大小。
另外若 UI 控制器分配過多的責任可能導致單個類別嘗試自己處理應用程序的所有工作,而不是將工作委託給其他類別。以這種方式為 UI 控制器分配過多的責任也會使測試變得更加困難。
因此將視圖數據邏輯與 UI 控制器邏輯分離起來更容易,更有效。
Architecture Components 為 UI 控制器提供 ViewModel,其主要功能為為 UI 準備數據。ViewModel 物件在配置更改期間會自動保留,讓它們保存的數據可立即用於下一個 Activity 或 Fragment 實例。
例如,如果需要在應用程序中顯示使用者清單,請確保把取得使用者清單的工作放在  ViewModel,而不是 Activity 或 Fragment,如下

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<User>>();
            loadUsers();
        }
        return users;
    }
    private void loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

接著你可以使用下方的方式取得使用者清單

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.
        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

如果重新建立 Activity,新的 Activity 將接收由前一個 Activity 的相同 MyViewModel 實體。當 Activity 結束時,將呼叫 ViewModel 的 onCleared() 方法,以便清理資源。
注意:在 ViewModel 中絕對不可參考到視圖(View),LifeCycle,或任何一個類別其可能持有 Activity context。
ViewModel 的設計原則之一就是讓測試容易撰寫,可以在不知道 View 或 LifeCycle 寫測試。
ViewModel 可以包含 LifecycleObservers 物件,如 LiveData。但 ViewModel 永遠不會觀察到生命週期感知的可觀察對象(如 LiveData 物件)的變化。
以架構來說通常是作為 MVVM 的 VM(ViewModel) 角色來呈現, 透過把和 View 不相關的邏輯提取出來放到 VM,一方面可以減輕 View 的職責,一方面提高其它模組內聚力,也讓測試更容易撰寫。

Dependency

參考官網

實作 ViewModel

實作方式非常簡單,只要繼承 ViewModel 即可。通常 ViewModel 會和 LiveData 同時使用關於 LiveDate 請參考這裡
如同 Overview 提到的 ViewModel 可以在配置改變時自動保留數據,因此應該盡量把數據部分放到 ViewModel 而不是 Actvity 或 Fragment

public class MyViewModel extends ViewModel {
  private static final String TAG = MyViewModel.class.getSimpleName();
}

Note:通常會和 LiveData一起使用,但這裡為了方便說明,先不加入 LiveData。

Using in Activity

public class ViewModelMainActivity extends AppCompatActivity {
  private static final String TAG = ViewModelMainActivity.class.getSimpleName();
  private MyViewModel mViewModel;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.view_model_main_activity);
    mViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
    Log.d(TAG,"mViewModel.toString():"+ mViewModel.toString());
  }
}

第12行透過 ViewModelProviders.of(this).get(MyViewModel.class); 初始化 mViewModel。
第13行用來測試旋轉螢幕後是否為同一個 ViewModel 實體。接著你可以開始旋轉螢幕了。
Note :
1.注意在 ViewModel 內絕不可引用 View, Lifecycle, 或是任何本身還會引用 activity context 的類別
若 ViewModel 內需要使用 context 就繼承 AndroidViewModel
2.當呼叫 ViewModelProviders.of(this).get(MyViewModel.class); 之後,ViewModel 就會保存在記憶體中,直到它的作用域消失。也就是當 ViewModelMainActivity 為 finish 狀態時,ViewModel 即會消失。

LiveDate 和 ViewModel 共用

LiveData 的用途為提供 observer 可以觀察本身所擁有的數據,而 ViewModel 則是提供 View 數據並可自動處理配置變化所引發的事件。
這兩者共同使用對於 View 來說便可發揮極大的功效。

public class MyViewModel extends ViewModel {
  private static final String TAG = MyViewModel.class.getSimpleName();
  private MutableLiveData<List<User>> mUsers;
  public LiveData<List<User>> getUsers() {
    if (mUsers == null) {
      mUsers = new MutableLiveData<>();
      loadUsers();
    }
    return mUsers;
  }
  private void loadUsers() {
   ...
  }
}

 

Scope of ViewModel

從圖中可以看到當 Activity 在 onCreate 時,ViewModel 就跟著產生。而當 Activity finish 之後,呼叫完 onDestroy 方法後,ViewModel 就跟著消失。

在 Fragment 之間分享數據

若同一個 Activity 擁有 2 個 Fragment , 也可以使用 ViewModel 讓這 2 個 Fragment 共享數據。

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
    public void select(Item item) {
        selected.setValue(item);
    }
    public LiveData<Item> getSelected() {
        return selected;
    }
}
public class MasterFragment extends Fragment {
    private SharedViewModel model;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}
public class DetailFragment extends Fragment {
 public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // Update the UI.
        });
    }
}

注意這兩個 Fragment 都使用了 getActivity 來取得 ViewModelProvider,因此,這兩個 Fragment 都接受相同的 SharedViewModel 實體。
這個做法有以下優點
1. Activity 不需要做任何事,也不需要了解 Fragment 之間的溝通。
2. Fragment 不需要彼此了解。若其中一個 Fragment 發生問題,另一個 Fragment 繼續正常工作。
 
 
 
 
 
 
 

分類
Uncategorized

LiveData 基本紀錄(Android Architecture Component)

LiveData 為 Android Architecture Component 其中之一,主要用途為持有可觀察數據(observable data),並讓有註冊且為活躍狀態的觀察者自動收到更新。
若了解或實作過 Observer Pattern (觀察者模式)會很容易了解,可以把 LiveData 看作 Subject (主題)也就是資料的來源。對 Subject 有興趣的 observers 可以訂閱主題,訂閱完成後當主題內的數據發生變化時,observers 便會自動收到通知。
基本上 Observer Pattern 是用來取代 polling 的解決方案。關於 Observer Pattern 可以參考這篇
 
回過頭來,首先看看 LiveData 的 OverView (這裡是官網介紹)
首先 LiveData 是一種擁有可觀察數據的類別(看字面不好懂,直接看實作吧) e.g.,

  private MutableLiveData<String> mCurrentName = new MutableLiveData<String>();

因為 LiveData 為 abstract class,通常在實作上會以 MutableLiveData (LiveData’s subclass)取代 LiveData。角括號可以傳入任何型別,包含 Collections,list 等等。
LiveData 最大的特色為它具有生命週期感知(Lifecycle-aware)能力該能力可以讓 LiveData 得知其他元件(Activity, Fragment, Service)的生命週期。
因此可以避免很多必須手動操作的情況,如螢幕旋轉時需要儲存數據,UI 和數據之間的同步,memoey leak 等等。
通常 LiveData 會和 ViewModel 同時使用。

如何使用 LiveData

1.建立 LiveData 實體並持有特定類型的數據。這通常在 ViewModel 類別內實作
2.建立 Observer 物件並定義 onChanged 方法,該方法內容用來描述當 LiveData 持有的數據改變時所作的事。你通常會在 UI Controlller 內(Activity, fragment)建立 Observer 物件
3.使用 observe 方法連結 Observer 物件和 LiveData 物件。observer 方法需要傳入LifecycleOwner 物件,這讓 Observer 物件訂閱 LiveData 物件並讓 LiveData 可以通知改變。你通常會在 UI Controller 內(activity, fragment)連結 Observer 物件
Note:
你可以藉由 observeForever(Observer) 方法來註冊一個 observer 而不需要結合 LifecycleOwner。在這種情況下 observer 會被當作永遠都是活躍的,因此總是會收到改變的資訊。反之,可以使用 removeObserver(Observer) 來移除 observer
當 LiveData 的資料被更新之後,所有的已註冊且相關連的 LifecycleOwner 為活躍的 observers 會收到通知。

Example

1.Dependency
(參考官網)

2.Create LiveData objects

LiveData 可以包含任何類型的數據,如 List,Map 等等。
LiveData 通常儲存在 ViewModel 內並透過 getter 方法來存取。

public class NameViewModel extends ViewModel {
  private MutableLiveData<String> mCurrentName = new MutableLiveData<String>();
  public MutableLiveData<String> getCurrentName() {
    return mCurrentName;
  }
}

Note:確保 LiveData 儲存在 ViewModel 而不是 activity 或 fragment

3.Observe LiveData objects

在大多數的情況下,onCreate 方法為初始觀察 LiveData 位置
在一般的情況下 LiveData 只在數據有更新時且觀察者為活躍的狀態才發送通知。但有 2 個例外,第 1 為當觀察者從非活躍狀態轉為活躍狀態時會收到通知
第 2 為當觀察者是第 2 次從非活躍狀態轉為活躍狀態,則只有在自上次活動狀態以來該值發生變動時才會收到更新
(也就是說當觀察者第 1 次從非活躍轉成活躍一定會收到更新。若是第 2 次從非活躍轉成活躍,且必須儲存的數據發生變化才會收到更新)

public class LiveDataMainActivity extends AppCompatActivity {
  private NameViewModel mViewModel;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.live_data_main_activity);
    mViewModel = ViewModelProviders.of(this).get(NameViewModel.class);
    Observer<String> nameObserver = new Observer<String>() {
      @Override
      public void onChanged(String newName) {
        //call back when mViewModel's mCurrentName changed
      }
    };
    mViewModel.getCurrentName().observe(this, nameObserver);
  }
}

 

4.Update LiveData objects

LiveData 可以透過 setValue 和 postValue 方法來改變數據,2者的差別為 setValue 可用於main Thread,postValue 可用於 worker thread

String newName = "john doe";
mViewModel.getCurrentName().setValue(newName);

因此當 setValue 呼叫之後,所有已註冊且為活躍的 observer 便會觸發 onChanged 方法

完整 source code

NameViewModel.java

import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.ViewModel;
public class NameViewModel extends ViewModel {
  private MutableLiveData<String> mCurrentName = new MutableLiveData<String>();
  public MutableLiveData<String> getCurrentName() {
    return mCurrentName;
  }
}

LiveDataMainActivity.java

import android.arch.lifecycle.Observer;
import android.arch.lifecycle.ViewModelProviders;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class LiveDataMainActivity extends AppCompatActivity {
  private static final String TAG = LiveDataMainActivity.class.getSimpleName();
  private NameViewModel mViewModel;
  private TextView mName;
  private Button mChangeName;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.live_data_main_activity);
    mName = (TextView) findViewById(R.id.name);
    mChangeName = (Button) findViewById(R.id.change);
    mChangeName.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        String newName = "john doe";
        mViewModel.getCurrentName().setValue(newName);
      }
    });
    mViewModel = ViewModelProviders.of(this).get(NameViewModel.class);
    Observer<String> nameObserver = new Observer<String>() {
      @Override
      public void onChanged(String newName) {
        mName.setText(newName);
      }
    };
    mViewModel.getCurrentName().observe(this, nameObserver);
  }
}

live_data_main_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".LiveDataMainActivity">
  <TextView
    android:id="@+id/name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>
  <Button
    android:id="@+id/change"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/name"
    android:text="Change name"
    />
</RelativeLayout>

 
 

分類
Uncategorized

Gson 搭配 Gsonformat 基本使用紀錄

Gson 為 Google 所有,可將 JSON 內容和 java object 互相轉換的開源函式庫。
最大的用途就是可直接將 Server 回傳的 json string 直接轉換成 java object,而不用再一一寫對應的 JsonObject。
而 GsonFormat 為 android studio 的外掛,用途是取得 json 內容之後,可以產生對應的 java object。
兩者搭配起來就可以方便的處理 Server 回傳訊息。
首先是 Gson 的部分:
這裡為Gson github連結,相關的安裝和使用都有介紹。

1.Gson Dependency

在 module 的 build.gradle 加入

dependencies {
...
  implementation 'com.google.code.gson:gson:2.8.5'
}

2.Using in code

2-1. Basic class
首先假設有個 User 類別為對應的 java object

public class User {
  private String name;
  private int age;
  private String sex;
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public int getAge() {
    return age;
  }
  public void setAge(int age) {
    this.age = age;
  }
  public String getSex() {
    return sex;
  }
  public void setSex(String sex) {
    this.sex = sex;
  }
}

2-2. java object to json string
(就是將已存在的java物件轉成json 格式的string)

  @Test
  public void test_UserToJSON() {
    User foxx = new User();
    foxx.setAge(99);
    foxx.setName("Foxx");
    foxx.setSex("male");
    Gson gson = new Gson();
    String result = gson.toJson(foxx, User.class);
    System.out.println("userToJSON:" + result);
    assertEquals("{\"name\":\"Foxx\",\"age\":99,\"sex\":\"male\"}", result);
  }

第9行即透過 gsong.toJson 將 java object 轉成 json string
2-3. json string to java object

  @Test
  public void test_JSONToUser() {
    Gson gson = new Gson();
    String source = "{\"name\":\"Foxx\",\"age\":99,\"sex\":\"male\"}";
    User target = gson.fromJson(source, User.class);
    assertNotNull(target);
  }

第5行透過 gson.fromJson 將 json string 轉成 java object
2-4. json string to list java object

Gson gson = new Gson();
List<RecordInfo> recordInfos = gson.fromJson(httpResult, new TypeToken<List<RecordInfo>>() {}.getType());

透過 TypeToken 把 Json 格式的 String 轉換為 List。
httpResult 即為 Json 格式的 String 內容如下

[
  {
    "type": "POWER"
    "location": "L1",
    "no": "1"
  },
  {
    "type": "POWER",
    "location": "L2",
    "no": "2"
  },
  {
    "type": "POWER",
    "location": "L3",
    "serno": "3"
  }
]

RecordInfo為對應的資料格式如下

public class RecordInfo {
  private String type;
  private String location;
  private String no;
  public String getType() {
    return type;
  }
  public void setType(String type) {
    this.type = type;
  }
  public String getLocation() {
    return location;
  }
  public void setLocation(String location) {
    this.location = location;
  }
  public String getNo() {
    return no;
  }
  public void setNo(String no) {
    this.no = no;
  }
}

 
接著是GsonFormat

1.Install GsonFormat

安裝方式和一般的 android studio 外掛相同。
開啟 android studio -> File -> Settings -> Plugins -> Browse repositories
->輸入 gsonformat -> install -> 安裝完成後 重開 android studio

2.How to use

2-1.首先手動建立對應的空 class (只有類別名稱無其他內容),以上面的 User 為例,我們就先建立一個User的空class。

public class User {
}

2-2.產生對應 json 的內容
假設已取得 Server 回傳的 json string 如下

{
    "name":"Foxx",
    "sex":"male",
    "age":"99"
}

回到 2-1 建立的 User class,點擊上方工具欄的 Code,再點擊 Generate,選擇GsonFormat,在顯示的視窗中貼上 2-2 的內容。

點擊 OK -> 再點擊 OK,完成後 User class 如下

public class User {
  private String name;
  private String sex;
  private String age;
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public String getSex() {
    return sex;
  }
  public void setSex(String sex) {
    this.sex = sex;
  }
  public String getAge() {
    return age;
  }
  public void setAge(String age) {
    this.age = age;
  }
}

從第3~29行即是 GsonFormat 產生的內容。

分類
Uncategorized

OKHttp 基本使用紀錄

OKHttp 是支援 HTTP & HTTP/2 的開源框架,透過它可以很方便發送 HTTP 請求。
具有以下特性:
1.API 設計簡單,可以透過幾行 code 即可發送 HTTP 請求
2.支援同步,異步請求。同步請求會 block 目前的 Thread,異步請求則不會 block
OKHttp官網連結
最新版本釋出
以下紀錄最基本的 get, post, post with json format 的使用方式。

1.相依性

在 module 的 build.gradle 加入

dependencies {
  ...
    implementation 'com.squareup.okhttp3:okhttp:3.11.0'
}

Note:目前(2018/09/28)最新版本為 3.11.0

2. Using in code

為了封裝 OKHtt p建立了 OKHttpWrapper 類別,外部皆透過該類別來使用OKHttp。
OKHttpWrapper.java

import android.util.Log;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.FormBody;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONObject;
public class OKHttpWrapper {
  private static final String TAG = OKHttpWrapper.class.getSimpleName();
  private static OkHttpClient sOKHttpClient = new OkHttpClient();
  public static void sendGet(String URL, Map<String, String> requestParameter, Callback callback) {
    Set<String> keys = requestParameter.keySet();
    for (String value : keys) {
      URL = new StringBuilder(URL + "&" + value + "=" + requestParameter.get(value)).toString();
    }
    Request request = new Request.Builder().url(URL).build();
    Call call = sOKHttpClient.newCall(request);
    call.enqueue(callback);
  }
  public static void sendPost(String URL, Map<String, String> requestParameter, Callback callback) {
    FormBody.Builder builder = new FormBody.Builder();
    Set<String> keys = requestParameter.keySet();
    for (String key : keys) {
      builder.add(key, requestParameter.get(key));
    }
    Request request = new Request.Builder().url(URL).post(builder.build()).build();
    Call call = sOKHttpClient.newCall(request);
    call.enqueue(callback);
  }
  public static void sendPostWithJSONFormat(String URL, Map<String, String> requestParameter,
      Callback callback) {
    MediaType JSON = MediaType.parse("application/json; charset=utf-8");
    RequestBody requestBody = RequestBody.create(JSON, new JSONObject(requestParameter).toString());
    Request request = new Request.Builder().url(URL).post(requestBody).build();
    Call call = sOKHttpClient.newCall(request);
    call.enqueue(callback);
  }
}

第 19 行建立 OKHttpClient 實體,各個請求皆透過該實體來動作。
第 21 行 sendGet 方法為發送 GET 使用,第 1 個參數為接收請求的 URL,第 2 個為請求的參數,第 3 個參數為發送請求之後的回應。
第 31 行 sendPost 方法為發送 POST 使用,第 1 個參數為接收請求的 URL,第 2 個為請求的參數,第 3 個參數為發送請求之後的回應。
第 44 行 sendPostWithJSONFormat 方法為發送 POST 但參數的格式為 JSON,第 1個參數為接收請求的 URL,第 2 個為請求的參數,第 3 個參數為發送請求之後的回應。
可以看到透過 OKHttp 發送請求相當簡單,接下來示範外部如何呼叫OKHttpWrapper。接收請求的Server使用 httpbin
以下為外部

  private void testSendRequest(String name, String id) {
    Map<String, String> httpParameter = new HashMap<>();
    httpParameter.put("Name", name);
    httpParameter.put("ID", id);
    OKHttpWrapper.sendPostWithJSONFormat(
        "http://httpbin.org/post", httpParameter, new Callback() {
          @Override
          public void onFailure(Call call, IOException e) {
          }
          @Override
          public void onResponse(Call call, Response response) throws IOException {
            Log.d(TAG, TAG+" response:"+response.body().string());
          }
        });
  }

第 2~4 行為組裝請求的參數。
第 6 行呼叫 OKHttpWrapper 的 sendPostWithJSONFormat 方法
第 1 個參數為為接受請求的 URL,第 2 個參數為請求的參數,第3個參數為發送請求後的回應,其中 onFailure 為請求失敗的回應。onResponse 則為請求成功的回應。

        testSendRequest("Foxx", "999");
        testSendRequest("Peter", "000");
        testSendRequest("May", "111");

Output

 "json": {
        "ID": "999",
        "Name": "Foxx"
      },
 "json": {
        "ID": "000",
        "Name": "Peter"
      },
 "json": {
        "ID": "111",
        "Name": "May"
      },

 

分類
Uncategorized

Arrays.binarySearch 直接使用於 android resource 的問題

為了方便使用陣列常常會直接在array.xml中宣告2個互相對應的陣列 e.g.,

  <string-array name="type">
    <item>0</item>
    <item>1</item>
    <item>2</item>
    <item>3</item>
    <item>4</item>
    <item>5</item>
  </string-array>
  <string-array name="code">
    <item>A</item>
    <item>C</item>
    <item>B</item>
    <item>E</item>
    <item>F</item>
    <item>G</item>
    <item>D</item>
  </string-array>

若直接在code中使用Arrays.binsearch 會出現 index 錯誤 e.g.,

        String[] targetArray = getResources().getStringArray(R.array.type);
        int index = Arrays.binarySearch(targetArray, 3);
        String[] codeArray = getResources().getStringArray(R.array.code);

原因是因為Arrays.binarySearch只能使用在已經排序(自然排序)過的陣列。
若該陣列還未排序會丟出Exception,說明index有問題。
比較快的解決方式就是自己寫取得對應的元素 e.g.,

    private int findSpecifyIndexInArray(String[] targetArray, String specify){
        for (int i = 0; i < targetArray.length; ++i) {
            if (targetArray[i].equals(specify)) {
                return i;
            }
        }
        return -1;
    }
    String[] targetArray = getResources().getStringArray(R.array.type);
    int index = findSpecifyIndexInArray(targetArray, 3);
    String[] codeArray = getResources().getStringArray(R.array.code);

 

分類
Uncategorized

PhotoView 使用介紹

PhotoView 是一個開源函式庫,主要提供圖片縮放處理,預設已提供兩指縮放,雙擊放大等等基本的手勢功能。
因此若需求相當簡單,可以考慮使用該函式庫來快速完成功能。
GitHub : https://github.com/chrisbanes/PhotoView
相關的相依性以及初始化可以參考官方github。

以下紀錄基本的使用方式:

1. Dependency

1-1.在 project module 的 build.gradle 加入

allprojects {
	repositories {
        maven { url "https://jitpack.io" }
    }
}

1-2.在 module module 的 build.gradle 加入

dependencies {
    implementation 'com.github.chrisbanes:PhotoView:latest.release.here'
}

 

2. declare in xml

<com.github.chrisbanes.photoview.PhotoView
    android:id="@+id/photo_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

注意 com.github.chrisbanes.photoview.PhotoView 命名必須完全相同
 

3. Using in code

3-1. init by resource id

PhotoView photoView = (PhotoView) findViewById(R.id.photo_view);
photoView.setImageResource(R.drawable.image);

3-2. init by bitmap

String photoPath = PHOTO_FILE_PATH + "/" + photoIndex + ".jpg";
Bitmap bitmap = BitmapFactory.decodeFile(photoPath);
PhotoView photoView = (PhotoView) findViewById(R.id.photo_view);
photoView.setImageBitmap(bitmap);

 
4. 雙擊圖片或拖曳圖片即有縮放效果,完全不用加入任何控制的程式碼。

分類
Uncategorized

RecyclerView 基本使用架構

這個基本使用架構主要套用MVP Patterrn,架構中包含1個顯示在V(Activity)的RecyclerView,提供了以下幾個基本功能:
1.RecyclerView可以即時檢視,新增,修改,刪除項目(ItemView)
2.可設定每頁應該包含項目數,切換頁面。
3.透過FinalDB來儲存項目。
4.自定義RecyclerView的ItemView
首先從架構的PackageDiagram來介紹,該架構包含3個package。
主要的package為左上角的basic package,共有6個class,包含MVP組成元件以及RecyclerView的元件。
右上角的database package,共有2個class為提供儲存資料的功能。
下方的common package,只有1個class,提供取得Application context。
Package 相依性如下圖

 
接著從主要的basic package 的 class diagram開始介紹。
MVP構成的元件為RecyclerItemManager(M),RecyclerViewPresenter(P),
RecyclerViewActivity(V),可以看出V和M不直接溝通,而是透過P交互
(雖然在標準的MVP中,V和P是透過interface交互,為了方便介紹架構就不另外為了V和P建立interface)。
RecyclerViewActivity為介面層,只負責初始化UI以及接受使用者輸入並轉發給RecyclerViewPresenter,不包含任何業務邏輯。
RecyclerViewPresenter為邏輯層,主要接受介面層轉發的指令,並再轉發給Model,當Model處理完回傳資料給邏輯層,處理介面邏輯後再回傳給介面層。
RecyclerItemManager為Model,會持有RecyclerItem list,該list是資料的中心,主要處理業務邏輯,不含任何的介面邏輯。
而 RecyclerViewAdapter 和 RecyclerViewHolder 為構成RecyclerView的元件。
RecyclerViewHolder負責管理ItemView(顯示在RecyclerView的項目)。
RecyclerViewAdapter負責建立RecyclerViewHolder以及綁定數據到holder。
最後是RecyclerItem為資料類別,主要是提供數據並顯示在RecyclerView的ItemView。

接著來看看source code。
首先是RecyclerViewActivity,先來看看該Activity使用的介面檔案。

<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"
  tools:context=".basic.RecycleViewActivity">
  <LinearLayout
    android:layout_width="match_parent"
    android:orientation="horizontal"
    android:layout_height="wrap_content">
    <Button
      android:id="@+id/recycler_view_btn_add"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="add"/>
  </LinearLayout>
  <android.support.v7.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>
</LinearLayout>

相當簡單,只包含1個Button,1個RecyclerView。
接著看看RecyclerViewActivity。

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.codefoxx.recycleviewexample.R;
import java.util.List;
public class RecycleViewActivity extends AppCompatActivity implements OnClickListener {
  private static final String TAG = RecycleViewActivity.class.getSimpleName();
  private RecyclerViewPresenter mRecyclerViewPresenter;
  private RecyclerView mRecyclerView;
  private RecyclerViewAdapter mRecyclerViewAdapter;
  private Button mAdd;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.recycler_view_main_activity);
    mRecyclerViewPresenter = new RecyclerViewPresenter(this);
    initData();
    initUI();
  }
  private void initData() {
    mRecyclerViewPresenter.loadData();
  }
  private void initUI() {
    mAdd = (Button) findViewById(R.id.recycler_view_btn_add);
    mAdd.setOnClickListener(this);
    initRecyclerView();
  }
  private void initRecyclerView() {
    mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
    LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
    linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
    mRecyclerView.setLayoutManager(linearLayoutManager);
    setScrollListenerToRecyclerView();
    mRecyclerViewAdapter = new RecyclerViewAdapter(
        mRecyclerViewPresenter.getItemsByPage(0));
    mRecyclerView.setAdapter(mRecyclerViewAdapter);
  }
  public void recyclerViewClickEvent(View view) {
    int position = mRecyclerView.getChildAdapterPosition(view);
    mRecyclerViewPresenter.showActionDialog(position);
  }
  private void setScrollListenerToRecyclerView() {
    mRecyclerView.addOnScrollListener(new OnScrollListener() {
      @Override
      public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        recyclerViewScrollToTopAndLoadPreviousPage(newState);
        recyclerViewScrollToBottomAndLoadNextPage(newState);
      }
    });
  }
  private void recyclerViewScrollToBottomAndLoadNextPage(int newState) {
    boolean isScrollToBottomEdge = !mRecyclerView.canScrollVertically(1);
    if (newState == RecyclerView.SCROLL_STATE_IDLE && isScrollToBottomEdge) {
      mRecyclerViewPresenter.loadNextPage();
    }
  }
  private void recyclerViewScrollToTopAndLoadPreviousPage(int newState) {
    boolean isScrollToTopEdge = !mRecyclerView.canScrollVertically(-1);
    if (newState == RecyclerView.SCROLL_STATE_IDLE && isScrollToTopEdge) {
      mRecyclerViewPresenter.loadPreviousPage();
    }
  }
  public void reloadRecyclerView(int page) {
    List<RecyclerItem> itemsOnPage = mRecyclerViewPresenter.getItemsByPage(page);
    mRecyclerViewAdapter = new RecyclerViewAdapter(itemsOnPage);
    mRecyclerView.setAdapter(mRecyclerViewAdapter);
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch (uiID) {
      case R.id.recycler_view_btn_add:
        mRecyclerViewPresenter.addNewItem();
        break;
    }
  }
}

第16行為MVP的Presenter。
第18~19行為構成RecyclerView的基本元件,要使用RecyclerView必須一併使用這2個元件。
第21行為提供加入RecyclerView的項目用。
第27行為Presenter的初始化,把Activity傳入建構式中綁定View。
第28行初始化資料。
第29行初始化UI介面。
第33行為了保持View的簡單,委託Presenter讀取資料。
第37~38行初始化mAdd按鈕。
第39行初始化RecyclerView(重點)。
第43行初始化mRecyclerView變數。
第44~46行建立LinearLayoutManager為垂直並設定給mRecyclerView。
第47行為mRecyclerView設定滾動的監聽器,此監聽器中會偵測mRecyclerView是否已滾動到該頁最上方或最下方,再觸發讀取上一頁或下一頁的動作。
第48~49行初始化mRecyclerViewAdapter。
第50行設定Adapter到RecyclerView。
note:第42行的initRecyclerView為初始化RecyclerView的動作為固定用法。
第53~56行為設定點擊RecyclerView的事件,注意該方法名稱必須對應於RecyclerView的ItemView所使用的layout xml中的onclick名稱。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
  android:onClick="recyclerViewClickEvent"
...

第58~67行為設定監聽RecyclerView滾動的事件,在滾動中會判斷是否已到該頁最上方或最下方並觸發讀取上下頁的動作。
第69~74行即為當RecyclerView滾動到最下方並讀取下一頁的動作。
第76~81行即為當RecyclerView滾動到最上方並讀取上一頁的動作。
第83~87行為更新RecyclerView的動作,該動作會在model改變之後被觸發。
第90~97行為Activity的介面點擊事件。
接著是RecyclerViewAdapter

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.codefoxx.recycleviewexample.R;
import java.util.List;
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecycleViewHolder> {
  private static final String TAG = RecyclerViewAdapter.class.getSimpleName();
  private List<RecyclerItem> mRecycleItems;
  public RecyclerViewAdapter(List<RecyclerItem> source) {
    mRecycleItems = source;
  }
  @Override
  public RecycleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View itemView = LayoutInflater
        .from(parent.getContext()).inflate(R.layout.recycle_item_view, parent, false);
    RecycleViewHolder viewHolder = new RecycleViewHolder(itemView);
    return viewHolder;
  }
  @Override
  public void onBindViewHolder(RecycleViewHolder holder, int position) {
    String listNumber = mRecycleItems.get(position).getListNumber();
    holder.mListNumber.setText(mRecycleItems.get(position).getListNumber());
    holder.mTime.setText(mRecycleItems.get(position).getTime());
    holder.mDescription.setText(mRecycleItems.get(position).getDescription());
    if (mRecycleItems.get(position).getIsLaunch()) {
      holder.mIsLaunch.setText("已啟用");
    } else {
      holder.mIsLaunch.setText("未啟用");
    }
    if (mRecycleItems.get(position).getIsVibrator()) {
      holder.mIsVibrator.setText("有震動");
    } else {
      holder.mIsLaunch.setText("無震動");
    }
  }
  @Override
  public int getItemCount() {
    return mRecycleItems.size();
  }
}

RecyclerViewAdapter為構成RecyclerView的元件,主要功能為建立ViewHolder以及綁定數據到ViewHolder。
基本的使用方式主要是覆寫3個方法(onCreateVieHolder, onBindViewHolder, getItemCount),並加入相關的實作內容。
第8行注意繼承RecyclerView.Adapter<RecycleViewHolder>
extends RecyclerView.Adapter為固定用法,<RecycleViewHolder>則是自定義類別,ViewHolder也是構成RecyclerView的基本元件。
第11行為adapter持有的數據,也是透過該數據綁定到ViewHolder上。
第18~23行為建立ViewHolder,在該方法中透過LayoutInflater產生需要的itemView並再將itemView傳入viewHolder中,最後回傳viewHolder。
注意第20行的R.layout.recycle_item_view為ItemView使用的介面檔。
note:該方法主要用意在建立viewHolder的介面,目前還未綁定數據到ViewHolder,綁定數據的動作是在onBindViewHolder方法。
第26~41行即為綁定數據到ViewHolder中。
第44~46行為回傳數據的大小。
接著來看看ItemView所使用的介面檔,該介面檔即是描述顯示在RecyclerView中的單個項目。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/basic_recycler_item_baisc"
  android:layout_width="match_parent"
  android:layout_height="110dp"
  android:descendantFocusability="blocksDescendants"
  android:orientation="vertical"
  android:onClick="recyclerViewClickEvent"
  android:weightSum="1">
  <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="0.6">
    <ImageView
      android:id="@+id/basic_recycler_item_iv_uplayout_upSplitLine"
      android:layout_width="fill_parent"
      android:layout_height="1sp"
      android:layout_alignParentTop="true">
    </ImageView>
    <TextView
      android:id="@+id/basic_recycler_item_tv_uplayout_list_number"
      android:textStyle="bold"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="20dp"
      android:layout_marginLeft="10dp"
      android:layout_alignParentLeft="true"
      android:layout_gravity="center"
      android:text="1"
      android:textSize="20sp"
      android:visibility="invisible"/>
    <TextView
      android:id="@+id/basic_recycler_item_tv_uplayout_time"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginLeft="10dp"
      android:layout_centerVertical="true"
      android:text="06:00"
      android:textSize="18sp"/>
    <TextView
      android:id="@+id/basic_recycler_item_et_uplayout_is_launch"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_centerInParent="true"
      android:text="已啟動"
      android:textSize="18sp"/>
    <ImageView
      android:id="@+id/basic_recycler_item_et_uplayout_is_launch_icon"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="8dp"
      android:layout_gravity="center_horizontal"
      android:layout_toLeftOf="@+id/basic_recycler_item_et_uplayout_is_launch"
      android:adjustViewBounds="true"
      android:background="#00000000"
      android:maxHeight="50dp"
      android:maxWidth="50dp"
      android:scaleType="fitCenter"/>
    <TextView
      android:id="@+id/basic_recycler_item_et_uplayout_is_vibrator"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginRight="10dp"
      android:layout_alignParentRight="true"
      android:layout_centerInParent="true"
      android:text="有震動"
      android:textSize="18sp"/>
    <ImageView
      android:id="@+id/basic_recycler_item_et_uplayout_is_vibrator_icon"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="8dp"
      android:layout_gravity="center_horizontal"
      android:layout_toLeftOf="@+id/basic_recycler_item_et_uplayout_is_vibrator"
      android:adjustViewBounds="true"
      android:background="#00000000"
      android:maxHeight="50dp"
      android:maxWidth="50dp"
      android:scaleType="fitCenter"
      />
  </RelativeLayout>
  <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="0.4">
    <ImageView
      android:id="@+id/basic_recycler_item_iv_downlayout_upSplitLine"
      android:layout_width="fill_parent"
      android:layout_height="1sp"
      android:layout_alignParentTop="true">
    </ImageView>
    <TextView
      android:id="@+id/basic_recycler_item_iv_downlayout_description_hint"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
      android:text="描述:"/>
    <TextView
      android:id="@+id/basic_recycler_item_iv_downlayout_description"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_toRightOf="@+id/basic_recycler_item_iv_downlayout_description_hint"
      android:text="這是描述。"/>
    <ImageView
      android:id="@+id/basic_recycler_item_iv_downlayout_downSplitLine"
      android:layout_width="fill_parent"
      android:layout_height="1sp"
      android:layout_alignParentBottom="true"></ImageView>
  </RelativeLayout>
</LinearLayout>

這裡唯一需要注意的是第8行

android:onClick="recyclerViewClickEvent"

這裡為了方便介紹直接使用onClick屬性指定回調方法名稱,該方法名稱即為在
RecyclerViewActivity第53行定義的方法,簡單的說就是當點擊R.id.basic_recycler_item_baisc UI元件時會觸發RecyclerViewActivity第53行定義的方法。
接著是RecyclerViewHolder

import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.codefoxx.recycleviewexample.R;
public class RecycleViewHolder extends RecyclerView.ViewHolder {
  private static final String TAG = RecyclerViewAdapter.class.getSimpleName();
  public LinearLayout mBasicLayout;
  public TextView mListNumber;
  public TextView mTime;
  public TextView mIsLaunch;
  public TextView mIsVibrator;
  public TextView mDescription;
  public RecycleViewHolder(View v) {
    super(v);
    mBasicLayout = (LinearLayout) v.findViewById(R.id.basic_recycler_item_baisc);
    mListNumber = (TextView) v.findViewById(R.id.basic_recycler_item_tv_uplayout_list_number);
    mTime = (TextView) v.findViewById(R.id.basic_recycler_item_tv_uplayout_time);
    mIsLaunch = (TextView) v.findViewById(R.id.basic_recycler_item_et_uplayout_is_launch);
    mIsVibrator = (TextView) v.findViewById(R.id.basic_recycler_item_et_uplayout_is_vibrator);
    mDescription = (TextView) v.findViewById(R.id.basic_recycler_item_iv_downlayout_description);
  }
}

ViewHolder相當簡單,主要是管理ItemView,若想保持封裝性也可以將public 欄位改為private並提供相對應的public方法。
以上就是RecyclerView所使用到有關View的元件。
接著介紹MVP的Presenter

import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.DialogInterface;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import com.codefoxx.recycleviewexample.R;
import com.codefoxx.recycleviewexample.database.RecyclerItemDBWrapper;
import com.krtc.common.tool.DialogFactory;
import java.util.List;
import java.util.UUID;
public class RecyclerViewPresenter {
  private static final String TAG = RecyclerViewPresenter.class.getSimpleName();
  private RecycleViewActivity mRecycleViewActivity;
  public RecyclerViewPresenter(
      RecycleViewActivity recycleViewActivity) {
    mRecycleViewActivity = recycleViewActivity;
  }
  public void showActionDialog(final int position) {
    AlertDialog.Builder actionDialog = new Builder(mRecycleViewActivity);
    RecyclerItem item = RecyclerItemManager.getInstance()
        .getItem(position);
    actionDialog.setTitle("選擇動作" + "提醒為:" + item.getDescription());
    ArrayAdapter<CharSequence> actionDialogAdapter = ArrayAdapter.createFromResource(
        mRecycleViewActivity,
        R.array.basic_recycler_view_item_action_select, android.R.layout.select_dialog_item);
    actionDialog.setAdapter(actionDialogAdapter, new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        final int REVIEW = 0;
        final int ADD = 1;
        final int DELETE = 2;
        final int MODIFY = 3;
        if (which == REVIEW) {
          reviewItem(position);
        } else if (which == ADD) {
          addNewItem();
        } else if (which == DELETE) {
          deleteItem(position);
        } else if (which == MODIFY) {
          modifyItem(position);
        }
        dialog.dismiss();
      }
    });
    actionDialog.show();
  }
  private void modifyItem(int position) {
    RecyclerItemManager.getInstance().modifyItem(position);
    mRecycleViewActivity.reloadRecyclerView(RecyclerItemManager.getInstance().getCurrentPageIndex());
  }
  private void deleteItem(int position) {
    RecyclerItemManager.getInstance().deleteItem(position);
    mRecycleViewActivity.reloadRecyclerView(RecyclerItemManager.getInstance().getCurrentPageIndex());
  }
  public void addNewItem() {
    RecyclerItemManager.getInstance().addItem(generateNewItem());
    mRecycleViewActivity.reloadRecyclerView(RecyclerItemManager.getInstance().getCurrentPageIndex());
  }
  private RecyclerItem generateNewItem() {
    RecyclerItem itemForAdd = new RecyclerItem();
    itemForAdd.setId(UUID.randomUUID().toString());
    itemForAdd.setTime("00:00");
    itemForAdd.setDescription("new description");
    itemForAdd.setIsLaunch(false);
    itemForAdd.setIsVibrator(false);
    itemForAdd.setListNumber("");
    return itemForAdd;
  }
  private void reviewItem(int position) {
    RecyclerItem recyclerItem = RecyclerItemManager.getInstance()
        .getItem(position);
    Toast.makeText(mRecycleViewActivity, recyclerItem.toString(), Toast.LENGTH_LONG)
        .show();
  }
  public void loadPreviousPage() {
    if (RecyclerItemManager.getInstance().isMorePreviousPage()) {
      showSwitchPreviousPageConfirmDialog();
    } else {
      showNoMorePreviousPageToast();
    }
  }
  private void showSwitchPreviousPageConfirmDialog() {
    DialogFactory
        .createTwoButtonAlertDialog(mRecycleViewActivity, "切換頁面", "是否切換到上一頁?", "否",
            new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
              }
            }, "是", new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
                int page = RecyclerItemManager.getInstance().decreaseCurrentPageIndex();
                mRecycleViewActivity.reloadRecyclerView(page);
              }
            }).show();
  }
  private void showNoMorePreviousPageToast() {
    Toast.makeText(mRecycleViewActivity, "已經沒有上一頁了", Toast.LENGTH_SHORT).show();
  }
  public void loadNextPage() {
    if (RecyclerItemManager.getInstance().isMoreNextPage()) {
      showSwitchNextPageConfirmDialog();
    } else {
      showNoMoreNextPageToast();
    }
  }
  private void showSwitchNextPageConfirmDialog() {
    DialogFactory
        .createTwoButtonAlertDialog(mRecycleViewActivity, "切換頁面", "是否切換到下一頁?", "否",
            new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
              }
            }, "是", new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
                int page = RecyclerItemManager.getInstance().increaseCurrentPageIndex();
                mRecycleViewActivity.reloadRecyclerView(page);
              }
            }).show();
  }
  private void showNoMoreNextPageToast() {
    Toast.makeText(mRecycleViewActivity, "已經沒有下一頁了", Toast.LENGTH_SHORT).show();
  }
  public List<RecyclerItem> getItemsByPage(int page) {
    return RecyclerItemManager.getInstance()
        .getItemsByPage(page);
  }
  public void loadData() {
    List<RecyclerItem> itemsForLoad = RecyclerItemDBWrapper.loadBasicRecyclerItem();
    RecyclerItemManager.getInstance().cleanAndAddItems(itemsForLoad);
  }
}

RecyclerViewPresenter主要接受RecyclerViewActivity轉發的指令,處理需要的邏輯之後再將該指令轉發給Model層,因此會持有對View和對Model的引用。
第15行持有對View的引用(mRecycleViewActivity)。
第17~20行藉由建構式傳入View,和mRecycleViewActivity連結。
第22~52行顯示動作對話框,當使用者點擊RecyclerView上的itemview時,該對話框顯示可操作動作。目前提供檢視,新增,刪除,修改,4個功能。
第54~57行為修改itemview動作。
第59~62行為刪除itemview動作。
第64~67行為新增itemview動作。
note:從以上動作內容可以看出一致性為首先委託RecyclerViewManager進行對model的修改,再通知View更新頁面。
第69~78行為新增itemview使用,這裡為了方便介紹所以新增類似的itemview,實際應用上可以提供使用者輸入新增的相關內容。
第80~85行為檢視itemview動作。
第87~93行為當RecyclerView滾動到該頁的最上方時讀取上一頁的內容。
第95~110行為顯示對話框提醒使用者是否切換到上一頁,使用者選擇否,不進行任何動作,使用者選擇是,就進行讀取上一頁的動作。
第112~114行為提醒使用者該頁已為第一頁。
第116~121行為當RecyclerView滾動到該頁的最下方時讀取下一頁的內容(相對於第87~93行的loadPreviousPage)。
第124~139行為顯示對話框提醒使用者是否切換到下一頁,使用者選擇否,不進行任何動作,使用者選擇是,就進行讀取下一頁的動作。
第141~143行為提醒使用者該頁已為最後頁。
第145~148行為委託RecyclerItemManager取得指定頁數的內容(ItemView)。
第150~153行為啟動RecyclerViewActivity時讀取儲存在資料庫的資料,注意這裡也是為了方便介紹所以直接在UI Thread操作資料庫,標準作法應該要在background thread進行。
接著介紹MVP的Model,RecyclerItemManager
RecyclerItemManager會持有一個RecyclerItem的List,該list的來源為資料庫的內容(RecyclerItemDBWrapper),並提供給RecyclerViewActivity來顯示。

import android.util.Log;
import .database.RecyclerItemDBWrapper;
import java.util.ArrayList;
import java.util.List;
public class RecyclerItemManager {
  private static final String TAG = RecyclerItemManager.class.getSimpleName();
  //Must keep this value big than real item, 1000 is big enough
  private static final int ITEM_PER_PAGE = 20;
  private static final RecyclerItemManager sInstance = new RecyclerItemManager();
  private List<RecyclerItem> mAllResults = new ArrayList<>();
  private List<PageHolder> mAllPageHolders = new ArrayList<>();
  private int mCurrentPageIndex;
  private RecyclerItemManager() {
  }
  public static RecyclerItemManager getInstance() {
    return sInstance;
  }
  public int getCurrentPageIndex() {
    return mCurrentPageIndex;
  }
  public void cleanAndAddItems(List<RecyclerItem> source) {
    mAllResults.clear();
    for (RecyclerItem item : source) {
      mAllResults.add(item);
    }
    mAllPageHolders.clear();
    computePages();
  }
  private void computePages() {
    if (mAllResults.size() < ITEM_PER_PAGE) {
      int totalPage = 1;
      List<RecyclerItem> subList = mAllResults;
      mAllPageHolders.add(new PageHolder(totalPage, subList));
    } else {
      int totalPage = computeTotalPage();
      computeItemsOfPage(totalPage);
    }
    showAllPageHolders();
  }
  private void showAllPageHolders() {
    for (PageHolder onePage : mAllPageHolders) {
      Log.d(TAG, "---");
      Log.d(TAG, "PageHolder.getPage():" + onePage.getPage());
      for (RecyclerItem one : onePage.getItems()) {
        Log.d(TAG, "item.getTime():" + one.getTime());
        Log.d(TAG, "item.getDescription():" + one.getDescription());
      }
      Log.d(TAG, "---");
    }
  }
  private int computeTotalPage() {
    int pageCount = mAllResults.size() / ITEM_PER_PAGE;
    int itemsOfEndPage = mAllResults.size() % ITEM_PER_PAGE;
    int totalPage = 0;
    if (itemsOfEndPage == 0) {
      totalPage = pageCount;
    } else {
      totalPage = pageCount + 1;
    }
    return totalPage;
  }
  private void computeItemsOfPage(int totalPage) {
    for (int page = 0; page < totalPage; ++page) {
      int lastPage = totalPage - 1;
      if (page == lastPage) {
        int endElement = mAllResults.size();
        List<RecyclerItem> subList = mAllResults.subList(page * ITEM_PER_PAGE, endElement);
        mAllPageHolders.add(new PageHolder(page, subList));
      } else {
        List<RecyclerItem> subList = mAllResults
            .subList(page * ITEM_PER_PAGE, (page * ITEM_PER_PAGE) + (ITEM_PER_PAGE));
        mAllPageHolders.add(new PageHolder(page, subList));
      }
    }
  }
  public boolean isMorePreviousPage() {
    if (mCurrentPageIndex > 0) {
      return true;
    }
    return false;
  }
  public boolean isMoreNextPage() {
    if (mCurrentPageIndex < mAllPageHolders.size() - 1) {
      return true;
    }
    return false;
  }
  public int decreaseCurrentPageIndex() {
    mCurrentPageIndex--;
    return mCurrentPageIndex;
  }
  public int increaseCurrentPageIndex() {
    mCurrentPageIndex++;
    return mCurrentPageIndex;
  }
  public List<RecyclerItem> getItemsByPage(int page) {
    PageHolder currentPageHolder = mAllPageHolders.get(page);
    return currentPageHolder.getItems();
  }
  public RecyclerItem getItem(int position) {
    PageHolder currentPageHolder = mAllPageHolders.get(mCurrentPageIndex);
    return currentPageHolder.getItems().get(position);
  }
  public void addItem(RecyclerItem item) {
    RecyclerItemDBWrapper.saveBasicRecyclerItem(item);
    cleanAndAddItems(RecyclerItemDBWrapper.loadBasicRecyclerItem());
  }
  public void isCurrentPageIndexValid() {
    if (mCurrentPageIndex == mAllPageHolders.size()) {
      mCurrentPageIndex--;
    }
  }
  public void deleteItem(int position) {
    RecyclerItem itemForDelete = RecyclerItemManager.getInstance().getItem(position);
    RecyclerItemDBWrapper.deleteItem(itemForDelete);
    RecyclerItemManager.getInstance()
        .cleanAndAddItems(RecyclerItemDBWrapper.loadBasicRecyclerItem());
    RecyclerItemManager.getInstance().isCurrentPageIndexValid();
  }
  public void modifyItem(int position) {
    RecyclerItem itemForModify = RecyclerItemManager.getInstance().getItem(position);
    itemForModify.setDescription("alread fix");
    itemForModify.setTime("alread fix");
    RecyclerItemDBWrapper.update(itemForModify);
    RecyclerItemManager.getInstance()
        .cleanAndAddItems(RecyclerItemDBWrapper.loadBasicRecyclerItem());
  }
  private static class PageHolder {
    private int mPage;
    private List<RecyclerItem> mItems;
    public PageHolder(int page, List<RecyclerItem> items) {
      mPage = page;
      mItems = items;
    }
    public int getPage() {
      return mPage;
    }
    public List<RecyclerItem> getItems() {
      return mItems;
    }
  }
}

第11行為設定每頁有多少ItemView的數量。
第12行為Singleton Pattern的寫法(搭配第17~18行,第20~22行)
第13~14行為計算每頁有多少個ItemView用。
Note:第14行的變數較為重要,該List即為每頁的內容 。e.g., 若 list 有3個element,則代表RecyclerView共有3頁,而每個element可以再透過pageHolder.getItems()來取得該頁上的項目。
第15行為表示目前所在頁數。
第17~18行為建構式私有化,目的為不透過建構式讓外界實體化該類別(Singleton Pattern)。
第20~22行為提供外界取得該類別的實體(Singleton Pattern)。
第24~26行取得目前頁數。
第28~36行為重新計算有多少頁,以及每頁的項目。
第38~49行為計算頁數用。基本算法就是藉由ITEM_PER_PAGE來計算每頁的內容。
第51~61行為顯示頁數以及每頁的內容,只是單純用來顯示。
第63~74行計算總頁數。
第76~89行計算每頁的內容。
第91~96為回傳是否還有上一頁。
第98~103為回傳是否還有下一頁。
第105~108行遞減頁數。
第110~113行遞增頁數。
第115~118行取得指定頁數的全部內容。透過mAllPageHolders取得某個element的內容。
第120~123行取得指定頁數的指定內容。透過mAllPageHolders取得某個element的內容再指定某個位置。
第125~128行加入itemview用。首先加入項目到資料庫中再重新刷新頁數及每頁的內容。注意這裡最好也是透過background thread來進行。
第130~134行檢查並更新目前頁數。這個方法比較特別,主要是用於當刪除項目時,若被刪除項目為該頁的最後一項,則目前頁數必須往前移動一頁。
第136~142刪除項目動作。其實作內容為首先從mAllPageHolders取得要刪除項目(第137行),再從資料庫刪除項目(第138行),接著重新刷新list(第139~140行),最後檢查並設定目前頁數是否正確(第141行)。
第144~151行修改項目動作。其實作內容為首先從mAllPageHolders取得要修改的項目(第145行),設定修改內容(第146~147行),再從資料庫更新項目(第148行),接著重新刷新list(第149~150行)。
第153~170行PageHolder為表示某頁及其內容用。mPage為該頁的頁數,mItems為該頁的內容。
最後為RecyclerItem,該類別為基本的資料類別,無邏輯,用來代表RecyclerView上的ItemView。

import net.tsz.afinal.annotation.sqlite.Id;
public class RecyclerItem {
  @Id
  private String id;
  private String listNumber;
  private String time;
  private boolean isLaunch;
  private boolean isVibrator;
  private String description;
  public String getListNumber() {
    return listNumber;
  }
  public void setListNumber(String listNumber) {
    this.listNumber = listNumber;
  }
  public String getTime() {
    return time;
  }
  public void setTime(String time) {
    this.time = time;
  }
  public boolean getIsLaunch() {
    return isLaunch;
  }
  public void setIsLaunch(boolean isLaunch) {
    this.isLaunch = isLaunch;
  }
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    this.description = description;
  }
  public boolean getIsVibrator() {
    return isVibrator;
  }
  public void setIsVibrator(boolean vibrator) {
    isVibrator = vibrator;
  }
  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  @Override
  public String toString() {
    return "BasicRecyclerItem{" +
        "listNumber='" + listNumber + '\'' +
        ", time='" + time + '\'' +
        ", isLaunch=" + isLaunch +
        ", isVibrator=" + isVibrator +
        ", description='" + description + '\'' +
        '}';
  }
}

比較需要注意的是第5行透過設定@Id為主鍵。
最重要的package介紹完之後,接著介紹其他協作package,這些package主要提供Application Context的取得以及資料庫的操作。
首先是database package,主要提供資料庫操作,使用的ORM為FinalDB,關於FinalDB參考這裡
包含2個類別(FinalDBwrapper,RecyclerItemDBWrapper)。
FinalDBWrapper

import .common.RecyclerViewSingletonApplication;
import net.tsz.afinal.FinalDb;
public class FinalDBWrapper {
    private static final String TAG = FinalDBWrapper.class.getSimpleName();
    public static final FinalDb DB_INSTANCE = FinalDb
        .create(RecyclerViewSingletonApplication.getInstance(), "test.db", true);
}

相當簡單,主要就是第6~7行的DB_INSTANCE,初始化該變數並設定資料庫名稱,debug模式。後續操作資料庫的動作都是透過DB_INSTANCE來執行。
而RecyclerViewSingletonApplication就是繼承Application,用來取得context。
RecyclerItemDBWrapper

import .basic.RecyclerItem;
import java.util.List;
public class RecyclerItemDBWrapper {
  public static void saveBasicRecyclerItem(RecyclerItem item) {
    FinalDBWrapper.DB_INSTANCE.save(item);
  }
  public static List<RecyclerItem> loadBasicRecyclerItem() {
    return FinalDBWrapper.DB_INSTANCE.findAll(RecyclerItem.class);
  }
  public static void deleteItem(RecyclerItem itemForDelete) {
    FinalDBWrapper.DB_INSTANCE.delete(itemForDelete);
  }
  public static void update(RecyclerItem itemForModify) {
    FinalDBWrapper.DB_INSTANCE.update(itemForModify);
  }
}

這個類別也相當簡單,主要提供資料庫的具體操作,具體操作都是透過FinalDBWrapper DB_INSTANCE 進行。
最後就是common package,只有一個類別RecyclerViewSingletonApplication,主要提供 Application context的取得

import android.app.Application;
public class RecyclerViewSingletonApplication extends Application {
  private static final String TAG = RecyclerViewSingletonApplication.class.getSimpleName();
  private static RecyclerViewSingletonApplication sUniqueInstance;
  public static RecyclerViewSingletonApplication getInstance() {
    return sUniqueInstance;
  }
  @Override
  public void onCreate() {
    super.onCreate();
    sUniqueInstance = this;
    sUniqueInstance.initData();
  }
  private void initData() {
  }
}

 
實際操作畫面