Activity
和 Fragment
做爲操做系統和應用之間的粘合類,不該該將全部代碼寫在它們裏面,它們甚至能夠當作是有生命週期的普通 View,大部分狀況下就是 被 用來簡單 顯示數據的api/friends
響應,好友詳情頁也顯示了好友的備註名,數據來源於服務器的 api/user
響應,此時在好友詳情頁更改了對這個好友的備註名,那麼好友列表並不知情,它的數據模型並無發生變化,因此仍是顯示原來的備註名,這就產生了數據不一致的問題ViewModel
用來爲指定的 UI 組件提供數據,它只負責根據業務邏輯獲取合適的數據,他不知道 View 的存在,因此它不受系統銷燬重建的影響,通常它的生命週期比 View 更長久
LiveData
是一個數據持有者,它持有的數據能夠是任何 Object 對象。它相似於傳統觀察者模式中的 Observable,當它持有的數據發生變化時會通知它全部的 Observer。同時它還能夠感知 Activity,Fragment 和 Service 的生命週期,只通知它們中 active 的,在生命週期結束時自動取消訂閱Activity/Fragment
持有 ViewModel
進行數據的渲染,ViewModel
持有 LiveData
形式的數據以便尊重應用組件的生命週期,可是獲取 LiveData
的具體實現應該由 Repository 完成ViewModel
不關心數據具體是怎麼得到的,甚至能夠不關心數據究竟是從哪拿到的建立項目時要勾選 【Use AndroidX artifacts】 複選框以便自動使用 AndroidX 支持庫,不然須要手動在 gradle.properties
文件中添加php
android.useAndroidX=true
android.enableJetifier=true
複製代碼
而後在項目根目錄建立 versions.gradle
文件,以便統一管理依賴和版本號java
ext.deps = [:]
def build_versions = [:]
build_versions.min_sdk = 14
build_versions.target_sdk = 28
ext.build_versions = build_versions
def versions = [:]
versions.android_gradle_plugin = "3.3.0"
versions.support = "1.1.0-alpha01"
versions.constraint_layout = "1.1.3"
versions.lifecycle = "2.0.0"
versions.room = "2.1.0-alpha04"
versions.retrofit = "2.5.0"
versions.okhttp = "3.12.1"
versions.junit = "4.12"
versions.espresso = "3.1.0-alpha4"
versions.atsl_runner = "1.1.0-alpha4"
versions.atsl_rules = "1.1.0-alpha4"
def deps = [:]
deps.android_gradle_plugin = "com.android.tools.build:gradle:$versions.android_gradle_plugin"
def support = [:]
support.app_compat = "androidx.appcompat:appcompat:$versions.support"
support.v4 = "androidx.legacy:legacy-support-v4:$versions.support"
support.constraint_layout = "androidx.constraintlayout:constraintlayout:$versions.constraint_layout"
support.recyclerview = "androidx.recyclerview:recyclerview:$versions.support"
support.cardview = "androidx.cardview:cardview:$versions.support"
support.design = "com.google.android.material:material:$versions.support"
deps.support = support
def lifecycle = [:]
lifecycle.runtime = "androidx.lifecycle:lifecycle-runtime:$versions.lifecycle"
lifecycle.extensions = "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle"
lifecycle.java8 = "androidx.lifecycle:lifecycle-common-java8:$versions.lifecycle"
lifecycle.compiler = "androidx.lifecycle:lifecycle-compiler:$versions.lifecycle"
deps.lifecycle = lifecycle
def room = [:]
room.runtime = "androidx.room:room-runtime:$versions.room"
room.compiler = "androidx.room:room-compiler:$versions.room"
deps.room = room
def retrofit = [:]
retrofit.runtime = "com.squareup.retrofit2:retrofit:$versions.retrofit"
retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit"
deps.retrofit = retrofit
deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:${versions.okhttp}"
deps.junit = "junit:junit:$versions.junit"
def espresso = [:]
espresso.core = "androidx.test.espresso:espresso-core:$versions.espresso"
deps.espresso = espresso
def atsl = [:]
atsl.runner = "androidx.test:runner:$versions.atsl_runner"
deps.atsl = atsl
ext.deps = deps
複製代碼
以顯示 谷歌的開源倉庫列表(api.github.com/users/googl…)爲例,先依賴好 ViewModel
、LiveData
和 Retrofit
:android
apply plugin: 'com.android.application'
android {
compileSdkVersion build_versions.target_sdk
defaultConfig {
applicationId "cn.frank.sample"
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation deps.support.app_compat
implementation deps.support.constraint_layout
implementation deps.lifecycle.runtime
implementation deps.lifecycle.extensions
annotationProcessor deps.lifecycle.compiler
implementation deps.room.runtime
annotationProcessor deps.room.compiler
implementation deps.retrofit.runtime
implementation deps.retrofit.gson
implementation deps.okhttp_logging_interceptor
testImplementation deps.junit
androidTestImplementation deps.atsl.runner
androidTestImplementation deps.espresso.core
}
複製代碼
而後根據習慣合理地設計源碼的目錄結構,如 git
public class RepoRepository {
private static RepoRepository sInstance;
public RepoRepository() {
}
public static RepoRepository getInstance() {
if (sInstance == null) {
synchronized (RepoRepository.class) {
if (sInstance == null) {
sInstance = new RepoRepository();
}
}
}
return sInstance;
}
public LiveData<List<Repo>> getRepo(String userId) {
final MutableLiveData<List<Repo>> data = new MutableLiveData<>();
ServiceGenerator.createService(GithubService.class)
.listRepos(userId)
.enqueue(new Callback<List<Repo>>() {
@Override
public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {
data.setValue(response.body());
}
@Override
public void onFailure(Call<List<Repo>> call, Throwable t) {
}
});
return data;
}
}
複製代碼
public class RepoViewModel extends AndroidViewModel {
private LiveData<List<Repo>> repo;
private RepoRepository repoRepository;
public RepoViewModel(@NonNull Application application) {
super(application);
this.repoRepository = ((SampleApp) application).getRepoRepository();
}
public void init(String userId) {
if (this.repo != null) {
return;
}
this.repo = repoRepository.getRepo(userId);
}
public LiveData<List<Repo>> getRepo() {
return repo;
}
}
複製代碼
public class RepoFragment extends Fragment {
private static final String ARG_USER_ID = "user_id";
private RepoViewModel viewModel;
private TextView repoTextView;
public RepoFragment() {
}
public static RepoFragment newInstance(String userId) {
RepoFragment fragment = new RepoFragment();
Bundle args = new Bundle();
args.putString(ARG_USER_ID, userId);
fragment.setArguments(args);
return fragment;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_repo, container, false);
repoTextView = (TextView) rootView.findViewById(R.id.repo);
return rootView;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Bundle args = getArguments();
if (args != null) {
String userId = args.getString(ARG_USER_ID);
viewModel = ViewModelProviders.of(this).get(RepoViewModel.class);
viewModel.init(userId);
viewModel.getRepo().observe(this, new Observer<List<Repo>>() {
@Override
public void onChanged(List<Repo> repos) {
StringBuilder builder = new StringBuilder();
if (repos != null) {
for (Repo repo : repos) {
builder.append(repo.getFull_name()).append("\n");
}
}
repoTextView.setText(builder);
}
});
}
}
}
複製代碼
這是最簡單直接的實現,但仍是存下不少模板代碼,還有不少地方能夠優化github
findViewById()
再 setText()
呢?在聲明或者建立 View 的時候就給它指定好對應的 ViewModel 不是更簡單直接麼ViewModel
的實現真的優雅嗎?init()
方法和 getRepo()
方法耦合的嚴重麼?ViewModel
應該在什麼時刻開始加載數據?對於第一個問題,Data Binding 組件是一個還算不錯的實現,能夠在佈局文件中使用 表達式語言 直接給 View 綁定數據,綁定能夠是單向的也能夠是雙向的。Data Binding 這樣綁定能夠避免內存泄漏,由於它會自動取消綁定。能夠避免空指針,由於它會寬容評估表達式。能夠避免同步問題,能夠在後臺線程更改非集合數據模型,由於它會在評估時本地化數據
爲了使用 Data Binding,須要在 app module 的 build.gradle
文件中添加數據庫
dataBinding {
enabled = true
}
複製代碼
利用 @{}
語法能夠給 View 的屬性綁定數據變量,可是該表達式語法應該儘量簡單直接,複雜的邏輯應該藉助於自定義 BindingAdapter
api
<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}"/>
</LinearLayout>
</layout>
複製代碼
不須要從新編譯代碼,構建工具就會爲每一個這樣的佈局文件自動生成一個對應的綁定類,繼承自 ViewDataBinding
,路徑爲 app/build/generated/data_binding_base_class_source_out/debug/dataBindingGenBaseClassesDebug/out/cn/frank/sample/databinding/FragmentRepoBinding.java
,默認的類名是佈局文件名的大駝峯命名加上 Binding 後綴,如 fragment_repo.xml
對應 FragmentRepoBinding
,能夠經過 <data class=".ContactItem">
自定義類名和所在包名。能夠經過 DataBindingUtil
的 inflate()
等靜態方法或自動生成的綁定類的 inflate()
等靜態方法獲取綁定類的實例,而後就能夠操做這個實例了數組
這個表達式語言的 操做符和關鍵字 包括: 數學運算 + - / * %
,字符串拼接 +
,邏輯 && ||
,二進制運算 & | ^
,一元操做符 + - ! ~
,移位 >> >>> <<
,比較 == > < >= <=
,判斷實例 instanceof
,分組 ()
,字符/字符串/數字/null
的字面量,強制轉化,方法調用,字段訪問,數組訪問 []
,三目運算符 ?:
,二目空缺省運算符 ??
緩存
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
android:text="@{user.displayName ?? user.lastName}"
android:text="@{user.lastName}"
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
複製代碼
小於比較符 <
須要轉義爲 <
,爲了不字符串轉義單引號和雙引號能夠隨便切換使用
<import>
的類衝突時能夠取別名加以區分服務器
<import type="android.view.View"/>
<import type="com.example.real.estate.View" alias="Vista"/>
複製代碼
<include>
佈局中能夠傳遞變量
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
<include layout="@layout/name" bind:user="@{user}"/>
<include layout="@layout/contact" bind:user="@{user}"/>
</LinearLayout>
</layout>
複製代碼
不支持 <merge>
結合 <include>
的使用
View 事件的分發處理有兩種機制,一種是 Method references,在表達式中直接經過監聽器方法的簽名來引用,Data Binding 會在編譯時評估這個表達式,若是方法不存在或者簽名錯誤那麼編譯就會報錯,若是表達式評估的結果是 null
那麼 Data Binding 就不會建立監聽器而是直接設置 null
監聽器,Data Binding 在 綁定數據的時候 就會建立監聽器的實例: android:onClick="@{handlers::onClickFriend}"
。一種是 Listener bindings,Data Binding 在 事件發生的時候 纔會建立監聽器的實例並設置給 view而後評估 lambda 表達式,android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
雖然 View 能夠綁定任何 PO 對象,可是所綁定對象的更改並不能自動引發 View 的更新,因此 Data Binding 內置了 Observable
接口和它的 BaseObservable
,ObservableBoolean
等子類能夠方便地將對象、字段和集合變成 observable
private static class User {
public final ObservableField<String> firstName = new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
複製代碼
private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
複製代碼
有時候綁定須要當即執行,如在 onBindViewHolder()
方法中:
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
複製代碼
Data Binding 在爲 View 設置表達式的值的時候會自動選擇對應 View 屬性的 setter 方法,如 android:text="@{user.name}"
會選擇 setText()
方法,可是像 android:tint
屬性沒有 setter 方法,可使用 BindingMethods
註解自定義方法名
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
複製代碼
若是要自定義 setter 方法的綁定邏輯,可使用 BindingAdapter
註解
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
複製代碼
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />
複製代碼
@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.get().load(url).error(error).into(view);
}
複製代碼
若是要自定義表達式值的自動類型轉換,可使用 BindingConversion
註解
<View android:background="@{isError ? @color/red : @color/white}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
複製代碼
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
複製代碼
ViewModel
能夠實現 Observable
接口並結合 PropertyChangeRegistry
能夠更方便地控制數據更改後的行爲
使用 @={}
符號能夠實現 View 和數據的雙向綁定
<CheckBox android:id="@+id/rememberMeCheckBox" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="@={viewmodel.rememberMe}" />
複製代碼
public class LoginViewModel extends BaseObservable {
// private Model data = ...
@Bindable
public Boolean getRememberMe() {
return data.rememberMe;
}
public void setRememberMe(Boolean value) {
// 爲了防止無限循環,必需要先檢查再更新
if (data.rememberMe != value) {
data.rememberMe = value;
saveData();
notifyPropertyChanged(BR.remember_me);
}
}
}
複製代碼
自定義屬性的雙向綁定還須要藉助 @InverseBindingAdapter
和 @InverseBindingMethod
@BindingAdapter("time")
public static void setTime(MyView view, Time newValue) {
// Important to break potential infinite loops.
if (view.time != newValue) {
view.time = newValue;
}
}
@InverseBindingAdapter("time")
public static Time getTime(MyView view) {
return view.getTime();
}
複製代碼
監聽屬性的更改,事件屬性以 AttrChanged
做爲後綴
@BindingAdapter("app:timeAttrChanged")
public static void setListeners( MyView view, final InverseBindingListener attrChange) {
// Set a listener for click, focus, touch, etc.
}
複製代碼
能夠藉助轉換器類定製 View 的顯示規則
<EditText android:id="@+id/birth_date" android:text="@={Converter.dateToString(viewmodel.birthDate)}" />
複製代碼
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.
}
}
複製代碼
Data Binding 內置了 android:text
,android:checked
等的雙向綁定
在 Activity 或 Fragment 的生命週期方法中進行其它組件的配置並不老是合理的,如在 onStart()
方法中註冊廣播接收器 A、開啓定位服務 A、啓用組件 A 的監聽、啓用組件 B 的監聽等等,在 onStop()
方法中註銷廣播接收器 A、關閉定位服務 A、停用組件 A 的監聽、停用組件 B 的監聽等等,隨着業務邏輯的增長這些生命週期方法變得愈來愈臃腫、愈來愈亂、愈來愈難以維護,若是這些組件在多個 Activity 或 Fragment 上使用那麼還得重複相同的邏輯,就更難以維護了。 並且若是涉及到異步甚至 沒辦法保證 onStart()
方法中的代碼 必定 在 onStop()
方法執行前執行
關注點分離,這些組件的行爲受生命週期的影響,因此它們本身應該意識到本身是生命週期敏感的組件,當生命週期變化時它們應該 本身決定 本身的行爲,而不是交給生命週期的擁有者去處理
生命週期有兩個要素: 事件和狀態,生命週期事件的發生通常會致使生命週期狀態的改變
生命週期敏感組件應該實現 LifecycleObserver
以觀察 LifecycleOwner
的生命週期,支持庫中的 Activity 和 Fragment 都實現了 LifecycleOwner
,能夠直接經過它的 getLifecycle()
方法獲取 Lifecycle
實例
MainActivity.this.getLifecycle().addObserver(new MyLocationListener());
複製代碼
class MyLocationListener implements LifecycleObserver {
private boolean enabled = false;
@OnLifecycleEvent(Lifecycle.Event.ON_START)
void start() {
if (enabled) {
// connect
}
}
public void enable() {
enabled = true;
if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
// connect if not connected
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
void stop() {
// disconnect if connected
}
}
複製代碼
GenericLifecycleObserver
接口繼承了 LifecycleObserver
,有一個接口方法 onStateChanged(LifecycleOwner, Lifecycle.Event)
代表它能夠接收全部的生命週期過渡事件
它的 observe()
方法源碼
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
assertMainThread("observe");
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// ignore
return;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
owner.getLifecycle().addObserver(wrapper);
}
複製代碼
說明 LiveData
只能在主線程中訂閱,訂閱的觀察者被包裝成生命週期組件的觀察者 LifecycleBoundObserver
class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserve @NonNull final LifecycleOwner mOwner;
LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer)
super(observer);
mOwner = owner;
}
@Override
boolean shouldBeActive() {
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}
@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());
}
@Override
boolean isAttachedTo(LifecycleOwner owner) {
return mOwner == owner;
}
@Override
void detachObserver() {
mOwner.getLifecycle().removeObserver(this);
}
}
複製代碼
當觀察到生命週期狀態變化時會調用 onStateChanged()
方法,因此當狀態爲 DESTROYED
的時候會移除數據觀察者和生命週期觀察者,shouldBeActive()
方法的返回值代表只有生命週期狀態是 STARTED
和 RESUMED
的 LifecycleOwner
對應的數據觀察者纔是 active 的,只有 active 的數據觀察者纔會被通知到,當數據觀察者 第一次 從 inactive 變成 active 時,也會 收到通知
observeForever()
方法也能夠訂閱,可是 LiveData
不會自動移除數據觀察者,須要主動調用 removeObserver()
方法移除
LiveData
的 MutableLiveData
子類提供了 setValue()
方法能夠在主線程中更改所持有的數據,還提供了 postValue()
方法能夠在後臺線程中更改所持有的數據
能夠繼承 LiveData
實現本身的 observable 數據,onActive()
方法代表有 active 的觀察者了,能夠進行數據更新通知了,onInactive()
方法代表沒有任何 active 的觀察者了,能夠清理資源了
單例的 LiveData
能夠實現多個 Activity 或 Fragment 的數據共享
能夠對 LiveData
持有的數據進行變換,須要藉助 Transformations
工具類
private final PostalCodeRepository repository;
private final MutableLiveData<String> addressInput = new MutableLiveData();
public final LiveData<String> postalCode =
Transformations.switchMap(addressInput, (address) -> {
return repository.getPostCode(address);
});
複製代碼
private LiveData<User> getUser(String id) {
...;
}
LiveData<String> userId = ...;
LiveData<User> user = Transformations.switchMap(userId, id -> getUser(id) );
複製代碼
LiveData
的 MediatorLiveData
子類能夠 merge 多個 LiveData 源,能夠像 ReactiveX 的操做符同樣進行各類變換
void businessLogic() {
showLoadingView();
request(uri, params, new Callbacks() {
@Override
void onSuccess(Result result) {
showDataView(result);
}
@Override
void onFailure(Error error) {
showErrorView(error);
}
});
}
複製代碼
void businessLogic() async {
showLoadingView()
Result result = await request(uri, params)
result.ok() ? showDataView(result) : showErrorView(result)
}
複製代碼