开局一张图,文章全靠编
谷歌在2018年I/O大会上推出了Android Jetpack
包含了许多实用的新组件,其中包含了之前推出的 Architecture Components
并加入了新的组件 比如: WorkManager
,Navigation
等。 最近就自己撸了一个聊天的demo,一起来学习一下吧。
效果图
整个项目的流程就是从固定写死的用户中选择一个登陆,然后与其他的用户进行对话。当然这里有很多的逻辑BUG,比如会出现自己与自己对话,多个用户同时登陆等等等。。 emmm,这个demo只是实现一个具体的场景,不对业务上的逻辑做过多讨论哈?
demo中用到的组件有:
Lifecycles
LiveData
ViewModel
Room
WorkManager (从入门到放弃)
融云IM SDK
引入组件
组件的引入请参考:官方文档?
另外 官方使用指南 也值得一读?本demo基本是按照这个指南来编写的。
Code
效果图看完了,我们来分解看一下,从第一个页面登陆页面说起吧
登陆页面看起来很简单,就是三条数据的展示,但其实一上来就在放大招了,基本上所有组件都用上了。先看代码:
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
supportActionBar?.title = "登陆"
val viewModel = ViewModelProviders.of(this)
.get(LoginViewModel::class.java)
val mAdapter = UserListAdapter(this)
login_list.adapter = mAdapter
login_list.itemAnimator = DefaultItemAnimator()
//订阅用户列表数据
viewModel.getUsers()?.observe(this, Observer {
// 为adapter设置新的值
it?.let {
mAdapter.setData(it)
}
})
mAdapter.setItemClick { _, position ->
val user = mAdapter.getData()?.get(position)
user?.let {
//进行连接登陆
AbstractIMClient.getInstance().connectServer(it.token)
val args = Bundle()
args.putParcelable("loginUser", it)
jumpActivity(MainActivity::class.java, args)
}
}
}
}
复制代码
上面的代码中,做的事情很简单:初始化RecycleView,设置点击事件,订阅ViewModel
返回的数据。在这里出现了第一个组件 ViewModel
:
val viewModel = ViewModelProviders.of(this)
.get(LoginViewModel::class.java)
复制代码
可以看到ViewModel
的获取方式是由系统ViewModelProviders
类获取的。那么ViewModel
到底是什么东西呢?在官方指南中有这样一段话:
A ViewModel provides the data for a specific UI component, such as a fragment or activity, and handles the communication with the business part of data handling, such as calling other components to load the data or forwarding user modifications. The ViewModel does not know about the View and is not affected by configuration changes such as recreating an activity due to rotation.
从中可以看出ViewModel
的作用与MVP架构中 P 层的作用是类似的,是view
与 数据 中间的桥梁,但与MVP不同的是,ViewModel
不持有 view
的引用,也不受到view
的生命周期影响,不去操作view
,只是为view
提供所需要的数据。那view
拿到数据后就可以进行渲染了,所以我们看到数据的订阅是view
自己去实现的:
viewModel.getUsers()?.observe(this, Observer {
// 为adapter设置新的值
it?.let {
mAdapter.setData(it)
}
})
复制代码
那在getUsers()
方法中返回值到底是什么呢?为什么可以去订阅呢?我们进到代码中一探究竟GO:
class LoginViewModel(application: Application) : AndroidViewModel(application) {
private var mObservableUsers: MediatorLiveData<MutableList<User>>? = null
init {
val userRepo = UserRepository(AppDataBase.getInstance(application.applicationContext).userDao())
mObservableUsers = MediatorLiveData()
mObservableUsers?.value = null
mObservableUsers?.addSource(userRepo.getAllUsers()) {
LogUtil.e("addSource ---> changed size = ${it?.size}")
mObservableUsers?.postValue(it)
}
}
fun getUsers(): LiveData<MutableList<User>>? {
return mObservableUsers
}
}
复制代码
我们自己的viewmodel
继承自 AndroidViewModel
。在getUsers
方法里,返回了一个LiveData
对象。这是出现的第二个组件,那它有什么作用呢?还是看官方指南的一段话:
LiveData is an observable data holder. It lets the components in your app observe LiveData objects for changes without creating explicit and rigid dependency paths between them. LiveData also respects the lifecycle state of your app components (activities, fragments, services) and does the right thing to prevent object leaking so that your app does not consume more memory.
这段话讲到LiveData
有三点特性:
- 持有数据
- 可观察(无强制依赖关系)
- 尊重生命周期
从代码看 这个LiveData
对象持有了我们真正的数据(用户列表),在view
层中通过订阅它,我们拿到了数据并填充到列表中。(代码中的 it
就是一个用户集合)
##LoginActivity
viewModel.getUsers()?.observe(this, Observer {
// 为adapter设置新的值
it?.let {
mAdapter.setData(it)
}
})
复制代码
每当数据更新的时候,都会回调到 onChanged
方法中。从而更新到UI,这跟我们用到的RxJava很相似,都是采用的订阅的模式。RxJava的优势在于它强大的 数据处理能力以及方便的线程切换等等,LiveData
的优势在于它是生命周期感知的,不用去处理view的生命周期。我们也没有做任何特殊处理配置更改(例如,用户旋转屏幕)。 ViewModel会在配置更改时自动恢复,因此只要新fragment/activity生效,它就会收到相同的ViewModel实例,并且会立即使用当前数据调用回调。 这就是ViewModels不应该直接引用Views的原因; 它们可以比View的生命周期更长久。
继续看官方指南的一段话:
A first idea for implementing the ViewModel might involve directly calling the Webservice to fetch the data and assign it back to the user object. Even though it works, your app becomes difficult to maintain as it grows. It gives too much responsibility to the ViewModel class which goes against the principle of separation of concerns that we've mentioned earlier. Additionally, the scope of a ViewModel is tied to an Activity or Fragment lifecycle, so losing all of the data when its lifecycle is finished is a bad user experience. Instead, our ViewModel will delegate this work to a new Repository module.
这里讲到,我们不应该直接在ViewModel
中去写有关获取数据的代码,数据应该由其他组件来提供,无论数据来自网络或者本地亦或者二中都有,这符合关注点分离原则。这和MVP架构是类似的,在P层中是从M层中拿到数据。而不是直接在P层中写获取数据的代码。
看这一段代码:
val userRepo = UserRepository(AppDataBase.getInstance(application.applicationContext).userDao())
mObservableUsers = MediatorLiveData()
mObservableUsers?.value = null
mObservableUsers?.addSource(userRepo.getAllUsers()) {
LogUtil.e("addSource ---> changed size = ${it?.size}")
mObservableUsers?.postValue(it)
}
复制代码
在上面的demo中,我们的数据由 UserRepository
提供,而它就是一个普通的类,相当于M层。 在进入到UserRepository
里之前,关于LiveData
还有一点:
LiveData
是一个抽象类,常使用的子类有:
MutableLiveData
就是我们常使用的普通的可变的LiveData
。
MediatorLiveData
则相当于一个中间者,他可以持有多个LiveData
的源数据,并在每一个源数据发生改变的时候回调自身的onChanged
方法将改变的数据传递出去。
现在进入到UserRepository
中去看一下数据的来源,以及怎么和LiveData
关联起来
class UserRepository(private val userDao: UserDao) {
fun getAllUsers(): LiveData<MutableList<User>> {
// refreshDb()
return userDao.getAll()
}
private fun refreshDb() {
val net = OneTimeWorkRequest.Builder(NetUsersWork::class.java).build()
val insert = OneTimeWorkRequest.Builder(InsertDbWork::class.java)
.build()
val delete = OneTimeWorkRequest.Builder(DeleteDbWork::class.java)
.build()
WorkManager.getInstance()
?.beginWith(net)
?.then(delete)
?.then(insert)
?.enqueue()
}
fun getUsersExceptId(id: Int): LiveData<MutableList<User>> {
return userDao.getUserListExcept(id)
}
}
复制代码
从代码中可以看到,getAllUsers
方法返回的其实是从数据库查询出来的用户集合。 refreshDb
方法是模拟从网络获取数据,并且更新到数据中。这里写的很糙,为了实现WorkManager
而写的。还是开头那句话。不对业务逻辑做过多讨论哈。
Room
是一个数据库的库,它的用法也很简单。
首先定义我们的实体类,通过 Entity 注解来标记这是要存到数据库中的表。
@Entity(tableName = "t_user", primaryKeys = ["id"])
data class User(var id: Int, var token: String, var name: String) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readInt(),
parcel.readString(),
parcel.readString())
override fun writeToParcel(dest: Parcel?, flags: Int) {
dest?.writeInt(id)
dest?.writeString(token)
dest?.writeString(name)
}
/..some code../
}
复制代码
其次,定义关于表操作的Dao
可以看到,关于Dao
中的操作,需要我们自己去写sql
语句。 并且查询结果可以使用LiveData
包裹。(Dao
的操作除了使用LiveData
包裹返回结果的,都不能在主线程中调用)
@Dao
interface UserDao {
@Query("select * from t_user")
fun getAll(): LiveData<MutableList<User>>
@Query("select * from t_user where id!=:loginId")
fun getUserListExcept(loginId: Int): LiveData<MutableList<User>>
@Delete
fun deleteUser(user: User)
@Update
fun updateUserList(users: List<User>)
/..some code../
}
复制代码
最后新建一个抽象类继承自RoomDatabase
,并定义好获取Dao
的抽象方法。最后通过Room.databaseBuilder(...).build()
获取
@Database(entities = [User::class], version = 1)
abstract class AppDataBase : RoomDatabase() {
abstract fun userDao(): UserDao
/..some code../
fun getInstance(context: Context): AppDataBase {
if (null == mInstance)
mInstance = Room.databaseBuilder(context, AppDataBase::class.java, DATABASE_NAME)
.build()
return mInstance!!
}
}
复制代码
稍微吐槽一下Room
运行的速度还是比较慢的,不知道是不是我模拟器问题,同等数据量使用GreenDao
速度要快一些。接下来看一下WorkManager
// ...and the result key:
const val NET_KEY_RESULT = "userList"
class NetUsersWork : Worker() {
override fun doWork(): Result {
val userList: MutableList<User> = mutableListOf()
val userName = arrayOf("Colin", "猪八戒", "孙悟空")
val token = arrayOf("fGTKLAzQF+8jGSk1nyAvnRQAiJI+bgFMB9ytQGNk5tVr83vzvDJlHwfQGpRzT4dvHnvp9wHIGheSmaswU4zC9g==",
"Gg3lim3AVbXVGL/7vW7i3xQAiJI+bgFMB9ytQGNk5tVr83vzvDJlH6FWR2lnHaHYzxsRr4O1/4GSmaswU4zC9g==",
"q6GNW12hGtJ7Z08sy0RsDoAmeyaZEnNAMcO9WK9bkc3wT/jlf9s8fIQc4If54MDyVbiYRm+jOA4YydwsOTN2TQ==")
for (i in 0 until userName.size) {
userList.add(User((i + 1), token[i], userName[i]))
}
//模拟网络请求
Thread.sleep(1500)
outputData = Data.Builder().putString(NET_KEY_RESULT, Gson().toJson(userList)).build()
LogUtil.e("网络请求完成----")
return Result.SUCCESS
}
}
复制代码
从代码来看,WorkManager
使用还是很简单的,继承Worker
类,在doWork
中进行耗时操作,如果有返回参数则通过setOutputData(Data data)
方法传递出去。最后返回值代表work的状态。跟AsyncTask
很相似,另外可以通过WorkManager
自定义任务的顺序、时机等。
WorkManager.getInstance()
?.beginWith(net)
?.then(delete)
?.then(insert)
?.enqueue()
复制代码
但是要吐槽的一点,Data
类能够放的参数只有基本数据类型,并且不支持Parcelable
。瞬间感觉有点鸡肋了。另外如果在doWork
方法中,有通过从其他线程回调得到的数据,如果不自己做处理(比如阻塞等待)就会顺序执行到return Result.SUCCESS
从而结束当前work
。我自己觉得还不如用RxJava
来做这些工作。
到这一步,整个架构已经清晰起来了,看图(官方的图):
整个架构还是很清楚的,并且也充分的解耦,最重要的一点是不用处理蛋疼的生命周期,因为系统已经帮我们处理好了~背后的工作者就是Lifecycle
了。
关于Llifecycle
具体请参看官方文档?
诶诶。。好像还是没看到LiveData
是如何拿到具体的值啊,前面那不是Romm
自动生成的嘛。你这不是在
嘿嘿。。不要着急,我们现在看一下聊天页面
class ChatActivity : AppCompatActivity() {
/..some code../
//发送消息,进行检查
private fun sendMessage() {
/。。some code。。/
//重点在这里
viewModel.sendTextMessage(message, targetUser.id.toString())
.observe(this, Observer {
it?.let {
chat.content.sentStatus = it.content.sentStatus
mAdapter.notifyItemChanged(position)
scrollBottom()
}
})
}
/..some code../
}
复制代码
class ChatViewModel(application: Application) : AndroidViewModel(application) {
private val sendObservable = MediatorLiveData<ChatEntity>()
fun sendTextMessage(text: String, targetId: String): LiveData<ChatEntity> {
sendObservable.addSource(AbstractIMClient.getInstance().sendTextMessage(text, targetId)) {
sendObservable.postValue(it)
}
return sendObservable
}
/。。some code。。/
}
复制代码
class RongClient : AbstractIMClient() {
override fun sendTextMessage(text: String, targetId: String): LiveData<ChatEntity> {
//构造 文本消息实体
val tmsg = TextMessage.obtain(text)
//构造 消息实体类
val msg = Message.obtain(targetId, Conversation.ConversationType.PRIVATE, tmsg)
val liveData: MutableLiveData<ChatEntity> = MutableLiveData()
RongIMClient.getInstance().sendMessage(msg, null, null, object : IRongCallback.ISendMessageCallback {
override fun onAttached(message: Message) {
val chat = ChatEntity(TYPE_SELF, message)
liveData.value = chat
}
override fun onSuccess(message: Message) {
val chat = ChatEntity(TYPE_SELF, message)
liveData.value = chat
}
override fun onError(message: Message, errorCode: RongIMClient.ErrorCode?) {
val chat = ChatEntity(TYPE_SELF, message)
liveData.value = chat
}
})
return liveData
}
}
复制代码
上面的代码片段也很清晰,ViewModel
调用AbstractIMClient
的方法发送消息,并添加到sendObservable
这个中间者里。并返回它,从RongClient
的代码可以看到,我们先创建一个MutableLiveData
对象实例。然后通过 融云SDK发送消息,在回调中 设置MutableLiveData
的 value
对象,最后返回MutableLiveData
对象出去。这样当消息开始发送,到失败或者成功都会改变vale
的值,从而外面也会回调onChanged()
方法对应的次数,改变消息的状态,达到刷新UI的效果。使用起来也是方便的不行。
还有一点是 可以看到融云SDK发送消息的结果是通过 接口回调出来的,按照以前的写法,在RongClient
这一层要想把结果传出去,还得从外面传进来回调接口类,有了LiveData
则方便很多了。
另外关于 kotlin真是越用越喜欢,哈哈哈。推荐Kotlin官方博客合集 里面很多kotlin的小Tips。
结束
整体看下来,Architecture Components 有助于我们构建一个稳定的符合 关注点分离的应用。 使用起来也很简单方便。整个demo下来,在具体的场景中完整的学一遍,很容易掌握。由于没有更新 studio 金丝雀 版,还没有去使用Navigation
组件。有时间去看一看,完整代码已上传到我的GitHub