ViewModel和LiveData 使用

官方代碼,建議將代碼下載下來對照著代碼閱讀。

ViewModel

舉個例子:我們在界面上有一個計時器,記錄我們在這個界面停留的時間,但是當我們旋轉屏幕的時候,會導致Activity重新創建實例,onCreate()方法會再次執行,導致計時器會重新從0開始計時。

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.android.lifecycles.step1.ChronoActivity1">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:id="@+id/hello_textview"/>

    <Chronometer
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/hello_textview"
        android:layout_centerHorizontal="true"
        android:id="@+id/chronometer"/>
</RelativeLayout>
public class ChronoActivity1 extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //在onCreate()方法中開始倒計時
        Chronometer chronometer = findViewById(R.id.chronometer);
        long startTime = SystemClock.elapsedRealtime();
       //每次onCreate()方法都會重新設置base
        chronometer.setBase(startTime);
        chronometer.start();
    }
}

當然我們可以通過其他手段解決這個問題,例如當屏幕旋轉的時候不讓Activity重新創建實例。
或者我們可以在onSaveInstanceState()方法中保存相應的數據,然后當Activity重新創建實例
的時候,我們在onCreate()方法中獲取保存的數據,然后設置計時器的開始時間。然后我們再
看看ViewModel的表現。

自定義一個ChronometerViewModel繼承ViewModel

public class ChronometerViewModel extends ViewModel {

    @Nullable
    private Long mStartTime;

    @Nullable
    public Long getStartTime() {
        return mStartTime;
    }

    public void setStartTime(final long startTime) {
        this.mStartTime = startTime;
    }
}

    // 創建或者直接返回一個已經存在的ViewModel
    ChronometerViewModel chronometerViewModel = ViewModelProviders.of(this)
    .get(ChronometerViewModel.class);
    if (chronometerViewModel.getStartTime() == null) {
        //chronometerViewModel如果沒設置過開始時間,那么說明這個新的ViewModel,
       //所以給它設置開始時間
        long startTime = SystemClock.elapsedRealtime();
        chronometerViewModel.setStartTime(startTime);
        chronometer.setBase(startTime);
    } else {
        //否則ViewModel已經在上個Activity的onCreate()方法中創建過了,屏幕旋轉以后,
        //ViewModel會被保存,我們直接獲取ViewModel里持有的時間
        chronometer.setBase(chronometerViewModel.getStartTime());
    }
    chronometer.start();

這樣就可以解決屏幕旋轉以后重新從0開始計時的問題了。
我們看一下關鍵代碼

ChronometerViewModel chronometerViewModel = ViewModelProviders.of(this)
.get(ChronometerViewModel.class);

ViewModelProvidersof()方法,只看方法的注釋,不看方法體

/**
 * 創建一個ViewModelProvider ,只要傳入的Activity存活,ViewModelProvider 就會被一直保留
 * @param activity 一個activity, 在誰的生命周期內,ViewModel會被保留
 * @return 一個ViewModelProvider 實例
 */
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
    initializeFactoryIfNeeded(checkApplication(activity));
    return new ViewModelProvider(ViewModelStores.of(activity), sDefaultFactory);
}

ViewModelProvidersget()方法,只看方法的注釋,不看方法體

//返回一個已經存在的ViewModel或者創建一個新的ViewModel實例
@NonNull
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
    String canonicalName = modelClass.getCanonicalName();
    if (canonicalName == null) {
        throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
    }
    return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}

看一下ViewModel被保留的周期

ViewModelScope.png

注意:Activity走了onDestroy()方法并不帶表這個Activity就結束了。可以通過ActivityisFinishing()方法來判斷。我們發現,在旋轉屏幕的時候isFinishing()方法返回false。在按下返回鍵的時候isFinishing()為true。

@Override
protected void onDestroy() {
    super.onDestroy();
    Log.d(TAG, "onDestroy: " + isFinishing());
}

ViewModel:ViewModel是一個用來為Activity或者Fragment準備和管理數據的類。ViewModel也可以用來處理Activity/Fragment和應用其他部分的通信。

一個ViewModel的創建總是和一個作用域(一個 Fragment/Activity)有關,并且只要這個作用域存活,那么這個ViewModel會被一直保留。例如,如果作用域是一個Activity,那么ViewModel會保留直到Activity結束。

