Android MVVM+Clean架构简介

本文主要介绍Android开发中MVVM Clean架构。

一、ViewModel

ViewModel 类是一种业务逻辑或屏幕级状态容器。它用于将状态公开给界面,以及封装相关的业务逻辑。 它的主要优点是,它可以缓存状态,并可在配置更改后持久保留相应状态。这意味着在 activity 之间导航时或进行配置更改后(例如旋转屏幕时),界面将无需重新提取数据。
在发生配置改变时 Activity 和 Fragment 会被销毁重建,它们内部的临时性数据(不是通过 Intent 传入的数据)就会丢失. 如果把这些临时数据放到 ViewModel 中, 则可以避免数据的丢失。当然也可以利用 onSaveInstanceState 来保留临时数据,但是如果临时数据的量较大,onSaveInstanceState 由于涉及了跨进程通信,较大的数据量会造成 marshallingunmashlling 消耗较大。而利用 ViewModel 其实是没有跨进程通信的消耗。但是它没有 onSaveInstanceState 提供的 Activity 被回收之后的数据恢复功能:在 Activity 位于后台时系统会在内存不足时将其回收,当 Activity 再次回到前台时,系统会把 onSaveInstanceState 中保存的数据通过 onRestoreInstanceState 和 onCreate 里的 savedInstanceState 参数传递给 Activity。

二、LiveData

LiveData是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 activity、fragment 或 service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。

使用 LiveData 具有以下优势:

·确保界面符合数据状态

LiveData 遵循观察者模式。当底层数据发生变化时,LiveData 会通知 Observer 对象。您可以整合代码以在这些 Observer 对象中更新界面。这样一来,您无需在每次应用数据发生变化时更新界面,因为观察者会替您完成更新。

·不会发生内存泄漏

观察者会绑定到 Lifecycle 对象,并在其关联的生命周期遭到销毁后进行自我清理。

·不会因 Activity 停止而导致崩溃

如果观察者的生命周期处于非活跃状态(如返回堆栈中的 activity),它便不会接收任何 LiveData 事件。

不再需要手动处理生命周期

界面组件只是观察相关数据,不会停止或恢复观察。LiveData 将自动管理所有这些操作,因为它在观察时可以感知相关的生命周期状态变化。

·数据始终保持最新状态

如果生命周期变为非活跃状态,它会在再次变为活跃状态时接收最新的数据。例如,曾经在后台的 Activity 会在返回前台后立即接收最新的数据。

·适当的配置更改

如果由于配置更改(如设备旋转)而重新创建了 Activity 或 Fragment,它会立即接收最新的可用数据。

三、Lifecycle

生命周期感知型组件可执行操作来响应另一个组件(如 Activity 和 Fragment)的生命周期状态的变化。

java 复制代码
class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, location -> {
            // update UI
        });
    }
    @Override
    public void onStart() {
        super.onStart();
        Util.checkUserStatus(result -> {
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start();
            }
        });
    }
    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
    }
}

在我们需要执行长时间运行的操作(如 onStart() 中的某种配置检查)时尤其如此。这可能会导致出现一种竞态条件,在这种条件下,onStop() 方法会在 onStart() 之前结束,这使得组件留存的时间比所需的时间要长。

java 复制代码
public class MyObserver implements DefaultLifecycleObserver {
    @Override
    public void onResume(LifecycleOwner owner) {
        connect()
    }
    @Override
    public void onPause(LifecycleOwner owner) {
        disconnect()
    }
}
myLifecycleOwner.getLifecycle().addObserver(new MyObserver());

四、DataBinding

使用声明性格式将布局中的界面组件绑定到应用中的数据源。

java 复制代码
@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        dataBinding = DataBindingUtil.setContentView(this, layout());
        dataBinding.setVariable(getBindingVariable(), getViewModel());
        dataBinding.setLifecycleOwner(this);
        liveDataObserve();
    }

布局中修改如下:

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
    <import type="com.example.mvvm.ui.main.MainViewModel" />
        <variable
            name="viewModel"
            type="MainViewModel" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <com.google.android.material.button.MaterialButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="50sp"
            android:onClick="@{()->viewModel.getUsers()}"
            android:text="json获取"/>
        <com.google.android.material.button.MaterialButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="50sp"
            android:onClick="@{()->viewModel.saveUsers()}"
            android:text="数据库保存"/>
        <com.google.android.material.button.MaterialButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="50sp"
            android:onClick="@{()->viewModel.getUserDetail()}"
            android:text="数据库获取"/>
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_user"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </LinearLayout>
</layout>

BindingAdapter :绑定适配器,是 Jetpack DataBinding 中用来扩展布局 xml 属性行为的注解,允许你针对布局 xml 中的一个或多个属性进行绑定行为扩展,这个属性可以是自定义属性,也可以是原生属性。

java 复制代码
public class ImageHelper {
    @BindingAdapter({"imageUrl"})
    public static void loadImage(ImageView imageView,String url){
        Glide.with(imageView.getContext())
                .load(url)
                .error(R.mipmap.ff)
                .placeholder(R.mipmap.ff)
                .into(imageView);
    }

对应布局中的view则是:

XML 复制代码
    <ImageView
            android:layout_width="90dp"
            android:layout_height="90dp"
            android:scaleType="centerCrop"
            app:imageUrl="@{url}"/>

五、什么是Clean Architecture

Clean Code 的作者 Robert C. Martin (Uncle Bob),写了一本书 Clean Architecture(《代码简洁之道》),从而提出了这个架构 (Clean Architecture)。

代码分为三个独立的层:

