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阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容