換句話說,這意味著如果ViewModel的所有者,例如一個Activity由于旋轉而被銷毀,但是ViewModel并不會銷毀,新創建的Activity的實例僅僅是重新關聯到已經存在的ViewModel
ViewModel存在的目的是為了獲取并保持對Activity/Fragment重要的信息。Activity/Fragment 應該能夠觀察到 ViewModel的變化。ViewModel通常通過LiveData或者Data Binding來暴露信息。也可以通過其他任何可觀察的對象,例如RxJava中的ObserVable

ViewModel的唯一的作用是管理UI的數據。ViewModel不能訪問UI或者持有Activity/Fragment的引用。

在Fragment之間共享ViewModel

舉個例子:在一個Activity中有兩個Fragment,每個Fragment里面都有一個SeekBar。當其中一個SeekBar進度改變的時候,也更新另外一個Fragment里面的SeekBar的進度。

Activity什么也沒做,就是布局文件里有兩個Fragment

public class Activity_step5 extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_step5_solution);
    }
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.android.lifecycles.step5_solution.Activity_step5">

    <fragment
        android:id="@+id/fragment1"
        android:name="com.example.android.lifecycles.step5_solution.Fragment_step5"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <fragment
        android:id="@+id/fragment2"
        android:name="com.example.android.lifecycles.step5_solution.Fragment_step5"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
</LinearLayout>

Fragment的實現

public class Fragment_step5 extends Fragment {

    private SeekBar mSeekBar;

    private SeekBarViewModel mSeekBarViewModel;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,  Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_step5, container, false);
        mSeekBar = root.findViewById(R.id.seekBar);

        mSeekBarViewModel = ViewModelProviders.of(getActivity()).get(SeekBarViewModel.class);

        subscribeSeekBar();

        return root;
    }

    private void subscribeSeekBar() {
        // Update the ViewModel when the SeekBar is changed.
        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, 
boolean fromUser) {
                //如果是用戶改變了seekbar的進度就更新ViewModel
                if (fromUser) {
                    Log.d("Step5", "Progress changed!");
                    mSeekBarViewModel.seekbarValue.setValue(progress);
                }
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) { }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) { }
        });
        // 當ViewModel改變了時候,更新seekBar的進度
        mSeekBarViewModel.seekbarValue.observe(getActivity(), new Observer<Integer>() {
            @Override
            public void onChanged(@Nullable Integer value) {
                if (value != null) {
                    mSeekBar.setProgress(value);
                }
            }
        });
    }
}

運行程序,可以看到當手動改變其中一個SeekBar的進度,另外一個也會跟著變

LiveData

上面說了,ViewModel通常通過LiveData或者Data Binding來暴露信息。也可以通過其他任何可觀察的對象,例如RxJava中的ObserVableLiveData 與普通的Observable不同,LiveData是生命周期感知的,這意味著它尊重其他應用程序組件的生命周期,例如ActivityFragmentServiceLiveData生命周期感知能力確保 LiveData僅僅去更新那些處于生命周期活動狀態的觀察者。(感覺這個比較厲害了)

注意:正常情況應該在onCreate中訂閱觀察者。如果在onStart或者onResume中訂閱會有問題:

  1. 比如當前Activity onStop了,然后從onStop重新onStart的時候又會訂閱一次,導致重復訂閱。

接著上面的例子:
我們想在Activity之外,每隔一秒鐘,更新Activity的UI。

新建一個LiveDataTimerViewModel

public class LiveDataTimerViewModel extends ViewModel {

    private static final int ONE_SECOND = 1000;
    //新建一個LiveData實例
    private MutableLiveData<Long> mElapsedTime = new MutableLiveData<>();

    private long mInitialTime;

    public LiveDataTimerViewModel() {
        mInitialTime = SystemClock.elapsedRealtime();
        Timer timer = new Timer();

        // 每隔一秒更新一次
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                final long newValue = 
(SystemClock.elapsedRealtime() - mInitialTime) / 1000;
                // setValue() 不能再后臺線程調用,所以使用post到主線程
                mElapsedTime.postValue(newValue);
            }
        }, ONE_SECOND, ONE_SECOND);

    }

    public LiveData<Long> getElapsedTime() {
        return mElapsedTime;
    }
}
public class ChronoActivity3 extends AppCompatActivity {

