LiveData 在 NetworkBoundResource 中的巧妙应用

1,181 阅读3分钟

场景:通过网络请求返回的数据来更新View

先抛出一个很常见的问题:如何通过网络请求的数据来更新View?这个问题的解决方案是很明确的:

  1. 在本地缓存/数据库,中查找是否有缓存,如果有缓存直接更新View。
  2. 如果没有找到缓存,发起网络请求。
  3. 网络请求返回结果后,更新缓存,更新View。

1.jpg

NetworkBoundResource 是什么

NetworkBoundResource是一种Android Jetpack架构组件中的设计模式,完成的就是上述的数据请过过程。由于View的刷新依赖于网络请求的返回的结果,而网络请求又需要一定的时间,整个过程是一个同步操作,代码中往往也需要传递callback,来完成View的更新。NetworkBoundResource 通过引入LiveData,把数据的请求变成了一个异步的操作,View的更新通过LiveData 的 observe 来完成。

2.jpg 上图中可以看到,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方法来获取用户数据。