如何在 Android 中使用 NetworkBoundResource 实现离线缓存?
几乎,每个需要通过网络获取数据的 android 应用程序都需要缓存。首先,让我们了解缓存是什么意思?我们大多数人都使用过需要从 Web 获取数据的应用程序。这种具有离线优先架构的应用程序将始终尝试从本地存储中获取数据。另一方面,如果出现故障,它会请求从网络获取数据,然后将其存储在本地,以备将来检索。数据将存储在 SQLite 数据库中。这种架构的优势在于,即使应用程序处于离线状态,我们也可以使用它。此外,由于数据被缓存,应用程序将响应更快。为了处理缓存,我们将使用NetworkBound Resource。它是一个辅助类,用于决定何时使用缓存数据以及何时从 Web 获取数据并更新视图。它在两者之间进行协调。
上面的决策树显示了 NetworkBound Resource 算法的算法。
算法
让我们看看这个算法的流程:
- 每当用户以离线模式访问应用程序时,数据都会被分派到视图中,它可以是片段或活动。
- 如果磁盘中没有数据或数据不足作为缓存,则应通过网络获取数据。
- 它检查是否需要登录(如果用户注销,则需要重新登录)。它重新进行身份验证,如果成功则获取数据,但失败了,则提示用户重新进行身份验证。
- 一旦凭据匹配,它就会通过网络获取数据。
- 如果获取阶段失败,则会提示用户。
- 否则,如果成功,则数据自动存储到本地存储中。然后刷新视图。
这里的要求是,当用户进入在线模式时,用户体验的变化应该很小。所以重新认证、通过网络获取数据和刷新视图等过程应该在后台完成。这里要注意的一件事是,如果用户凭据(例如密码或用户名)发生了一些更改,则用户只需要重新登录。
执行
为了更多地了解这一点,让我们构建一个应用程序。这是一个简单的新闻应用程序,它使用伪造的 API 从网络获取数据。让我们看看我们的应用程序的高级设计:
- 它将使用 MVVM 架构。
- 用于缓存数据的 SQLite 数据库。
- 使用 Kotlin Flow。(Kotlin 协程)
- 用于依赖注入的 Dagger Hilt。
上图是将在我们的应用程序中实现的架构的概述。 Android 推荐使用这种架构来开发现代架构良好的 android 应用程序。让我们开始构建项目。
分步实施
第 1 步:创建一个新项目
要在 Android Studio 中创建新项目,请参阅如何在 Android Studio 中创建/启动新项目。请注意,选择Kotlin作为编程语言。
第 2 步:设置布局
始终建议先设置布局,然后再实现逻辑。所以我们将首先创建布局。如前所述,我们将从 Web 服务中获取数据。由于这是一个示例项目,我们只需从随机数据生成器中获取数据。现在数据是汽车列表,其中将包括以下属性:
- 汽车的品牌和型号
- 汽车的传输
- 汽车颜色
- 汽车的驱动类型。
- 汽车的燃料类型。
- 汽车的车型。
我们将使用 RecyclerView 来显示列表。因此,首先需要设计列表中每个元素的外观。其次是制作清单。
XML
XML
Kotlin
package com.gfg.carlist.api
import com.gfg.carlist.data.CarList
import retrofit2.http.GET
interface CarListAPI {
// Companion object to hold the base URL
companion object{
const val BASE_URL = "https://random-data-api.com/api/"
}
// The number of cars can be varied using the size.
// By default it is kept at 20, but can be tweaked.
// @GET annotation to make a GET request.
@GET("vehicle/random_vehicle?size=20")
// Store the data in a list.
suspend fun getCarList() : List
}
Kotlin
package com.gfg.carlist.di
import android.app.Application
import androidx.room.Room
import com.gfg.carlist.api.CarListAPI
import com.gfg.carlist.data.CarListDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit =
Retrofit.Builder()
.baseUrl(CarListAPI.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
@Singleton
fun provideCarListAPI(retrofit: Retrofit): CarListAPI =
retrofit.create(CarListAPI::class.java)
@Provides
@Singleton
fun provideDatabase(app: Application): CarListDatabase =
Room.databaseBuilder(app, CarListDatabase::class.java, "carlist_database")
.build()
}
Kotlin
package com.gfg.carlist.data
import androidx.room.Entity
import androidx.room.PrimaryKey
// Data Class to store the data
// Here the name of the table is "cars"
@Entity(tableName = "cars")
data class CarList(
@PrimaryKey val make_and_model: String,
val color: String,
val transmission: String,
val drive_type: String,
val fuel_type: String,
val car_type: String
)
Kotlin
package com.gfg.carlist.data
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [CarList::class], version = 1)
abstract class CarListDatabase : RoomDatabase() {
abstract fun carsDao(): CarsDao
}
Kotlin
package com.gfg.carlist.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface CarsDao {
// Query to fetch all the data from the
// SQLite database
// No need of suspend method here
@Query("SELECT * FROM cars")
// Kotlin flow is an asynchronous stream of values
fun getAllCars(): Flow>
// If a new data is inserted with same primary key
// It will get replaced by the previous one
// This ensures that there is always a latest
// data in the database
@Insert(onConflict = OnConflictStrategy.REPLACE)
// The fetching of data should NOT be done on the
// Main thread. Hence coroutine is used
// If it is executing on one one thread, it may suspend
// its execution there, and resume in another one
suspend fun insertCars(cars: List)
// Once the device comes online, the cached data
// need to be replaced, i.e. delete it
// Again it will use coroutine to achieve this task
@Query("DELETE FROM cars")
suspend fun deleteAllCars()
}
Kotlin
package com.gfg.carlist.data
import androidx.room.withTransaction
import com.gfg.carlist.api.CarListAPI
import com.gfg.carlist.util.networkBoundResource
import kotlinx.coroutines.delay
import javax.inject.Inject
class CarListRepository @Inject constructor(
private val api: CarListAPI,
private val db: CarListDatabase
) {
private val carsDao = db.carsDao()
fun getCars() = networkBoundResource(
// Query to return the list of all cars
query = {
carsDao.getAllCars()
},
// Just for testing purpose,
// a delay of 2 second is set.
fetch = {
delay(2000)
api.getCarList()
},
// Save the results in the table.
// If data exists, then delete it
// and then store.
saveFetchResult = { CarList ->
db.withTransaction {
carsDao.deleteAllCars()
carsDao.insertCars(CarList)
}
}
)
}
Kotlin
package com.gfg.carlist.features.carlist
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.gfg.carlist.data.CarList
import com.gfg.carlist.databinding.CarlistItemBinding
class CarAdapter : ListAdapter(CarListComparator()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CarViewHolder {
val binding =
CarlistItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return CarViewHolder(binding)
}
override fun onBindViewHolder(holder: CarViewHolder, position: Int) {
val currentItem = getItem(position)
if (currentItem != null) {
holder.bind(currentItem)
}
}
// View Holder class to hold the view
class CarViewHolder(private val binding: CarlistItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(carlist: CarList) {
binding.apply {
carName.text = carlist.make_and_model
carTransmission.text = carlist.transmission
carColor.text = carlist.color
carDriveType.text = carlist.drive_type
carFuelType.text = carlist.fuel_type
carCarType.text = carlist.car_type
}
}
}
// Comparator class to check for the changes made.
// If there are no changes then no need to do anything.
class CarListComparator : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: CarList, newItem: CarList) =
oldItem.make_and_model == newItem.make_and_model
override fun areContentsTheSame(oldItem: CarList, newItem: CarList) =
oldItem == newItem
}
}
Kotlin
package com.gfg.carlist.features.carlist
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import com.gfg.carlist.data.CarListRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
// Using Dagger Hilt library to
// inject the data into the view model
@HiltViewModel
class CarListViewModel @Inject constructor(
repository: CarListRepository
) : ViewModel() {
val cars = repository.getCars().asLiveData()
}
Kotlin
package com.gfg.carlist.features.carlist
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.gfg.carlist.databinding.ActivityCarBinding
import com.gfg.carlist.util.Resource
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class CarActivity : AppCompatActivity() {
// Helps to preserve the view
// If the app is closed, then after
// reopening it the app will open
// in a state in which it was closed
// DaggerHilt will inject the view-model for us
private val viewModel: CarListViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// The bellow segment would
// instantiate the activity_car layout
// and will create a property for different
// views inside it!
val binding = ActivityCarBinding.inflate(layoutInflater)
setContentView(binding.root)
val carAdapter = CarAdapter()
binding.apply {
recyclerViewer.apply {
adapter = carAdapter
layoutManager = LinearLayoutManager(this@CarActivity)
}
viewModel.cars.observe(this@CarActivity) { result ->
carAdapter.submitList(result.data)
progressBar.isVisible = result is Resource.Loading<*> && result.data.isNullOrEmpty()
textViewError.isVisible = result is Resource.Error<*> && result.data.isNullOrEmpty()
textViewError.text = result.error?.localizedMessage
}
}
}
}
现在,让我们对列表布局进行编码:
XML
第 3 步:现在让我们创建 API 包
CarListAPI.kt
科特林
package com.gfg.carlist.api
import com.gfg.carlist.data.CarList
import retrofit2.http.GET
interface CarListAPI {
// Companion object to hold the base URL
companion object{
const val BASE_URL = "https://random-data-api.com/api/"
}
// The number of cars can be varied using the size.
// By default it is kept at 20, but can be tweaked.
// @GET annotation to make a GET request.
@GET("vehicle/random_vehicle?size=20")
// Store the data in a list.
suspend fun getCarList() : List
}
第 4 步:实现应用模块
模块只不过是一个对象类,它为应用程序的源代码提供了一个容器。它封装了与任务关联的数据模型。 android 架构建议在视图模型中尽量少使用业务逻辑,因此业务应用程序任务在 app 模块中表示。它将包括三种方法:
- 一种通过 Retrofit 调用 API 的方法
- 提供列表的方法
- 一种提供数据库或构建数据库的方法。
应用模块.kt
科特林
package com.gfg.carlist.di
import android.app.Application
import androidx.room.Room
import com.gfg.carlist.api.CarListAPI
import com.gfg.carlist.data.CarListDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit =
Retrofit.Builder()
.baseUrl(CarListAPI.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
@Singleton
fun provideCarListAPI(retrofit: Retrofit): CarListAPI =
retrofit.create(CarListAPI::class.java)
@Provides
@Singleton
fun provideDatabase(app: Application): CarListDatabase =
Room.databaseBuilder(app, CarListDatabase::class.java, "carlist_database")
.build()
}
步骤 5:创建数据类
我们已经完成了 API 的处理,从 Web 服务中获取数据,但是在哪里存储数据呢?让我们创建一个类来存储数据。我们必须创建一个数据类。如果应用程序只是获取和公开数据,那么它将只有一个数据类文件。但是在这里,我们必须获取、公开和缓存数据。因此,房间在这里发挥作用。所以在数据类中,我们必须创建一个实体。
CarList.kt
科特林
package com.gfg.carlist.data
import androidx.room.Entity
import androidx.room.PrimaryKey
// Data Class to store the data
// Here the name of the table is "cars"
@Entity(tableName = "cars")
data class CarList(
@PrimaryKey val make_and_model: String,
val color: String,
val transmission: String,
val drive_type: String,
val fuel_type: String,
val car_type: String
)
由于我们将在本地缓存数据,因此需要创建一个数据库。
汽车清单数据库.kt
科特林
package com.gfg.carlist.data
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [CarList::class], version = 1)
abstract class CarListDatabase : RoomDatabase() {
abstract fun carsDao(): CarsDao
}
由于我们已经创建了一个表,我们需要一些查询来从表中检索数据。这是通过使用DAO或d ATA甲CCESSöbject实现。
CarsDao.kt
科特林
package com.gfg.carlist.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface CarsDao {
// Query to fetch all the data from the
// SQLite database
// No need of suspend method here
@Query("SELECT * FROM cars")
// Kotlin flow is an asynchronous stream of values
fun getAllCars(): Flow>
// If a new data is inserted with same primary key
// It will get replaced by the previous one
// This ensures that there is always a latest
// data in the database
@Insert(onConflict = OnConflictStrategy.REPLACE)
// The fetching of data should NOT be done on the
// Main thread. Hence coroutine is used
// If it is executing on one one thread, it may suspend
// its execution there, and resume in another one
suspend fun insertCars(cars: List)
// Once the device comes online, the cached data
// need to be replaced, i.e. delete it
// Again it will use coroutine to achieve this task
@Query("DELETE FROM cars")
suspend fun deleteAllCars()
}
用于处理来自 Web 服务的数据和本地数据的存储库类。
CarListRepository.kt
科特林
package com.gfg.carlist.data
import androidx.room.withTransaction
import com.gfg.carlist.api.CarListAPI
import com.gfg.carlist.util.networkBoundResource
import kotlinx.coroutines.delay
import javax.inject.Inject
class CarListRepository @Inject constructor(
private val api: CarListAPI,
private val db: CarListDatabase
) {
private val carsDao = db.carsDao()
fun getCars() = networkBoundResource(
// Query to return the list of all cars
query = {
carsDao.getAllCars()
},
// Just for testing purpose,
// a delay of 2 second is set.
fetch = {
delay(2000)
api.getCarList()
},
// Save the results in the table.
// If data exists, then delete it
// and then store.
saveFetchResult = { CarList ->
db.withTransaction {
carsDao.deleteAllCars()
carsDao.insertCars(CarList)
}
}
)
}
第 6 步:处理 UI
记得在第 1 步中,我们创建了一个 RecyclerView 来公开汽车列表。但这项工作直到现在还没有完成。我们需要制作一个适配器和一个 ViewModel。这两个类一起工作来定义我们的数据如何显示。
车载适配器.kt
科特林
package com.gfg.carlist.features.carlist
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.gfg.carlist.data.CarList
import com.gfg.carlist.databinding.CarlistItemBinding
class CarAdapter : ListAdapter(CarListComparator()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CarViewHolder {
val binding =
CarlistItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return CarViewHolder(binding)
}
override fun onBindViewHolder(holder: CarViewHolder, position: Int) {
val currentItem = getItem(position)
if (currentItem != null) {
holder.bind(currentItem)
}
}
// View Holder class to hold the view
class CarViewHolder(private val binding: CarlistItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(carlist: CarList) {
binding.apply {
carName.text = carlist.make_and_model
carTransmission.text = carlist.transmission
carColor.text = carlist.color
carDriveType.text = carlist.drive_type
carFuelType.text = carlist.fuel_type
carCarType.text = carlist.car_type
}
}
}
// Comparator class to check for the changes made.
// If there are no changes then no need to do anything.
class CarListComparator : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: CarList, newItem: CarList) =
oldItem.make_and_model == newItem.make_and_model
override fun areContentsTheSame(oldItem: CarList, newItem: CarList) =
oldItem == newItem
}
}
CarListViewModel.kt
科特林
package com.gfg.carlist.features.carlist
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import com.gfg.carlist.data.CarListRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
// Using Dagger Hilt library to
// inject the data into the view model
@HiltViewModel
class CarListViewModel @Inject constructor(
repository: CarListRepository
) : ViewModel() {
val cars = repository.getCars().asLiveData()
}
最后,我们必须创建一个活动来显示来自 ViewModel 的数据。请记住,所有业务逻辑都应该存在于 ViewModel 中,而不是存在于 Activity 中。活动也不应该保存数据,因为当屏幕倾斜时,数据会被破坏,因此加载时间会增加。因此,活动的目的是仅显示数据。
CarActivity.kt
科特林
package com.gfg.carlist.features.carlist
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.gfg.carlist.databinding.ActivityCarBinding
import com.gfg.carlist.util.Resource
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class CarActivity : AppCompatActivity() {
// Helps to preserve the view
// If the app is closed, then after
// reopening it the app will open
// in a state in which it was closed
// DaggerHilt will inject the view-model for us
private val viewModel: CarListViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// The bellow segment would
// instantiate the activity_car layout
// and will create a property for different
// views inside it!
val binding = ActivityCarBinding.inflate(layoutInflater)
setContentView(binding.root)
val carAdapter = CarAdapter()
binding.apply {
recyclerViewer.apply {
adapter = carAdapter
layoutManager = LinearLayoutManager(this@CarActivity)
}
viewModel.cars.observe(this@CarActivity) { result ->
carAdapter.submitList(result.data)
progressBar.isVisible = result is Resource.Loading<*> && result.data.isNullOrEmpty()
textViewError.isVisible = result is Resource.Error<*> && result.data.isNullOrEmpty()
textViewError.text = result.error?.localizedMessage
}
}
}
}
最后,我们完成了编码部分。成功构建项目后,应用程序将如下所示:
输出:
以下视频演示了该应用程序。
输出说明:
项目链接:点此