    private LiveDataTimerViewModel mLiveDataTimerViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.chrono_activity_3);

        mLiveDataTimerViewModel = ViewModelProviders.of(this)
.get(LiveDataTimerViewModel.class);

        subscribe();
    }

    /**
     * 新建一個Observer,然后訂閱 mLiveDataTimerViewModel.getElapsedTime()
     */
    private void subscribe() {
        final Observer<Long> elapsedTimeObserver = new Observer<Long>() {
            @Override
            public void onChanged(@Nullable final Long aLong) {
                String newText = ChronoActivity3.this.getResources().getString(
                        R.string.seconds, aLong);
                ((TextView) findViewById(R.id.timer_textview)).setText(newText);
                Log.d("ChronoActivity3", "Updating timer");
            }
        };

        mLiveDataTimerViewModel.getElapsedTime().observe(this, elapsedTimeObserver);
    }
}

運行上面的代碼你發現,只有在Activity活動的時候(也就是生命周期狀態是STARTEDRESUMED的時候),log日志才會輸出,當你點擊HOME鍵或者打開其他的APP的時候,log日志不會輸出。當你回到Activity的時候,日志才會接著輸出。

看一下LiveDataobserve()方法

 /**
  * 將指定的觀察者添加到LifecycleOwner生命周期之內的觀察者列表中。事件是在主線程分發的。如果LiveData已經被設置了
  * 數據,那么數據會被發送給這個新添加的觀察者。
  *
  * 只有當LifecycleOwner處在活動狀態的時候{@link Lifecycle.State#STARTED} or
  * {@link Lifecycle.State#RESUMED} ,這個observer才會收到事件。
  *
  * 如果LifecycleOwner到了銷毀狀態 {@link Lifecycle.State#DESTROYED},這個observer會被自動移除。
  * 
  * 當這個LifecycleOwner處于不活動的狀態的時候,如果數據改變了,這個observer不會收到任何更新。
  * 當LifecycleOwner重新回到了active的狀態,這個oberver會自動收到最新的數據。
  * 
  * 只要指定的LifecycleOwner沒有被銷毀,LiveData就一直持有observer和LifecycleOwner的強引用
  * 當LifecycleOwner被銷毀了,LiveData會移除observer和LifecycleOwner的引用。
  *
  * 如果指定的LifecycleOwner已經處于銷毀狀態{@linkLifecycle.State#DESTROYED} ,方法直接返回。 
  * 
  * 如果指定的LifecycleOwner和oberver元組已經在觀察這列表里了,方法直接返回。
  *
  * 如果observer已經和另外一個關聯的LifecycleOwner在觀察者列表里了,
  * LiveData拋出IllegalArgumentException。
  *
  * @param owner    LifecycleOwner,用來控制observer
  * @param observer 觀察者,用來接收事件
  */
@MainThread
public void observe(LifecycleOwner owner, @NonNull Observer<T> observer) {
    if (owner.getLifecycle().getCurrentState() == DESTROYED) {
        //LifecycleOwner處于銷毀狀態,直接返回。
        return;
    }
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    LifecycleBoundObserver existing = mObservers.putIfAbsent(observer, wrapper);
    if (existing != null && existing.owner != wrapper.owner) {
        throw new IllegalArgumentException("Cannot add the same observer"
                + " with different lifecycles");
    }
    if (existing != null) {
        return;
    }
    owner.getLifecycle().addObserver(wrapper);
}

注意,AppCompatActivitySupportActivity的子類,而SupportActivity實現了LifecycleOwner。所以AppCompatActivity是一個生命周期持有者。

public class SupportActivity extends Activity implements LifecycleOwner {
...
}

訂閱生命周期事件

我們已經知道SupportActivity是一個生命周期持有者了。一個生命周期持有者會在不同的生命周期發出不同的生命周期事件。我們可以觀察這些事件,并根據這些事件,進行基于生命周期的操作。

LifecycleOwner 獲取Lifecycle的方法

public interface LifecycleOwner {
    /**
     * 返回當前生命周期持有者的生命周期
     */
    @NonNull
    Lifecycle getLifecycle();
}

