感受LiveData 與 ViewModel結合之美

LiveDataViewModel都是Android官方架構組件(Android Architecture Components)之一。

1.前言

雖說這篇是說LiveDataViewModel,但是或多或少都有涉及另外一個組件:Lifecycles 。它們連同Room都是在17年穀歌IO大會推出的,當時還是預覽版,大致17年底時推出了正式版。到今年的IO大會過後,又增加了許多新成員。

可以看到27.0.0的v7庫有依賴Lifecycles

當時Lifecycles有集成進SupportActivity

其實一開始我沒有太當回事。。。直到27.1.0以後:

好吧,今天的主角出現了,LiveDataViewModel。看到這裏我覺得是該瞭解一波了。

順便看一下截止目前最新的v7:

發現好多常用的組件分離出了v4包,比如ViewPagerSwipeRefreshLayout,這裏就不多說了。

2.優勢

LiveData 是一個可以感知 Activity 、Fragment生命週期的數據容器。當 LiveData 所持有的數據改變時,它會通知相應的界面代碼進行更新。同時,LiveData 持有界面代碼 Lifecycle 的引用,這意味着它會在界面代碼(LifecycleOwner)的生命週期處於 started 或 resumed 時作出相應更新,而在 LifecycleOwner 被銷燬時停止更新。

上面的描述介紹了LiveData的優點:不用手動控制生命週期,不用擔心內存泄露,數據變化時會收到通知。

ViewModel 將視圖的數據和邏輯從具有生命週期特性的實體(如 Activity 和 Fragment)中剝離開來。直到關聯的 Activity 或 Fragment 完全銷燬時,ViewModel 纔會隨之消失,也就是說,即使在旋轉屏幕導致 Fragment 被重新創建等事件中,視圖數據依舊會被保留。ViewModels 不僅消除了常見的生命週期問題,而且可以幫助構建更爲模塊化、更方便測試的用戶界面。

ViewModel的優點也很明顯,爲Activity 、Fragment存儲數據,直到完全銷燬。尤其是屏幕旋轉的場景,常用的方法都是通過onSaveInstanceState()保存數據,再在onCreate()中恢復,真的是很麻煩。

其次因爲ViewModel存儲了數據,所以ViewModel可以在當前ActivityFragment中實現數據共享。

那麼LiveDataViewModel的組合使用可以說是雙劍合璧,而Lifecycles貫穿其中。

3.基本使用

這裏我們按照官方Demo來簡單說明,

1. 數據儲存

/**
 * A ViewModel used for the {@link ChronoActivity3}.
 */
public class LiveDataTimerViewModel extends ViewModel {

    private static final int ONE_SECOND = 1000;

    private MutableLiveData<Long> mElapsedTime = new MutableLiveData<>();

    private long mInitialTime;

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

        // Update the elapsed time every second.
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                final long newValue = (SystemClock.elapsedRealtime() - mInitialTime) / 1000;
                // setValue() cannot be called from a background thread so post to main thread.
                mElapsedTime.postValue(newValue);
            }
        }, ONE_SECOND, ONE_SECOND);

    }

    public LiveData<Long> getElapsedTime() {
        return mElapsedTime;
    }
}

LiveDataTimerViewModel很簡單,在初始化時啓動一個定時任務,每隔一秒通過postValue方法刷新一下數據。

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();
    }

    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也很簡單,創建一個觀察者elapsedTimeObserver。當LiveDataTimerViewModel中數據有變化時,它就會接收到最新的數據。當然你的頁面要處於STARTED 或者 RESUMED。除非你使用observeForever來觀察數據,有興趣的可以去查看源碼來了解實現原理。

mLiveDataTimerViewModel.getElapsedTime().observeForever(elapsedTimeObserver);

2. Fragmnet 之間數據共享

public class SeekBarViewModel extends ViewModel {

    public MutableLiveData<Integer> seekbarValue = new MutableLiveData<>();
}

SeekBarViewModel中存儲一個Integer類型的數據。

public class Fragment_step5 extends Fragment {

    private SeekBar mSeekBar;

    private SeekBarViewModel mSeekBarViewModel;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View root = inflater.inflate(R.layout.fragment_step5, container, false);
        mSeekBar = root.findViewById(R.id.seekBar);
        //注意這裏是getActivity()
        mSeekBarViewModel = ViewModelProviders.of(getActivity()).get(SeekBarViewModel.class);

        subscribeSeekBar();
        return root;
    }

    private void subscribeSeekBar() {

        // 當SeekBar變化時,更新ViewModel中的數據.
        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if (fromUser) {
                    Log.d("Step5", "Progress changed!");
                    mSeekBarViewModel.seekbarValue.setValue(progress);
                }
            }
            ......
        });

        // 當ViewModel數據變化時,更新SeekBar。
        mSeekBarViewModel.seekbarValue.observe(this, new Observer<Integer>() {
            @Override
            public void onChanged(@Nullable Integer value) {
                if (value != null) {
                    mSeekBar.setProgress(value);
                }
            }
        });
    }
}

實現效果:

