一、前言
AAC出现之前,已经有了 DataBinding 帮助我们构建 MVVM 的应用程序,但仍有许多缺陷,比如:
Activity / Fragment 销毁时,VM 并不知情,在一段时间内收到数据变更后还会试图去刷新UI导致Crash(虽然,在 ViewDataBinding 类中最后执行 executePendingBindings 之前会检查 mRoot(即根视图)是否 attach,仍有小概率Crash);同样也可能有内存泄露。
AAC的出现,弥补上这点(代码写的太烂导致的问题,这不能怪 Google哈),AAC的设计目标就是:健壮、易测、可维护!
另一个重要的原则是应该。
模型是负责处理应用数据的组件。它们独立于应用中的 View 对象和应用组件,因此不受应用的生命周期以及相关的关注点的影响。
持久性是理想之选,原因如下:
- 如果 Android 操作系统销毁应用以释放资源,用户不会丢失数据。
- 当网络连接不稳定或不可用时,应用会继续工作。
应用所基于的模型类应明确定义数据管理职责,这样将使应用更可测试且更一致。
AAC的组件库:
- LifeCycle-Aware Component(可感知的生命周期的组件)
- LiveData(可感知的数据更新)
- ViewModel(视图模型)
- Repository(数据仓库)
- Room(数据抽象层,期望用来基于SQLite持久数据)
基于AAC的MVVM应用架构如下:
final-architecture.png
二、组件介绍
2.1、LifeCycle-Aware Component
它是由Lifecycle,LifecycleOwner组成!
2.1.1、LifeCycle
Lifecycle 是一个用来保存组件生命周期的状态信息的类,它允许其他对象观察这些状态信息。
主要使用了Event 和 State 这两个枚举类来跟踪绑定的组件的生命周期状态信息。
- Event:生命周期事件。这些事件与 activities 和 fragments 的事件形成映射关系。
- State:组件的生命周期状态,由 Lifecycle 对象进行跟踪。
2.1.2、LifecycleOwner
LifecycleOwner 是只有一个方法的接口,表示这个类有一个 Lifecycle。只有一个 getLifecycle() 方法:
- 从粗糙的显示界面到精细的显示界面的切换。
- 停止和开始视频缓冲。
- 开始和停止网络连接。
- 暂停和恢复动画效果。
2.1.3、示例
没有 LifeCycle 时,我们是这么实现定位功能的:
class MyLocationListener {
public MyLocationListener(Context context, Callback callback) {
// ...
}
void start() {
// connect to system location service
}
void stop() {
// disconnect from system location service
}
}
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
@Override
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, (location) -> {
// update UI
});
}
@Override
public void onStart() {
super.onStart();
myLocationListener.start();
// manage other components that need to respond to the activity lifecycle
}
@Override
public void onStop() {
super.onStop();
myLocationListener.stop();
// manage other components that need to respond to the activity lifecycle
}
}
使用 LifeCycle 来实现定位功能:
class MyLocationListener implements LifecycleObserver {
private boolean enabled = false;
public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
lifecycle.addObserver(this);
...
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
void start() {
if (enabled) {
// connect
}
}
public void enable() {
enabled = true;
if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
// connect if not connected
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
void stop() {
// disconnect if connected
}
}
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
// update UI
});
Util.checkUserStatus(result -> {
if (result) {
myLocationListener.enable();
}
});
}
}
2.2、LiveData
。
和其他可被观察的类不同的是,LiveData 是有生命周期感知能力的,这意味着它可以在 activities, fragments 或者 services 生命周期是活跃状态时更新这些组件。
lifecycle 中提到的 STARTED 和 RESUMED就是活跃状态,只有在这两个状态下LiveData 是会通知数据变化的。
当对应的生命周期对象 DESTORY 时,才能移除观察者。
对于 activities 和 fragments 非常重要,因为他们可以在生命周期结束的时候立刻解除对数据的订阅,从而避免内存泄漏等问题。
- 界面与数据保持一致性;
- 避免内存泄漏;
- 不会再产生由于Activity处于stop状态而引起的崩溃(如果观察者的生命周期是不活跃的,例如 activity 处于后台,那么将不会收到任何 LiveData 事件);
- 不再需要手动的管理生命周期;
- 总是实时刷新数据。当组件处于活跃状态或者从不活跃状态到活跃状态时总是能收到最新的数据;
- 适当的configuration changes。如果 activity 或者 fragment 因为 configuration change 重新创建,例如设备旋转,立刻能收到最新的数据;
- 资源共享。使用单例模式来扩展 LiveData 包装系统服务,数据可以在你的应用程序间共享;
示例:
LiveData 其实是一个包装类,适用于任何数据类型,通常与 ViewModel 共同使用。如下示例演示了如何创建一个 LiveData 对象。
class UserProfileViewModel : ViewModel() {
var user = MutableLiveData<User>(); // LiveData 是抽象类,实际用 MutableLiveData
}
2.3、ViewModel
ViewModel 对象为特定的界面组件(如 Fragment 或 Activity)提供数据,并包含数据处理业务逻辑,以与模型进行通信。
例如,ViewModel 可以调用其他组件来加载数据,还可以转发用户请求来修改数据。
ViewModel 不了解界面组件,因此不受配置更改(如在旋转设备时重新创建 Activity)的影响。
image1.png
- 避免由于(Activity/Fragment)被系统随时销毁或重新创建引起的数据丢失。
- 对于简单的数据可以使用 onSaveInstanceState()保存,在onCreate()中恢复。
- 对于少量的用户数据,比如UI状态是没有问题的。
- 但是对于大量的数据,比如用户列表,这样做就会不合适。
- 避免 UI 组件自己管理的维护成本以及容易产生的内存泄漏。
- 由于 UI 组件频繁的异步请求,需要很多时间等待结果回调,UI 组件需要人为的管理这些回调。
- 不仅浪费资源,还容易因为界面已经销毁而产生内存泄漏。
- 避免职责过度集中;
- UI 组件需要对用户的操作作出响应,并且处理和操作系统的通信,同时还需要从数据库或者网络载入对应的数据。
- 一个类所需要负责的事情太多了,不仅使代码臃肿,也造成了测试难度加大。
示例:
// User.kt
data class User(
var name: String? = null,
var password: String? = null
)
// UserViewModel.kt
class UserViewModel : ViewModel() {
var user = MutableLiveData<User>()
fun start() {
Thread {
while (true) {
Thread.sleep(3000)
user.postValue(User(
UUID.randomUUID().toString().substring(0, 8),
UUID.randomUUID().toString().substring(0, 4)
))
}
}.start()
}
}
访问 ViewModel
// layout
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
// Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// androidx 2.0.0 写法;
// 1.x 写法 ViewModelProvider.of
val userViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(UserViewModel::class.java)
userViewModel.user.observe(this, Observer { user ->
text.text = "${user.name} - ${user.password}"
})
}
}
2.4、Repository
Repository 这层的思想很简单:ViewModel 层需要获取数据,数据来自:
- 本地(SQLite、SharedPreference、文件、内存)
- 网络
因此,Repository 可以很好的理解为数据抽象层(数据中心 / 数据仓库)。
ViewModel 不需要关心数据从哪里来,只在需要时,向对应的 Repository 请求即可!
// RepoCallback.kt
typealias onRepoCallback<T> = (data: T) -> Unit?
// UserRpository.kt
class UserRepository {
fun start(callback: onRepoCallback<User>) {
Thread {
while (true) {
Thread.sleep(3000)
callback(User(
UUID.randomUUID().toString().substring(0, 8),
UUID.randomUUID().toString().substring(0, 4)
))
}
}.start()
}
}
// UserViewModel.kt
class UserViewModel : ViewModel() {
var user = MutableLiveData<User>()
private val userRepository = UserRepository()
fun getUserData() {
userRepository.start { data -> user.postValue(data) }
}
}
可以看到:
- 回调方式定义了一个全局泛型变量(通过 typealias)
- Repository 入参使用了该变量(换成 Retrofit 调用,再回调不用再修改 VM)
- ViewModel 添加 Repository 实例,并采用 closure 方式完成数据的更新(postValue到主线程)
2.5、Room
Room在SQLite上提供了一个方便访问的抽象层。App把经常需要访问的数据存储在本地将会大大改善用户的体验。这样用户在网络不好时仍然可以浏览内容。当用户网络可用时,可以更新用户的数据。
- Room 是一个对象映射库,可利用最少的样板代码实现本地数据持久性。
- 在编译时,它会根据数据架构验证每个查询,这样损坏的 SQL 查询会导致编译时错误而不是运行时失败。
- Room 可以抽象化处理原始 SQL 表格和查询的一些底层实现细节。
- 它还允许您观察对数据库数据(包括集合和连接查询)的更改,并使用 LiveData 对象公开这类更改。
- 它甚至明确定义了解决一些常见线程问题(如访问主线程上的存储空间)的执行约束。
使用原始的SQLite可以提供这样的功能,但是有以下两个缺点:
- 没有编译时SQL语句的检查。尤其是当你的数据库表发生变化时,需要手动的更新相关代码,这会花费相当多的时间并且容易出错。
- 编写大量SQL语句和Java对象之间相互转化的代码。
针对以上的缺点,Google提供了Room来解决这些问题。Room包含以下三个重要组成部分:
- Database:数据库
- Entities:数据库中表对应的 Java 对象
- DAOs:操作数据库的 CRUD 方式
关于 Room,本文就不再展开了,有需要的直接看官方Demo!
网友评论