前言
在大多數(shù)車載系統(tǒng)應(yīng)用架構(gòu)中,一個完整的應(yīng)用往往會包含三層:
HMI
Human Machine Interface,顯示UI信息,進行人機交互。Service
在系統(tǒng)后臺進行數(shù)據(jù)處理,監(jiān)控數(shù)據(jù)狀態(tài)。SDK
根據(jù)業(yè)務(wù)邏輯Service
對外暴露的通信接口,其他模塊通過它來完成IPC通信。
當(dāng)然并不是所有的應(yīng)用都需要Service
,只有不能長久的駐留在內(nèi)存中,且需要監(jiān)控系統(tǒng)數(shù)據(jù)和行為的應(yīng)用才需要Service
。
舉個例子,系統(tǒng)的OTA需要一個Service
在IVI的后臺監(jiān)控云服務(wù)或SOA接口的消息,然后完成升級包的下載等。也需要一個HMI
顯示升級的Release Note、確認用戶是否同意升級等,這個HMI往往會被歸納在系統(tǒng)設(shè)置中。Service
與HMI
之間的IPC通信,則需要暴露一個SDK
來完成,這個其他模塊的HMI
也可以通過這個SDK
完成與Service
的IPC通信。
反例則是,Launcher 可以長久的駐留在內(nèi)存,所以它也就不需要Service
和SDK
。
本篇文章主要講解,如在HMI層中構(gòu)建一個適合車載系統(tǒng)應(yīng)用的MVVM架構(gòu)。本文涉及的源碼:https://github.com/linux-link/CarMvvmArch
MVVM 架構(gòu)分層邏輯
MVVM 架構(gòu)的原理以及與MVC&MVP的區(qū)別,網(wǎng)上已經(jīng)有很多相關(guān)的優(yōu)秀文章,這里就不再贅述,本篇文章將聚焦如何車載應(yīng)用中利用Jetpack組件將 MVVM 架構(gòu)真正落地實現(xiàn)。
當(dāng)前的Android應(yīng)用的MVVM架構(gòu)分層邏輯,都源自圖-2 Android官方給出的指導(dǎo)建議,我們也同樣基于這套邏輯來實現(xiàn)MVVM架構(gòu)。
封裝適合車載應(yīng)用 MVVM 框架
車載應(yīng)用相對于手機應(yīng)用來說開發(fā)周期和復(fù)雜度都要小很多,所以我們封裝的重點是View層,ViewModel 層和 Model 層的封裝則會相對簡單一些。
封裝 Model 層
一般來說我們會把訪問網(wǎng)絡(luò)的工具類封裝在Model層,但是車載系統(tǒng)應(yīng)用的 HMI 層通常沒有訪問網(wǎng)絡(luò)的功能,所以 Model 層我們直接留空即可。
public abstract class BaseRepository {
}
封裝 ViewModel 層
VideModel 層的封裝很簡單,只需要將Model的實例傳入,方便 ViewModel 的實現(xiàn)類調(diào)用即可。
封裝 ViewModel
public abstract class BaseViewModel<M extends BaseRepository> extends ViewModel {
protected M mRepository;
public BaseViewModel(M repository) {
mRepository = repository;
}
public M getRepository() {
return mRepository;
}
}
封裝 AndroidViewModel
public abstract class BaseAndroidViewModel<M extends BaseRepository> extends AndroidViewModel {
protected M mRepository;
public BaseAndroidViewModel(Application application, @Nullable M repository) {
super(application);
mRepository = repository;
}
public M getRepository() {
return mRepository;
}
}
封裝 View 層
在 View 層中我們需要引入Databinding
和ViewModel
,并且定義出 View 的一些實現(xiàn)規(guī)范。
在實際使用中,并不是每一個界面都需要使用MVVM架構(gòu), 所以需要額外封裝一個只引入Databinding
的 Frangment 和 Activity
基于 DataBinding 封裝 Fragment
public abstract class BaseBindingFragment<V extends ViewDataBinding> extends BaseFragment {
private static final String TAG = TAG_FWK + BaseBindingFragment.class.getSimpleName();
protected V mBinding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
LogUtils.logV(TAG, "[onCreateView]");
if (getLayoutId() == 0) {
throw new RuntimeException("getLayout() must be not null");
}
mBinding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false);
mBinding.setLifecycleOwner(this);
mBinding.executePendingBindings();
initView();
return mBinding.getRoot();
}
protected abstract void initView();
@LayoutRes
protected abstract int getLayoutId();
public V getBinding() {
return mBinding;
}
}
在 BindingFragment 的基礎(chǔ)上添加 ViewModel
public abstract class BaseMvvmFragment<Vm extends BaseViewModel, V extends ViewDataBinding> extends BaseBindingFragment<V> {
protected Vm mViewModel;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
initViewModel();
View view = super.onCreateView(inflater, container, savedInstanceState);
initObservable(mViewModel);
if (getViewModelVariable() != 0) {
mBinding.setVariable(getViewModelVariable(), mViewModel);
}
return view;
}
@Override
public void onStart() {
super.onStart();
loadData(getViewModel());
}
private void initViewModel() {
Class<Vm> modelClass;
Type type = getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
modelClass = (Class<Vm>) ((ParameterizedType) type).getActualTypeArguments()[0];
} else {
modelClass = (Class<Vm>) BaseViewModel.class;
}
Object object = getViewModelOrFactory();
if (object instanceof ViewModel){
mViewModel = (Vm) object;
}else if (object instanceof ViewModelProvider.Factory){
mViewModel = new ViewModelProvider(this, (ViewModelProvider.Factory) object)
.get(modelClass);
}else {
mViewModel = new ViewModelProvider(this,
new ViewModelProvider.NewInstanceFactory()).get(modelClass);
}
}
protected abstract Object getViewModelOrFactory();
protected abstract int getViewModelVariable();
protected abstract void initObservable(Vm viewModel);
protected abstract void loadData(Vm viewModel);
protected Vm getViewModel() {
return mViewModel;
}
}
基于 DataBinding 封裝 Activity
public abstract class BaseBindingActivity<V extends ViewDataBinding> extends BaseActivity {
protected V mBinding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getLayoutId() == 0) {
throw new RuntimeException("getLayout() must be not null");
}
mBinding = DataBindingUtil.setContentView(this, getLayoutId());
mBinding.setLifecycleOwner(this);
mBinding.executePendingBindings();
initView();
}
@LayoutRes
protected abstract int getLayoutId();
public V getBinding() {
return mBinding;
}
protected abstract void initView();
}
在 BindingActivity 的基礎(chǔ)上添加 ViewModel
public abstract class BaseMvvmActivity<Vm extends BaseViewModel, V extends ViewDataBinding> extends BaseBindingActivity<V> {
protected Vm mViewModel;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
initViewModel();
super.onCreate(savedInstanceState);
if (getViewModelVariable() != 0) {
mBinding.setVariable(getViewModelVariable(), mViewModel);
}
mBinding.executePendingBindings();
initObservable(mViewModel);
}
@Override
protected void onStart() {
super.onStart();
loadData(mViewModel);
}
private void initViewModel() {
Class<Vm> modelClass;
Type type = getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
modelClass = (Class<Vm>) ((ParameterizedType) type).getActualTypeArguments()[0];
} else {
modelClass = (Class<Vm>) BaseViewModel.class;
}
Object object = getViewModelOrFactory();
if (object instanceof BaseViewModel){
mViewModel = (Vm) object;
}else if (object instanceof ViewModelProvider.Factory){
mViewModel = new ViewModelProvider(this, (ViewModelProvider.Factory) object)
.get(modelClass);
}else {
mViewModel = new ViewModelProvider(this,
new ViewModelProvider.NewInstanceFactory()).get(modelClass);
}
}
protected abstract Object getViewModelOrFactory();
protected abstract int getViewModelVariable();
protected abstract void initObservable(Vm viewModel);
protected abstract void loadData(Vm viewModel);
protected Vm getViewModel() {
return mViewModel;
}
}
重點解釋一下幾個abstract的方法
- Object getViewModelOrFactory()
返回ViewModel的實例或ViewModelFactory實例
- int getViewModelVariable()
返回XML中ViewModel的Variable****Id。例如:BR.viewModel.
- void initObservable(Vm viewModel)
在此處操作ViewModel中LiveData的。例如:下面這類方法,都應(yīng)該寫在這個方法體里面。目的是為了便于維護
viewModel.getTempLive().observe(this, new Observer<String>() {
@Override
public void onChanged(String temp) {
LogUtils.logI(TAG, "[onChanged] " + temp);
}
});
- void initView()
在此處進行初始化UI的操作。例如:初始化RecyclerView,設(shè)定ClickListener等等。
- void loadData(Vm viewModel)
在此處使用ViewModel
進行請求用于初始化UI的數(shù)據(jù)。
基于框架實現(xiàn)MVVM架構(gòu)
接下來我們基于上面封裝的 MVVM 框架,來實現(xiàn)一個最基礎(chǔ)的 MVVM 架構(gòu)下的demo。
定義公共組件
創(chuàng)建 ViewModelFactory
定義ViewModel
的實例化方式,單一Module下ViewModel
的創(chuàng)建應(yīng)該集中在一個ViewModelFactory
中
// default 權(quán)限,不對外部公開此類
class AppViewModelFactory implements ViewModelProvider.Factory {
// 創(chuàng)建 viewModel 實例
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
try {
if (modelClass == HvacViewModel.class) {
return modelClass.getConstructor(HvacRepository.class, AppExecutors.class)
.newInstance(AppInjection.getHvacRepository(), AppExecutors.get());
} else {
throw new RuntimeException(modelClass.getSimpleName() + "create failed");
}
} catch (NoSuchMethodException | IllegalAccessException
| InstantiationException | InvocationTargetException exception) {
exception.printStackTrace();
throw new RuntimeException(exception);
}
}
}
創(chuàng)建 AppInjection
如果應(yīng)用中沒有使用 Dagger 或 Hilt 等依賴注入框架,那么為了便于日后的維護,無論是車載應(yīng)用還是手機應(yīng)用,都建議定義一個AppInjection
來將應(yīng)用中的單例、ViewModel、Repository等實例的獲取統(tǒng)一到一個入口程序中。
public class AppInjection {
// ViewModel 工廠
private final static AppViewModelFactory mViewModelFactory = new AppViewModelFactory();
public static <T extends ViewModel> T getViewModel(ViewModelStoreOwner store, Class<T> clazz) {
return new ViewModelProvider(store, mViewModelFactory).get(clazz);
}
public static AppViewModelFactory getViewModelFactory() {
return mViewModelFactory;
}
/**
* 受保護的權(quán)限,除了ViewModel,其它模塊不應(yīng)該需要Model層的實例
*
* @return {@link HvacRepository}
*/
protected static HvacRepository getHvacRepository() {
return new HvacRepository(getHvacManager());
}
public static HvacManager getHvacManager() {
return HvacManager.getInstance();
}
}
構(gòu)建 Model 層
在車載應(yīng)用中 Model 層的主要數(shù)據(jù)源無外乎 有三種網(wǎng)絡(luò)數(shù)據(jù)源、HMI本地數(shù)據(jù)源、IPC(進程間通信)數(shù)據(jù)源,其中最常見的是只有IPC數(shù)據(jù)源,三種數(shù)據(jù)源都有的情況往往會出現(xiàn)在主機廠商自行開發(fā)的車載地圖應(yīng)用中。所以我們這里只考慮如何基于IPC數(shù)據(jù)源構(gòu)造Model層
定義一個 XXX``Repository
繼承自 BaseRepository
,再根據(jù)業(yè)務(wù)需要定義出我們需要使用的接口,這里的HvacManager
就是service提供的用來進行跨進程通信的IPC-SDK中的入口。
public class HvacRepository extends BaseRepository {
private static final String TAG = IpcApp.TAG_HVAC + HvacRepository.class.getSimpleName();
private final HvacManager mHvacManager;
private HvacCallback mHvacViewModelCallback;
private final IHvacCallback mHvacCallback = new IHvacCallback() {
@Override
public void onTemperatureChanged(double temp) {
if (mHvacViewModelCallback != null) {
// 處理遠程數(shù)據(jù),講他轉(zhuǎn)換為應(yīng)用中需要的數(shù)據(jù)格式或內(nèi)容
String value = String.valueOf(temp);
mHvacViewModelCallback.onTemperatureChanged(value);
}
}
};
public HvacRepository(HvacManager hvacManager) {
mHvacManager = hvacManager;
mHvacManager.registerCallback(mHvacCallback);
}
public void clear() {
mHvacManager.unregisterCallback(mHvacCallback);
}
public void requestTemperature() {
LogUtils.logI(TAG, "[requestTemperature]");
mHvacManager.requestTemperature();
}
public void setTemperature(int temperature) {
LogUtils.logI(TAG, "[setTemperature] " + temperature);
mHvacManager.setTemperature(temperature);
}
public void setHvacListener(HvacCallback callback) {
LogUtils.logI(TAG, "[setHvacListener] " + callback);
mHvacViewModelCallback = callback;
}
public void removeHvacListener(HvacCallback callback) {
LogUtils.logI(TAG, "[removeHvacListener] " + callback);
mHvacViewModelCallback = null;
}
}
Repository
通過一個HvacCallback
將監(jiān)聽的遠程數(shù)據(jù)處理后返回給ViewModel
。
如果應(yīng)用會與多個不同的模塊進行IPC通信,那么建議將這些由不同模塊提供的IPC-SDK封裝在一個Manager中進行統(tǒng)一管理。
構(gòu)建ViewModel
在Jetpack中ViewModel
的用途是封裝界面控制器的數(shù)據(jù),以使數(shù)據(jù)在配置更改后仍然存在。在Android的MVVM 架構(gòu)設(shè)計中,ViewModel
是最關(guān)鍵的一層,通過持有Repository
的引用來進行外部通信
public class HvacViewModel extends BaseViewModel<HvacRepository> {
private static final String TAG = IpcApp.TAG_HVAC + HvacViewModel.class.getSimpleName();
private final HvacRepository mRepository;
// 線程池框架。某些場景,ViewModel訪問Repository中的方法可能會需要切換到子線程。
private final AppExecutors mAppExecutors;
private MutableLiveData<String> mTempLive;
private final HvacCallback mHvacCallback = new HvacCallback() {
@Override
public void onTemperatureChanged(String temp) {
LogUtils.logI(TAG, "[onTemperatureChanged] " + temp);
getTempLive().postValue(temp);
}
};
public HvacViewModel(HvacRepository repository, AppExecutors executors) {
super(repository);
mRepository = repository;
mAppExecutors = executors;
mRepository.setHvacListener(mHvacCallback);
}
@Override
protected void onCleared() {
super.onCleared();
mRepository.removeHvacListener(mHvacCallback);
mRepository.release();
}
/**
* 請求頁面數(shù)據(jù)
*/
public void requestTemperature() {
mRepository.requestTemperature();
}
/**
* 將溫度數(shù)據(jù)設(shè)定到Service中
*
* @param view
*/
public void setTemperature(View view) {
mRepository.setTemperature(getTempLive().getValue());
}
public MutableLiveData<String> getTempLive() {
if (mTempLive == null) {
mTempLive = new MutableLiveData<>();
}
return mTempLive;
}
}
構(gòu)建View層
最后就是構(gòu)建View層,一把就是Activity/Fragment和XML。
HvacActivity
中各個方法含義我們上面封裝BaseMvvmActivity的時候已經(jīng)解釋過了,這里不再贅述。
public class HvacActivity extends BaseMvvmActivity<HvacViewModel, ActivityHvacBinding> {
private static final String TAG = IpcApp.TAG_HVAC + HvacActivity.class.getSimpleName();
@Override
protected int getLayoutId() {
return R.layout.activity_hvac;
}
@Override
protected Object getViewModelOrFactory() {
return AppInjection.getViewModelFactory();
}
@Override
protected int getViewModelVariable() {
return BR.viewModel;
}
@Override
protected void initView() {
}
@Override
protected void initObservable(HvacViewModel viewModel) {
viewModel.getTempLive().observe(this, new Observer<String>() {
@Override
public void onChanged(String temp) {
LogUtils.logI(TAG, "[onChanged] " + temp);
}
});
}
@Override
protected void loadData(HvacViewModel viewModel) {
viewModel.requestTemperature();
}
}
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.mvvm.hmi.ipc.ui.HvacViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:onClick="@{viewModel::setTemperature}"
android:text="確定"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_temperature" />
<EditText
android:id="@+id/et_temperature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@={viewModel.tempLive}"
app:layout_constraintBottom_toBottomOf="@+id/textView"
app:layout_constraintStart_toEndOf="@+id/textView"
app:layout_constraintTop_toTopOf="@+id/textView" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:text="Temperature:"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
以上就是如何封裝一個適合車載應(yīng)用使用的 MVVM 框架。不知道你有沒有發(fā)現(xiàn),在HMI中使用AIDL方法。通常是比較麻煩的。我們需要在HMI與Service完成綁定后,我們才能調(diào)用Service中實現(xiàn)的Binder方法。但是示例中我們使用的SDK,并沒進行綁定操作,而是直接進行調(diào)用。關(guān)于如何編寫基于AIDL的SDK,就放到下一章再介紹,感謝您的閱讀。
本文所涉及的源碼請訪問:https://github.com/linux-link/CarMvvmArch