场景:通过网络请求返回的数据来更新View
先抛出一个很常见的问题:如何通过网络请求的数据来更新View?这个问题的解决方案是很明确的:
- 在本地缓存/数据库,中查找是否有缓存,如果有缓存直接更新View。
- 如果没有找到缓存,发起网络请求。
- 网络请求返回结果后,更新缓存,更新View。
NetworkBoundResource 是什么
NetworkBoundResource是一种Android Jetpack架构组件中的设计模式,完成的就是上述的数据请过过程。由于View的刷新依赖于网络请求的返回的结果,而网络请求又需要一定的时间,整个过程是一个同步操作,代码中往往也需要传递callback,来完成View的更新。NetworkBoundResource 通过引入LiveData,把数据的请求变成了一个异步的操作,View的更新通过LiveData 的 observe 来完成。
上图中可以看到,Repository 向外抛出去了一个LiveData,等到拿到了数据后,直接更新LiveData。外部只需要监听LiveData,就可以在数据更新时候来更新View.
NetworkBoundResource 源码如下:
package com.android.example.github.repository
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import com.android.example.github.AppExecutors
import com.android.example.github.api.ApiEmptyResponse
import com.android.example.github.api.ApiErrorResponse
import com.android.example.github.api.ApiResponse
import com.android.example.github.api.ApiSuccessResponse
import com.android.example.github.vo.Resource
abstract class NetworkBoundResource<ResultType, RequestType>
@MainThread constructor(private val appExecutors: AppExecutors) {
private val result = MediatorLiveData<Resource<ResultType>>()
init {
result.value = Resource.loading(null)
@Suppress("LeakingThis")
val dbSource = loadFromDb()
result.addSource(dbSource) { data ->
result.removeSource(dbSource)
if (shouldFetch(data)) {
fetchFromNetwork(dbSource)
} else {
result.addSource(dbSource) { newData ->
setValue(Resource.success(newData))
}
}
}
}
@MainThread
private fun setValue(newValue: Resource<ResultType>) {
if (result.value != newValue) {
result.value = newValue
}
}
private fun fetchFromNetwork(dbSource: LiveData<ResultType>) {
val apiResponse = createCall()
// we re-attach dbSource as a new source, it will dispatch its latest value quickly
result.addSource(dbSource) { newData ->
setValue(Resource.loading(newData))
}
result.addSource(apiResponse) { response ->
result.removeSource(apiResponse)
result.removeSource(dbSource)
when (response) {
is ApiSuccessResponse -> {
appExecutors.diskIO().execute {
saveCallResult(processResponse(response))
appExecutors.mainThread().execute {
// we specially request a new live data,
// otherwise we will get immediately last cached value,
// which may not be updated with latest results received from network.
result.addSource(loadFromDb()) { newData ->
setValue(Resource.success(newData))
}
}
}
}
is ApiEmptyResponse -> {
appExecutors.mainThread().execute {
// reload from disk whatever we had
result.addSource(loadFromDb()) { newData ->
setValue(Resource.success(newData))
}
}
}
is ApiErrorResponse -> {
onFetchFailed()
result.addSource(dbSource) { newData ->
setValue(Resource.error(response.errorMessage, newData))
}
}
}
}
}
protected open fun onFetchFailed() {}
fun asLiveData() = result as LiveData<Resource<ResultType>>
@WorkerThread
protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body
@WorkerThread
protected abstract fun saveCallResult(item: RequestType)
@MainThread
protected abstract fun shouldFetch(data: ResultType?): Boolean
@MainThread
protected abstract fun loadFromDb(): LiveData<ResultType>
@MainThread
protected abstract fun createCall(): LiveData<ApiResponse<RequestType>>
}
以下是一个使用NetworkBoundResource模式的代码示例,假设我们有一个数据模型User:
data class User(val id: Int, val name: String, val email: String)
我们将使用Room作为本地数据库框架,Retrofit作为远程数据源框架,以及ViewModel和LiveData作为呈现数据的组件。 首先,我们需要创建一个包含本地缓存和远程数据源交互逻辑的仓库类:
class UserRepository(private val userDao: UserDao, private val userService: UserService) {
fun getUser(id: Int): LiveData<Resource<User>> {
return object : NetworkBoundResource<User, User>() {
override fun loadFromDb(): LiveData<User> {
return userDao.getUserById(id)
}
override fun shouldFetch(data: User?): Boolean {
return data == null
}
override fun saveCallResult(item: User) {
userDao.insertUser(item)
}
override fun createCall(): LiveData<ApiResponse<User>> {
return userService.getUser(id)
}
}.asLiveData()
}
}
在上面的示例中,我们创建了一个名为getUser的方法,该方法返回LiveData<Resource>类型。在该方法中,我们创建了一个NetworkBoundResource对象,并重写了它的四个方法:loadFromDb、shouldFetch、saveCallResult和createCall。
- loadFromDb:从本地缓存中加载数据。
- shouldFetch:决定是否需要从远程数据源获取数据。
- saveCallResult:将从远程数据源获取的数据存储到本地缓存中。
- createCall:创建一个Retrofit的LiveData对象,用于从远程数据源获取数据。
接下来,我们需要定义一个ViewModel类,用于将数据呈现给UI:
class UserViewModel(private val userRepository: UserRepository) : ViewModel() {
private val _user = MutableLiveData<Resource<User>>()
val user: LiveData<Resource<User>>
get() = _user
fun getUser(id: Int) {
_user.value = Resource.loading(null)
userRepository.getUser(id).observeForever { result ->
_user.value = result
}
}
}
在上面的示例中,我们定义了一个getUser方法,该方法通过调用UserRepository的getUser方法来获取用户数据,并使用LiveData将数据呈现给UI。 最后,我们需要在UI层(如Activity或Fragment)中观察UserViewModel的user属性,以获取用户数据并更新UI:
class UserActivity : AppCompatActivity() {
private val viewModel by viewModels<UserViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
viewModel.user.observe(this, { result ->
when (result.status) {
Status.SUCCESS -> {
val user = result.data
// 更新UI
}
Status.ERROR -> {
val message = result.message ?: getString(R.string.unknown_error)
// 显示错误信息
}
Status.LOADING -> {
// 显示加载中状态
}
}
})
viewModel.getUser(1)
}
}
在上面的示例中,我们使用observe方法观察UserViewModel的user属性,并根据不同的状态更新UI。在onCreate方法中,我们调用getUser方法来获取用户数据。