Android 車載應用開發與分析 (3)- 構建 MVVM 架構(Java版)

語言: CN / TW / HK

前言

在大多數車載系統應用架構中,一個完整的應用往往會包含三層:

  • HMI Human Machine Interface,顯示UI資訊,進行人機互動。

  • Service 在系統後臺進行資料處理,監控資料狀態。

  • SDK 根據業務邏輯Service對外暴露的通訊介面,其他模組通過它來完成IPC通訊。

當然並不是所有的應用都需要Service,只有不能長久的駐留在記憶體中,且需要監控系統資料和行為的應用才需要Service

舉個例子,系統的OTA需要一個Service在IVI的後臺監控雲服務或SOA介面的訊息,然後完成升級包的下載等。也需要一個HMI顯示升級的Release Note、確認使用者是否同意升級等,這個HMI往往會被歸納在系統設定中。ServiceHMI之間的IPC通訊,則需要暴露一個SDK來完成,這個其他模組的HMI也可以通過這個SDK完成與Service的IPC通訊。

反例則是,Launcher 可以長久的駐留在記憶體,所以它也就不需要ServiceSDK

本篇文章主要講解,如在HMI層中構建一個適合車載系統應用的MVVM架構。本文涉及的原始碼:https://github.com/linux-link/CarMvvmArch

MVVM 架構分層邏輯

MVVM 架構的原理以及與MVC&MVP的區別,網上已經有很多相關的優秀文章,這裡就不再贅述,本篇文章將聚焦如何車載應用中利用Jetpack元件將 MVVM 架構真正落地實現。

image.png

當前的Android應用的MVVM架構分層邏輯,都源自圖-2 Android官方給出的指導建議,我們也同樣基於這套邏輯來實現MVVM架構。

image.png

封裝適合車載應用 MVVM 框架

車載應用相對於手機應用來說開發週期和複雜度都要小很多,所以我們封裝的重點是View層,ViewModel 層和 Model 層的封裝則會相對簡單一些。

封裝 Model 層

一般來說我們會把訪問網路的工具類封裝在Model層,但是車載系統應用的 HMI 層通常沒有訪問網路的功能,所以 Model 層我們直接留空即可。

``` public abstract class BaseRepository {

}

```

封裝 ViewModel 層

VideModel 層的封裝很簡單,只需要將Model的例項傳入,方便 ViewModel 的實現類呼叫即可。

封裝 ViewModel

``` public abstract class BaseViewModel extends ViewModel {

protected M mRepository;

public BaseViewModel(M repository) {
    mRepository = repository;
}

public M getRepository() {
    return mRepository;
}

}

```

封裝 AndroidViewModel

``` public abstract class BaseAndroidViewModel extends AndroidViewModel {

protected M mRepository;

public BaseAndroidViewModel(Application application, @Nullable M repository) {
    super(application);
    mRepository = repository;
}

public M getRepository() {
    return mRepository;
}

}

```

封裝 View 層

在 View 層中我們需要引入DatabindingViewModel,並且定義出 View 的一些實現規範。

在實際使用中,並不是每一個介面都需要使用MVVM架構, 所以需要額外封裝一個只引入DatabindingFrangmentActivity

基於 DataBinding 封裝 Fragment

``` public abstract class BaseBindingFragment 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 的基礎上新增 ViewModel

``` public abstract class BaseMvvmFragment extends BaseBindingFragment {

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 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 的基礎上新增 ViewModel

``` public abstract class BaseMvvmActivity extends BaseBindingActivity {

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的VariableId。例如:BR.viewModel.

  • void initObservable(Vm viewModel)

在此處操作ViewModel中LiveData的。例如:下面這類方法,都應該寫在這個方法體裡面。目的是為了便於維護

``` viewModel.getTempLive().observe(this, new Observer() { @Override public void onChanged(String temp) { LogUtils.logI(TAG, "[onChanged] " + temp); } });

```

  • void initView()

在此處進行初始化UI的操作。例如:初始化RecyclerView,設定ClickListener等等。

  • void loadData(Vm viewModel)

在此處使用ViewModel進行請求用於初始化UI的資料。

基於框架實現MVVM架構

接下來我們基於上面封裝的 MVVM 框架,來實現一個最基礎的 MVVM 架構下的demo。

定義公共元件

建立 ViewModelFactory

定義ViewModel的例項化方式,單一Module下ViewModel的建立應該集中在一個ViewModelFactory

``` // default 許可權,不對外部公開此類 class AppViewModelFactory implements ViewModelProvider.Factory {

// 建立 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);
    }
}

}

```

建立 AppInjection

如果應用中沒有使用 DaggerHilt 等依賴注入框架,那麼為了便於日後的維護,無論是車載應用還是手機應用,都建議定義一個AppInjection來將應用中的單例、ViewModel、Repository等例項的獲取統一到一個入口程式中。

``` 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;
}

/**
 * 受保護的許可權,除了ViewModel,其它模組不應該需要Model層的例項
 *
 * @return {@link HvacRepository}
 */
protected static HvacRepository getHvacRepository() {
    return new HvacRepository(getHvacManager());
}

public static HvacManager getHvacManager() {
    return HvacManager.getInstance();
}

}

```

構建 Model 層

在車載應用中 Model 層的主要資料來源無外乎 有三種網路資料來源HMI本地資料來源IPC(程序間通訊)資料來源,其中最常見的是隻有IPC資料來源,三種資料來源都有的情況往往會出現在主機廠商自行開發的車載地圖應用中。所以我們這裡只考慮如何基於IPC資料來源構造Model

定義一個 XXX``Repository 繼承自 BaseRepository,再根據業務需要定義出我們需要使用的介面,這裡的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) {
            // 處理遠端資料,講他轉換為應用中需要的資料格式或內容
            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將監聽的遠端資料處理後返回給ViewModel

如果應用會與多個不同的模組進行IPC通訊,那麼建議將這些由不同模組提供的IPC-SDK封裝在一個Manager中進行統一管理。

構建ViewModel

在Jetpack中ViewModel的用途是封裝介面控制器的資料,以使資料在配置更改後仍然存在。在Android的MVVM 架構設計中,ViewModel是最關鍵的一層,通過持有Repository的引用來進行外部通訊

``` public class HvacViewModel extends BaseViewModel {

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

/**
 * 請求頁面資料
 */
public void requestTemperature() {
    mRepository.requestTemperature();
}

/**
 * 將溫度資料設定到Service中
 *
 * @param view
 */
public void setTemperature(View view) {
    mRepository.setTemperature(getTempLive().getValue());
}

public MutableLiveData<String> getTempLive() {
    if (mTempLive == null) {
        mTempLive = new MutableLiveData<>();
    }
    return mTempLive;
}

}

```

構建View層

最後就是構建View層,一把就是Activity/Fragment和XML。

HvacActivity中各個方法含義我們上面封裝BaseMvvmActivity的時候已經解釋過了,這裡不再贅述。

``` public class HvacActivity extends BaseMvvmActivity {

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

}

```

```

<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>

```

以上就是如何封裝一個適合車載應用使用的 MVVM 框架。不知道你有沒有發現,在HMI中使用AIDL方法。通常是比較麻煩的。我們需要在HMI與Service完成繫結後,我們才能呼叫Service中實現的Binder方法。但是示例中我們使用的SDK,並沒進行繫結操作,而是直接進行呼叫。關於如何編寫基於AIDL的SDK,就放到下一章再介紹,感謝您的閱讀。

本文所涉及的原始碼請訪問:https://github.com/linux-link/CarMvvmArch

參考資料

應用架構指南  |  Android 開發者  |  Android Developers