Lifecycle類用來定義具有Android生命周期的對象。

Lifecycle類的內部類Lifecycle.Event是一個枚舉類,定義了生命周期持有者發出的所有事件類型

枚舉值 描述
ON_ANY 可以用來匹配任何事件
ON_CREATE onCreate事件
ON_DESTROY onDestroy事件
ON_PAUSE onPause事件
ON_RESUME onResume事件
ON_CREATE onCreate事件
ON_START onStart事件
ON_STOP onStop事件

Lifecycle類的內部類Lifecycle.State也是一個枚舉類,定義了生命周期持有者所有的生命周期狀態。如下圖所示。

state.png

舉個例子

我們想在Activity活動的時候,注冊一個LocationListener來獲取位置信息,然后在onPause的時候,移除監聽器,那我們可以通過Activity的生命周期事件來實現。

自定義的LocationListener

private class MyLocationListener implements LocationListener {
        @Override
        public void onLocationChanged(Location location) {
            //位置改變的時候,改變界面上的經緯度
            TextView textView = findViewById(R.id.location);
            textView.setText(location.getLatitude() + ", " + location.getLongitude());
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
        }

        @Override
        public void onProviderEnabled(String provider) {
            Toast.makeText(LocationActivity.this,
                    "Provider enabled: " + provider, Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onProviderDisabled(String provider) {
        }
    }

要觀察生命周期事件,首先要實現LifecycleObserver接口。
BoundLocationListener類實現了 LifecycleObserver接口,當生命周期持有者LifecycleOwner 處于ON_RESUME的狀態的時候,我們獲取定位服務,并在位置變化的時候通知LocationListener更新信息。當生命周期持有者LifecycleOwner 處于ON_PAUSE的狀態的時候我們移除LocationListener

public class BoundLocationManager {

    public static void bindLocationListenerIn(LifecycleOwner lifecycleOwner,
                                              LocationListener listener, Context context) {
        new BoundLocationListener(lifecycleOwner, listener, context);
    }

    @SuppressWarnings("MissingPermission")
    static class BoundLocationListener implements LifecycleObserver {
        private final Context mContext;
        private LocationManager mLocationManager;
        private final LocationListener mListener;

        public BoundLocationListener(LifecycleOwner lifecycleOwner,
                                     LocationListener listener, Context context) {
            mContext = context;
            mListener = listener;
            //注釋1處,觀察LifecycleOwner的生命周期事件
            lifecycleOwner.getLifecycle().addObserver(this);
        }
        
        //通過注解處理不同的生命周期事件
        @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
        void addLocationListener() {
            mLocationManager =
                    (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
            mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0,
 mListener);
            Log.d("BoundLocationMgr", "Listener added");

            // Force an update with the last location, if available.
            Location lastLocation = mLocationManager.getLastKnownLocation(
                    LocationManager.GPS_PROVIDER);
            if (lastLocation != null) {
                mListener.onLocationChanged(lastLocation);
            }
        }
        
        //通過注解處理不同的生命周期事件
        @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
        void removeLocationListener() {
            if (mLocationManager == null) {
                return;
            }
            mLocationManager.removeUpdates(mListener);
            mLocationManager = null;
            Log.d("BoundLocationMgr", "Listener removed");
        }
    }
}

注釋1處,在BoundLocationListener的構造函數中,觀察LifecycleOwner的生命周期事件。然后使用注解在收到Lifecycle.Event.ON_RESUME事件的時候添加位置監聽。在收到Lifecycle.Event.ON_RESUME事件的時候移除位置監聽。

然后將生命周期持有者和生命周期事件觀察者綁定。這里忽略定位權限的處理。

public class LocationActivity extends AppCompatActivity {
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.location_activity);
            bindLocationListener();
        }

 private void bindLocationListener() {
        BoundLocationManager.bindLocationListenerIn(this, mGpsListener, 
                     getApplicationContext());
    }

}

運行程序,不斷旋轉手機的時候,輸出如下

D/BoundLocationMgr: Listener added
D/BoundLocationMgr: Listener removed
D/BoundLocationMgr: Listener added
D/BoundLocationMgr: Listener removed

參考鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容