Google Architecture ViewModel

why發表於2017-12-06

關於ViewModel 介紹,文章不過多闡述。官方文件

本文將從三個方面做一定闡述

  1. 為什麼會推出ViewModel,它能帶來什麼好處?
  2. 一些例子
  3. 相關注意事項

為什麼需要ViewModel

  • 你需要處理配置變化

    • 筆者認為,使用者可以隨時改變配置(i.e 旋轉螢幕,切換語言,切換系統文字大小 ...),這可能會導致當前Activity重建,這些都不受開發者的控制,但是你又不得不處理它
    • 可能很多APP在配置清單檔案中申明瞭每一個Activityorientation = portrait ,但是你無法禁止使用者去改變語言、文字大小。這樣就可能會導致Activity被移除或者重新建立
  • 為什麼onSaveInstanceState依舊不夠

    傳統的做法都是在配置發生變化即 onSaveInstanceState 方法去save data, 在onCreate去restore data

    但是這裡有兩個限制

    • onSaveInstanceState方法不能夠快取較大的資料,筆者之前嘗試快取上百兆資料發現丟擲了TransactionTooLargeException
    • 儲存的資料一定需要實現serializable 或者 Parceable, 但是有時候這些資料來自第三方庫,我們不能修改它,對於某些場景,很難在onSaveonSaveInstanceState中儲存資料

基於上述兩點,ViewModel應運而生

  • 配置改變前後資料儲存與恢復

一些例子

基礎功能

public class ZeroViewModel extends ViewModel {
    public User user;
}
複製程式碼
public class ZeroDemo extends AppCompatActivity {
    private TextView tv;
    private ZeroViewModel vm;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tv_btn);
        tv = findViewById(R.id.tv_simple);
        
        vm = ViewModelProviders.of(this).get(ZeroViewModel.class);
        System.out.println("szw vm.user = " + vm.user);
    }
    
    // android:onClick="onClickSimpleButton"
    public void onClickSimpleButton(View v) {
        vm.user = new User(23, "jorden");
    }
}
複製程式碼

旋轉螢幕,vm.user 依舊 != null

同一個Activity不同例項

  1. 同時存在兩個例項
public class SameClass01 extends AppCompatActivity {
    private TextView tv;
    private ZeroViewModel vm;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tv_btn);
        tv = findViewById(R.id.tv_simple);

        vm = ViewModelProviders.of(this).get(ZeroViewModel.class);
        System.out.println("szw SameClass01 : " + vm.user);

    }
    // launch the second instance
    // android:onClick="onClickSimpleButton"
    public void onClickSimpleButton(View v) {
        vm.user = new User(100, "SuperMario");
        startActivity(new Intent(this, SameClass01.class));
    }
}
複製程式碼

即使有兩個同類的Activity例項,第一個vm.user 持有的依舊是Mario,第二個vm.user 持有null . 這符合筆者的預期

  1. finish再重新建立
public class SameClass02 extends AppCompatActivity {
    private TextView tv;
    private ZeroViewModel vm;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tv_btn);
        tv = findViewById(R.id.tv_simple);
        vm = ViewModelProviders.of(this).get(ZeroViewModel.class);
        System.out.println("szw SameClass02 onCreate() : " + vm.user);
    }
    // android:onClick="onClickSimpleButton"
    public void onClickSimpleButton(View v) {
        vm.user = new User(22, "test");
    }

    // android:onClick="onClickSimpleButton2"
    public void onClickSimpleButton2(View v) {
        System.out.println("szw SameClass02 : saved = "+vm.user);
    }
}
複製程式碼

先啟動SameClass02 ,執行onClickSimpleButton,finish重新開啟,日誌輸出null

上述兩個例子表現正常

和Static申明的變數比較

  1. 基礎比較
public class SameVm {
    public static User user;
}
複製程式碼
public class ZeroDemo extends AppCompatActivity {
    private TextView tv;
    private ZeroViewModel vm;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tv_btn);
        tv = findViewById(R.id.tv_simple);

        String value = savedInstanceState == null ? "emptyBundle" : savedInstanceState.getString("key");
        System.out.println("szw onCreate() " + value);

        vm = ViewModelProviders.of(this).get(ZeroViewModel.class);
        System.out.println("szw vm.user = " + vm.user);

        System.out.println("szw static = "+SameVm.user);
    }

    // android:onClick="onClickSimpleButton"
    public void onClickSimpleButton(View v) {
        vm.user = new User(23, "jorden");
        SameVm.user = new User(21, "king");
    }
}
複製程式碼

旋轉螢幕後,日誌輸出

szw vm.user = User{id=23, name='jorden'}
szw static = User{id=21, name='king’}
複製程式碼
  1. 終止應用Terminate Application

和1同樣的操作

szw vm.user = null
szw static = null
複製程式碼

從上面兩個例子,感覺沒什麼不同。他們都能快取資料,終止應用程式都會被銷燬

它們的不同之處:

  • ViewModel 主要是為了解耦,有點類似於MVP中的P,ViewModel 是MvvM中VM。你能在ViewModel中做非同步操作(i.e訪問網路),你可以改變data並且讓View接受到通知LiveData
  • static value 能被任何類修改,但是ViewModel 是Activity的私有變數,有點類似ThreadLocal
  • ViewModel 可以判斷Activity是正常銷燬或者配置改變,進而做出不同的響應,finish->removedata ,configurationchange->savedata,靜態變數卻不能

相關注意事項

  1. ViewModel不要應用Activity等相關例項,容易造成記憶體洩漏
  2. 如果你需要在ViewMolde中獲取Resource LocationManager等系統服務,可以繼承AndroidViewModel
  3. ViewModel本身不支援事件模型(EventBus),你可以使用LiveData,當然為了解決旋轉螢幕後,再次註冊Observer,重複提示,可以使用SingleLiveEvent
  4. 當系統回收我們應用時,ViewModel 並不能儲存資料,我們依舊需要複寫onSaveInstanceState方法

原始碼如下

public class DupliViewModel extends ViewModel {
    private SingleLiveEvent<String> message = new SingleLiveEvent<>();

    public void fetchMessage(){
        message.setValue("A New Value");
    }

    public LiveData<String> getMessage() {
        return message;
    }
}
複製程式碼
public class DupliObserverDemo extends AppCompatActivity {
    private TextView tv;
    private DupliObserverDemo self;
    private DupliViewModel vm;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tv_btn);
        self = this;
        tv = findViewById(R.id.tv_simple);

        vm = ViewModelProviders.of(this).get(DupliViewModel.class);
        vm.getMessage().observe(this, new Observer<String>() {
            @Override
            public void onChanged(@Nullable String s) {
                System.out.println("szw updated ~");
                Toast.makeText(self, "updated "+s, Toast.LENGTH_SHORT).show();
            }
        });
    }
    
    // android:onClick="onClickSimpleButton"
    public void onClickSimpleButton(View v) {
        vm.fetchMessage();
    }

}
複製程式碼

相關文章