  1. Presentation Layer
  2. Domain Layer
  3. Data Layer

Presentation Layer

这部分主要包括我们的Activity,Fragment,ViewModel。Activity与Domain层通讯执行操作,不直接和Data层通讯。用户所有的操作(如点击事件等)在Presentation层通过ViewModel向下传递。

java 复制代码
@Inject
    public MainViewModel(GetUserListCase getUserListCase, GetUserDetailCase getUserDetailCase, SaveUserListCase saveUserListCase) {
        this.getUserListCase = getUserListCase;
        this.getUserDetailCase = getUserDetailCase;
        this.saveUserListCase = saveUserListCase;
    }

    public void getUsers() {
        this.getUserListCase.execute(new UserListObserver(), GetUserListCase.Params.forFileName("users.json"));
    }

    public void saveUsers() {
        this.saveUserListCase.execute(new SaveUserListObserver(), SaveUserListCase.Params.forFileName("users.json"));
    }

    public void getUserDetail() {
        this.getUserDetailCase.execute(new UserDetailsObserver(), GetUserDetailCase.Params.forUser(1));
    }

Domain Layer

这一层主要包含我们所有的Use Case。我们定义一个抽象类,所有的case都要继承这个UseCase,以规范UseCase的使用。基类UseCase会对入参和出参进行规范,入参需要实现IRequestValues接口,出参要实现IResponseValue接口。

java 复制代码
public class GetUserListCase extends UseCase<GetUserListCase.Params,GetUserListCase.UsersEntity > {


    private UserRepository userRepository;

    @Inject
    public GetUserListCase(UserRepository userRepository) {
        super();
        this.userRepository = userRepository;
    }


    @Override
    protected Observable<UsersEntity> buildUseCaseObservable(GetUserListCase.Params params) {
        return userRepository.getUsers(params.fileName);
    }
      public static final class UsersEntity implements IResponseValue {
        private List<UserViewEntity> users;

        public List<UserViewEntity> getUsers() {
            return users;
        }

        public void setUsers(List<UserViewEntity> users) {
            this.users = users;
        }
    }

    public static final class Params implements IRequestValues {

        private final String fileName;

        private Params(String fileName) {
            this.fileName = fileName;
        }
}

UseCase是ViewModel和Repository之间的媒介,当我们需要新增功能时,只需要新增一个UseCase。UseCase面向接口,调用需要使用的Repository接口类中的对应方法。UseCase中通过Hitl依赖注入,调用对应的Repository接口类的实现类。

单个UseCase只应该具备单个的功能,如:获取用户的列表,并且对应User的Repository中单个方法(获取用户的列表)如果需要获取单个用户的详情等场景,需要增加UseCase(如GetUserDetailCase),并调用Repository对应的方法。

Data Layer

这里具有Domain层可以使用的所有存储库。此层向外部类公开数据源 API。

java 复制代码
public class UserDataRepository implements IUserRepository {
    private final Context context;
    @Inject
    Gson gson;
    @Inject
    UserDataMapper userDataMapper;
    @Inject
    AppDbHelper appDbHelper;
    @Inject
    UserDataRepository(@ApplicationContext Context context) {
        this.context = context;
    }

    @Override
    public Observable<List<UserViewEntity>> getUsers(String fileName) {
        return LocalJsonResolutionUtils.getJson(context, fileName).map(it ->
                userDataMapper.transformList(gson.fromJson(it, new TypeToken<List<UserModel>>() {
                }.getType())));
    }

    @Override
    public Observable<UserViewEntity> getUserById(int userId) {
        return appDbHelper.getUserDetail(userId).map(this.userDataMapper::transform);
    }

    @Override
    public Observable<Void> saveUserList(String fileName) {
        return LocalJsonResolutionUtils.getJson(context, fileName).map(it -> {
        appDbHelper.saveUserList(gson.fromJson(it, new               TypeToken<List<UserModel>>() {}.getType()));
        return null;
        });
    }
}

UserDataRepository可以区分从网络或者本地获取数据,当获取的数据发生改变时,只需要修改此处的代码,并进行相应的转换即可,而不需要改变上层代码。

UserDataRepository为IUserRepository接口的具体实现,后续根据业务需要,可以在IUserRepository中新增接口。在业务变动较大的情况下,可以新增IUserRepository的接口实现类,然后通过Hilt对外提供注入,这样只需要修改data层,上层的调用可以基本保持不变。

六、总结Clean架构

优点:

  • 代码比使用普通 MVVM 更容易测试。
  • 代码进一步解耦(最大的优势)。
  • 包结构更易于导航。
  • 该项目甚至更容易维护。
  • 可以更快地添加新功能。

缺点:

  • 它添加了很多额外的类,因此对于低复杂度的项目来说并不理想。
相关推荐
selt7912 小时前
Redisson之RedissonLock源码完全解析
android·java·javascript
Yao_YongChao3 小时前
Android MVI处理副作用(Side Effect)
android·mvi·mvi副作用
非凡ghost4 小时前
JRiver Media Center(媒体管理软件)
android·学习·智能手机·媒体·软件需求
席卷全城4 小时前
Android 推箱子实现(引流文章)
android
齊家治國平天下4 小时前
Android 14 系统中 Tombstone 深度分析与解决指南
android·crash·系统服务·tombstone·android 14
maycho1236 小时前
MATLAB环境下基于双向长短时记忆网络的时间序列预测探索
android
思成不止于此6 小时前
【MySQL 零基础入门】MySQL 函数精讲(二):日期函数与流程控制函数篇
android·数据库·笔记·sql·学习·mysql
brave_zhao7 小时前
达梦数据库(DM8)支持全文索引功能,但并不直接兼容 MySQL 的 FULLTEXT 索引语法
android·adb
sheji34167 小时前
【开题答辩全过程】以 基于Android的网上订餐系统为例,包含答辩的问题和答案
android
easyboot7 小时前
C#使用SqlSugar操作mysql数据库
android·sqlsugar