本文主要介绍Android开发中MVVM Clean架构。
一、ViewModel
ViewModel 类是一种业务逻辑或屏幕级状态容器。它用于将状态公开给界面,以及封装相关的业务逻辑。 它的主要优点是,它可以缓存状态,并可在配置更改后持久保留相应状态。这意味着在 activity 之间导航时或进行配置更改后(例如旋转屏幕时),界面将无需重新提取数据。
在发生配置改变时 Activity 和 Fragment 会被销毁重建,它们内部的临时性数据(不是通过 Intent 传入的数据)就会丢失. 如果把这些临时数据放到 ViewModel 中, 则可以避免数据的丢失。当然也可以利用 onSaveInstanceState 来保留临时数据,但是如果临时数据的量较大,onSaveInstanceState 由于涉及了跨进程通信,较大的数据量会造成 marshalling 和 unmashlling 消耗较大。而利用 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)。
代码分为三个独立的层:
- Presentation Layer
- Domain Layer
- 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 更容易测试。
- 代码进一步解耦(最大的优势)。
- 包结构更易于导航。
- 该项目甚至更容易维护。
- 可以更快地添加新功能。
缺点:
- 它添加了很多额外的类,因此对于低复杂度的项目来说并不理想。