這個頁面是上下各有一個Fragment_step5的Fragment,Fragment中各有一個SeekBar。效果是拖動其中的SeekBar,另一邊的SeekBar也會隨之一樣變化。

4.簡化使用

這裏我寫了一個小小的工具庫Saber來處理(好吧,猝不及防的廣告。。。),使用註解處理器(Annotation Processor)將繁瑣的代碼自動生成。

首先創建一個類,使用@LiveData註解標記你要保存的數據。注意這裏的參數名稱,下面會用到。

public class SeekBar {

    @LiveData
    Integer value;
}

Build – > Make Project 生成代碼如下:

public class SeekBarViewModel extends ViewModel {
  private MutableLiveData<Integer> mValue;

  public MutableLiveData<Integer> getValue() {
    if (mValue == null) {
      mValue = new MutableLiveData<>();
    }
    return mValue;
  }

  public Integer getValueValue() {
    return getValue().getValue();
  }

  public void setValue(Integer mValue) {
    if (this.mValue == null) {
      return;
    }
    this.mValue.setValue(mValue);
  }

  public void postValue(Integer mValue) {
    if (this.mValue == null) {
      return;
    }
    this.mValue.postValue(mValue);
  }
}

提供了ViewModel的常用操作。setXXX()要在主線程中調用,而postXXX()既可在主線程也可在子線程中調用。一般情況下可以直接使用。比如上面的Fragment例子。簡化爲:

public class TestFragment extends Fragment {

    private SeekBar mSeekBar;

    @BindViewModel(isShare = true) //<--標記需要綁定的ViewModel
    SeekBarViewModel mSeekBarViewModel;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View root = inflater.inflate(R.layout.fragment_test, container, false);
        mSeekBar = root.findViewById(R.id.seekBar);
        Saber.bind(this); // <--這裏綁定ViewModel
        subscribeSeekBar();
        return root;
    }

    private void subscribeSeekBar() {

        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if (fromUser) {
                    mSeekBarViewModel.setValue(progress);
                }
            }
            ......
        });
    }

    @OnChange(model = "mSeekBarViewModel") //<--接收變化
    void setData(Integer value){ //注意這裏使用 @LiveData 標記的參數名
        if (value != null) {
            mSeekBar.setProgress(value);
        }
    }
}

默認使用@BindViewModel用於數據儲存,如果需要Fragment之間數據共享,需要@BindViewModel(isShare = true),當然也要保證傳入相同的key值。默認key值是類的規範名稱,也就是包名加類名。

這裏寫圖片描述

所以一旦需要互通的Fragment類名或包名不一致,就無法數據共享。這時可以指定key值:@BindViewModel(key = "value")

對於第一個例子我們可以這樣使用:

public class LiveDataTimerViewModel extends TimerViewModel {// <-- 繼承生成的ViewModel

    private static final int ONE_SECOND = 1000;

    private long mInitialTime;

    public LiveDataTimerViewModel() {

        mInitialTime = SystemClock.elapsedRealtime();
        Timer timer = new Timer();

        // Update the elapsed time every second.
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                final long newValue = (SystemClock.elapsedRealtime() - mInitialTime) / 1000;
                // setValue() cannot be called from a background thread so post to main thread.
                postTime(newValue); //<--直接使用post方法。
            }
        }, ONE_SECOND, ONE_SECOND);

    }

}

Activity如下:

public class ChronoActivity3 extends AppCompatActivity {

    private TextView textView;

    @BindViewModel
    LiveDataTimerViewModel mTimerViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);       
        textView = this.findViewById(R.id.tv);
        Saber.bind(this); // <-- 綁定
    }

    @OnChange(model = "mTimerViewModel")
    void setData(Long time){
        String newText = MainActivity.this.getResources().getString(R.string.seconds, time);
        textView.setText(newText);
        Log.d("ChronoActivity3 ", "Updating timer");
    }
}

是不是使用起來更加的簡潔了,如果一個頁面有多個ViewModel可能效果更加的明顯。

5.原理

因爲實現大量借鑑了butterknife,所以使用方法與butterknife幾乎一模一樣。是不是想起了 butterknife@BindView@OnClick

其實原理也不復雜,就是生成一個類來幫我們來獲取ViewModel並實現數據的變化監聽。如下:

public class MainActivity_Providers implements UnBinder {
  private MainActivity target;

  @UiThread
  public MainActivity_Providers(MainActivity target) {
    this.target = target;
    init();
  }

  private void init() {
    target.mTimerViewModel = ViewModelProviders.of(target).get(LiveDataTimerViewModel.class);
    target.mTimerViewModel.getTime().observe(target, new Observer<Long>() {
      @Override
      public void onChanged(Long value) {
        target.setData(value);
      }
    });
  }

  @CallSuper
  @UiThread
  public void unbind() {
    MainActivity target = this.target;
    if (target == null) {
      throw new IllegalStateException("Bindings already cleared.");
    }
    this.target = null;
  }
}

所有代碼已上傳至Github。希望大家多多點贊支持!有什麼問題及建議也可以提Issues,讓我們將他慢慢的完善起來。

6.參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章