本章深入 Android 数据层全栈:OkHttp 拦截器链原理、Retrofit 协程集成与类型安全、Room 数据库高级特性、DataStore 替代 SharedPreferences、Paging 3 分页加载与离线优先架构。
📋 章节目录
| 节 | 主题 |
|---|---|
| 5.1 | OkHttp 原理与拦截器链 |
| 5.2 | Retrofit 进阶 |
| 5.3 | Room 数据库 |
| 5.4 | DataStore |
| 5.5 | Paging 3 分页加载 |
| 5.6 | 离线优先架构 |
5.1 OkHttp 原理与拦截器链
拦截器链架构
Request
↓
RetryAndFollowUpInterceptor(重试/重定向)
↓
BridgeInterceptor(添加 Headers:Content-Type / Cookie / gzip)
↓
CacheInterceptor(HTTP 缓存处理)
↓
ConnectInterceptor(建立连接)
↓
[自定义 NetworkInterceptor]
↓
CallServerInterceptor(真正发送请求 & 读取响应)
↓
Response
自定义拦截器
kotlin
// 1. 认证拦截器(自动添加 Token + 无感刷新)
class AuthInterceptor(
private val tokenRepository: TokenRepository
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenRepository.getAccessToken()
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.addHeader("Accept", "application/json")
.build()
val response = chain.proceed(request)
// Token 过期自动刷新(无感刷新)
if (response.code == 401) {
response.close()
return try {
val newToken = tokenRepository.refreshTokenSync()
val newRequest = request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
chain.proceed(newRequest)
} catch (e: Exception) {
// 刷新失败,跳转登录
tokenRepository.clearTokens()
response
}
}
return response
}
}
// 2. 日志拦截器(完整请求/响应日志)
class LoggingInterceptor(
private val logger: Logger = Logger.DEFAULT
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val startTime = System.nanoTime()
logger.log("→ ${request.method} ${request.url}")
request.body?.let { body ->
val buffer = Buffer()
body.writeTo(buffer)
logger.log(" Body: ${buffer.readUtf8()}")
}
val response = chain.proceed(request)
val duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)
logger.log("← ${response.code} ${request.url} (${duration}ms)")
return response
}
}
// 3. 缓存拦截器(离线缓存策略)
class CacheControlInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
// 对 GET 请求设置缓存
return if (request.method == "GET") {
response.newBuilder()
.header("Cache-Control", "public, max-age=60") // 缓存 60 秒
.build()
} else response
}
}
// 4. 重试拦截器
class RetryInterceptor(private val maxRetries: Int = 3) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var response: Response? = null
var exception: IOException? = null
repeat(maxRetries) { attempt ->
try {
response?.close()
response = chain.proceed(request)
if (response!!.isSuccessful) return response!!
} catch (e: IOException) {
exception = e
if (attempt < maxRetries - 1) {
Thread.sleep(1000L * (attempt + 1)) // 指数退避
}
}
}
return response ?: throw exception!!
}
}
// OkHttpClient 配置
fun provideOkHttpClient(
tokenRepository: TokenRepository,
cache: Cache
): OkHttpClient {
return OkHttpClient.Builder()
.cache(cache)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor(AuthInterceptor(tokenRepository))
.addInterceptor(LoggingInterceptor())
.addNetworkInterceptor(CacheControlInterceptor())
.addInterceptor(RetryInterceptor(maxRetries = 3))
// 证书锁定
.certificatePinner(
CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build()
)
.build()
}
5.2 Retrofit 进阶
API 接口定义
kotlin
interface ProductApi {
// 基本 GET
@GET("products")
suspend fun getProducts(
@Query("page") page: Int = 1,
@Query("limit") limit: Int = 20,
@Query("category") category: String? = null
): ApiResponse<List<ProductDto>>
// 路径参数
@GET("products/{id}")
suspend fun getProductById(@Path("id") id: Int): ApiResponse<ProductDto>
// POST with Body
@POST("orders")
suspend fun createOrder(@Body orderRequest: CreateOrderRequest): ApiResponse<OrderDto>
// 文件上传(Multipart)
@Multipart
@POST("upload/image")
suspend fun uploadImage(
@Part("description") description: RequestBody,
@Part image: MultipartBody.Part
): ApiResponse<UploadResult>
// 动态 URL
@GET
suspend fun downloadFile(@Url url: String): ResponseBody
// 带完整 Response(需要处理 Headers)
@GET("products")
suspend fun getProductsWithHeaders(): Response<ApiResponse<List<ProductDto>>>
}
// 统一响应包装
data class ApiResponse<T>(
@SerializedName("code") val code: Int,
@SerializedName("message") val message: String,
@SerializedName("data") val data: T?,
@SerializedName("total") val total: Int? = null
) {
val isSuccess: Boolean get() = code == 200
}
// 网络异常体系
sealed class NetworkException(message: String) : Exception(message) {
class HttpException(val code: Int, message: String) : NetworkException(message)
class TimeoutException : NetworkException("请求超时,请检查网络")
class NoNetworkException : NetworkException("网络不可用,请检查网络连接")
class ServerException(message: String) : NetworkException(message)
class ParseException(message: String) : NetworkException("数据解析失败: $message")
}
统一错误处理
kotlin
// 扩展函数:安全执行请求
suspend fun <T> safeApiCall(block: suspend () -> ApiResponse<T>): Result<T> {
return try {
val response = block()
if (response.isSuccess && response.data != null) {
Result.success(response.data)
} else {
Result.failure(NetworkException.ServerException(response.message))
}
} catch (e: retrofit2.HttpException) {
val errorBody = e.response()?.errorBody()?.string()
Result.failure(NetworkException.HttpException(e.code(), errorBody ?: e.message()))
} catch (e: java.net.SocketTimeoutException) {
Result.failure(NetworkException.TimeoutException())
} catch (e: java.io.IOException) {
Result.failure(NetworkException.NoNetworkException())
} catch (e: Exception) {
Result.failure(NetworkException.ParseException(e.message ?: ""))
}
}
// 在 Repository 中使用
class ProductRepositoryImpl @Inject constructor(
private val api: ProductApi,
private val dao: ProductDao
) : ProductRepository {
override suspend fun getProducts(page: Int): Result<List<Product>> {
return safeApiCall {
api.getProducts(page = page)
}.map { dtos ->
dtos.map { it.toDomain() }
}
}
}
// Retrofit 实例配置(Hilt Module)
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideCache(@ApplicationContext context: Context): Cache {
val cacheDir = File(context.cacheDir, "http_cache")
return Cache(cacheDir, 50 * 1024 * 1024) // 50MB
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideProductApi(retrofit: Retrofit): ProductApi =
retrofit.create(ProductApi::class.java)
}
5.3 Room 数据库
Entity / DAO / Database 定义
kotlin
// Entity 定义
@Entity(
tableName = "products",
indices = [
Index(value = ["category_id"]),
Index(value = ["name"]) // 加速搜索
]
)
data class ProductEntity(
@PrimaryKey val id: Int,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "price") val price: Double,
@ColumnInfo(name = "description") val description: String,
@ColumnInfo(name = "image_url") val imageUrl: String,
@ColumnInfo(name = "category_id") val categoryId: Int,
@ColumnInfo(name = "is_favorite") val isFavorite: Boolean = false,
@ColumnInfo(name = "updated_at") val updatedAt: Long = System.currentTimeMillis()
)
// 关系型 Entity
@Entity(tableName = "orders")
data class OrderEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "user_id") val userId: Int,
@ColumnInfo(name = "total_amount") val totalAmount: Double,
@ColumnInfo(name = "status") val status: String,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)
@Entity(
tableName = "order_items",
foreignKeys = [
ForeignKey(
entity = OrderEntity::class,
parentColumns = ["id"],
childColumns = ["order_id"],
onDelete = ForeignKey.CASCADE // 订单删除时,订单项也删除
)
]
)
data class OrderItemEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "order_id") val orderId: Int,
@ColumnInfo(name = "product_id") val productId: Int,
@ColumnInfo(name = "quantity") val quantity: Int,
@ColumnInfo(name = "price") val price: Double
)
// 关联查询数据类
data class OrderWithItems(
@Embedded val order: OrderEntity,
@Relation(
parentColumn = "id",
entityColumn = "order_id"
)
val items: List<OrderItemEntity>
)
// DAO 接口
@Dao
interface ProductDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(products: List<ProductEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(product: ProductEntity): Long
@Update
suspend fun update(product: ProductEntity)
@Delete
suspend fun delete(product: ProductEntity)
@Query("SELECT * FROM products ORDER BY updated_at DESC")
fun getAllProducts(): Flow<List<ProductEntity>> // Flow:自动通知变化
@Query("SELECT * FROM products WHERE id = :id")
suspend fun getProductById(id: Int): ProductEntity?
@Query("SELECT * FROM products WHERE category_id = :categoryId")
fun getProductsByCategory(categoryId: Int): Flow<List<ProductEntity>>
@Query("SELECT * FROM products WHERE name LIKE '%' || :query || '%' OR description LIKE '%' || :query || '%'")
fun searchProducts(query: String): Flow<List<ProductEntity>>
@Query("UPDATE products SET is_favorite = :isFavorite WHERE id = :id")
suspend fun updateFavoriteStatus(id: Int, isFavorite: Boolean)
// 分页查询(配合 Paging 3)
@Query("SELECT * FROM products ORDER BY updated_at DESC")
fun getProductsPaged(): PagingSource<Int, ProductEntity>
// 聚合查询
@Query("SELECT COUNT(*) FROM products")
suspend fun getProductCount(): Int
@Query("SELECT AVG(price) FROM products WHERE category_id = :categoryId")
suspend fun getAveragePriceByCategory(categoryId: Int): Double
// 事务(原子性操作)
@Transaction
suspend fun replaceAll(products: List<ProductEntity>) {
deleteAll()
insertAll(products)
}
@Query("DELETE FROM products")
suspend fun deleteAll()
}
@Dao
interface OrderDao {
@Insert
suspend fun insertOrder(order: OrderEntity): Long
@Insert
suspend fun insertOrderItems(items: List<OrderItemEntity>)
// 关联查询
@Transaction
@Query("SELECT * FROM orders WHERE user_id = :userId ORDER BY created_at DESC")
fun getOrdersWithItems(userId: Int): Flow<List<OrderWithItems>>
// 复合事务
@Transaction
suspend fun createOrderWithItems(order: OrderEntity, items: List<OrderItemEntity>) {
val orderId = insertOrder(order).toInt()
insertOrderItems(items.map { it.copy(orderId = orderId) })
}
}
// TypeConverters(复杂类型转换)
class Converters {
@TypeConverter
fun fromStringList(value: List<String>): String = Gson().toJson(value)
@TypeConverter
fun toStringList(value: String): List<String> =
Gson().fromJson(value, object : TypeToken<List<String>>() {}.type)
@TypeConverter
fun fromDate(date: Date?): Long? = date?.time
@TypeConverter
fun toDate(timestamp: Long?): Date? = timestamp?.let { Date(it) }
}
// Database 配置
@Database(
entities = [ProductEntity::class, OrderEntity::class, OrderItemEntity::class],
version = 3,
exportSchema = true // 导出 Schema(用于 Migration 测试)
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun productDao(): ProductDao
abstract fun orderDao(): OrderDao
companion object {
@Volatile private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3) // 迁移
.fallbackToDestructiveMigration() // 开发时使用(生产慎用)
.build().also { INSTANCE = it }
}
}
}
}
// Migration 定义
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE products ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("""
CREATE TABLE IF NOT EXISTS `order_items` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`order_id` INTEGER NOT NULL,
`product_id` INTEGER NOT NULL,
`quantity` INTEGER NOT NULL,
`price` REAL NOT NULL,
FOREIGN KEY(`order_id`) REFERENCES `orders`(`id`) ON DELETE CASCADE
)
""".trimIndent())
}
}
5.4 DataStore(替代 SharedPreferences)
kotlin
// Proto DataStore(强类型,推荐)
// 定义 .proto 文件:src/main/proto/user_preferences.proto
/*
syntax = "proto3";
option java_package = "com.example";
option java_multiple_files = true;
message UserPreferences {
string theme = 1;
bool notifications_enabled = 2;
string language = 3;
int32 text_size = 4;
}
*/
// Preferences DataStore(键值对)
class UserPreferencesDataStore @Inject constructor(
@ApplicationContext private val context: Context
) {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "user_preferences"
)
companion object {
val THEME_KEY = stringPreferencesKey("theme")
val NOTIFICATIONS_KEY = booleanPreferencesKey("notifications_enabled")
val LANGUAGE_KEY = stringPreferencesKey("language")
val FONT_SIZE_KEY = floatPreferencesKey("font_size")
val LAST_LOGIN_KEY = longPreferencesKey("last_login_timestamp")
}
// 读取(Flow,自动感知变化)
val themeFlow: Flow<String> = context.dataStore.data
.catch { e ->
if (e is IOException) emit(emptyPreferences())
else throw e
}
.map { preferences ->
preferences[THEME_KEY] ?: "system"
}
val userPreferencesFlow: Flow<UserPreferencesData> = context.dataStore.data
.catch { if (it is IOException) emit(emptyPreferences()) else throw it }
.map { prefs ->
UserPreferencesData(
theme = prefs[THEME_KEY] ?: "system",
notificationsEnabled = prefs[NOTIFICATIONS_KEY] ?: true,
language = prefs[LANGUAGE_KEY] ?: "zh",
fontSize = prefs[FONT_SIZE_KEY] ?: 16f
)
}
// 写入(suspend)
suspend fun setTheme(theme: String) {
context.dataStore.edit { prefs ->
prefs[THEME_KEY] = theme
}
}
suspend fun setNotificationsEnabled(enabled: Boolean) {
context.dataStore.edit { prefs ->
prefs[NOTIFICATIONS_KEY] = enabled
}
}
// 批量更新(原子性)
suspend fun updatePreferences(theme: String, language: String) {
context.dataStore.edit { prefs ->
prefs[THEME_KEY] = theme
prefs[LANGUAGE_KEY] = language
}
}
suspend fun clearAll() {
context.dataStore.edit { it.clear() }
}
}
data class UserPreferencesData(
val theme: String,
val notificationsEnabled: Boolean,
val language: String,
val fontSize: Float
)
5.5 Paging 3 分页加载
PagingSource(网络分页)
kotlin
class ProductPagingSource(
private val api: ProductApi,
private val category: String? = null
) : PagingSource<Int, Product>() {
override fun getRefreshKey(state: PagingState<Int, Product>): Int? {
// 刷新时从哪一页开始(根据当前可见位置计算)
return state.anchorPosition?.let { anchor ->
state.closestPageToPosition(anchor)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchor)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Product> {
val page = params.key ?: 1
return try {
val response = api.getProducts(page = page, limit = params.loadSize, category = category)
val products = response.data?.map { it.toDomain() } ?: emptyList()
val total = response.total ?: 0
LoadResult.Page(
data = products,
prevKey = if (page == 1) null else page - 1,
nextKey = if (products.isEmpty() || page * params.loadSize >= total) null else page + 1
)
} catch (e: retrofit2.HttpException) {
LoadResult.Error(e)
} catch (e: IOException) {
LoadResult.Error(e)
}
}
}
// RemoteMediator(网络 + 本地缓存结合)
@OptIn(ExperimentalPagingApi::class)
class ProductRemoteMediator(
private val api: ProductApi,
private val database: AppDatabase,
private val category: String? = null
) : RemoteMediator<Int, ProductEntity>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, ProductEntity>
): MediatorResult {
val page = when (loadType) {
LoadType.REFRESH -> 1
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
?: return MediatorResult.Success(endOfPaginationReached = false)
// 根据最后一项计算下一页
(state.pages.size + 1)
}
}
return try {
val response = api.getProducts(page = page, limit = state.config.pageSize)
val products = response.data ?: emptyList()
database.withTransaction {
if (loadType == LoadType.REFRESH) {
database.productDao().deleteAll()
}
database.productDao().insertAll(products.map { it.toEntity() })
}
MediatorResult.Success(endOfPaginationReached = products.isEmpty())
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
}
// Repository 集成 Paging 3
class ProductRepository @Inject constructor(
private val api: ProductApi,
private val database: AppDatabase
) {
@OptIn(ExperimentalPagingApi::class)
fun getProductsPaged(category: String? = null): Flow<PagingData<Product>> {
return Pager(
config = PagingConfig(
pageSize = 20,
prefetchDistance = 5,
enablePlaceholders = false,
initialLoadSize = 40
),
remoteMediator = ProductRemoteMediator(api, database, category),
pagingSourceFactory = { database.productDao().getProductsPaged() }
).flow.map { pagingData ->
pagingData.map { entity -> entity.toDomain() }
}
}
}
// ViewModel
@HiltViewModel
class ProductListViewModel @Inject constructor(
private val repository: ProductRepository
) : ViewModel() {
val productsPagingData: Flow<PagingData<Product>> =
repository.getProductsPaged().cachedIn(viewModelScope)
}
// Compose UI
@Composable
fun PagedProductList(viewModel: ProductListViewModel = hiltViewModel()) {
val lazyPagingItems = viewModel.productsPagingData.collectAsLazyPagingItems()
LazyColumn {
items(
count = lazyPagingItems.itemCount,
key = lazyPagingItems.itemKey { it.id }
) { index ->
val product = lazyPagingItems[index]
if (product != null) {
ProductCard(product = product, onClick = {})
} else {
ProductCardPlaceholder()
}
}
// 加载状态
lazyPagingItems.apply {
when {
loadState.refresh is LoadState.Loading -> {
item { Box(Modifier.fillMaxWidth(), Alignment.Center) { CircularProgressIndicator() } }
}
loadState.append is LoadState.Loading -> {
item { Box(Modifier.fillMaxWidth(), Alignment.Center) { CircularProgressIndicator(Modifier.size(32.dp)) } }
}
loadState.refresh is LoadState.Error -> {
val error = loadState.refresh as LoadState.Error
item {
ErrorItem(
message = error.error.message ?: "加载失败",
onRetry = { retry() }
)
}
}
}
}
}
}
@Composable
private fun ProductCardPlaceholder() {
Box(modifier = Modifier.fillMaxWidth().height(80.dp).padding(horizontal = 16.dp, vertical = 4.dp)
.background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(8.dp)))
}
@Composable
private fun ErrorItem(message: String, onRetry: () -> Unit) {
Column(Modifier.fillMaxWidth().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Text(message, color = MaterialTheme.colorScheme.error)
Spacer(Modifier.height(8.dp))
Button(onClick = onRetry) { Text("重试") }
}
}
5.6 离线优先架构
kotlin
// 离线优先 Repository 模式(Single Source of Truth)
class ProductRepository @Inject constructor(
private val remoteDataSource: ProductRemoteDataSource,
private val localDataSource: ProductLocalDataSource,
private val networkMonitor: NetworkMonitor
) {
// 始终从本地读取(数据库是真实数据源)
fun observeProducts(): Flow<List<Product>> =
localDataSource.observeProducts()
.map { entities -> entities.map { it.toDomain() } }
// 刷新:从网络拉取并写入本地
suspend fun refreshProducts(): Result<Unit> {
if (!networkMonitor.isOnline) {
return Result.failure(NetworkException.NoNetworkException())
}
return safeApiCall { remoteDataSource.fetchProducts() }
.onSuccess { dtos ->
localDataSource.replaceAll(dtos.map { it.toEntity() })
}
.map { }
}
// 组合 Flow:自动触发刷新
fun getProductsWithRefresh(): Flow<DataResult<List<Product>>> = flow {
emit(DataResult.Loading)
refreshProducts() // 触发网络刷新
observeProducts()
.collect { products ->
emit(DataResult.Success(products))
}
}.catch { e ->
emit(DataResult.Error(e))
// 降级:发送本地缓存
observeProducts().collect { products ->
emit(DataResult.Stale(products, e))
}
}
}
sealed class DataResult<out T> {
object Loading : DataResult<Nothing>()
data class Success<T>(val data: T) : DataResult<T>()
data class Error(val exception: Throwable) : DataResult<Nothing>()
data class Stale<T>(val data: T, val exception: Throwable) : DataResult<T>() // 有缓存但过期
}
// 网络监听
class NetworkMonitor @Inject constructor(
@ApplicationContext private val context: Context
) {
val isOnline: Boolean
get() {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = cm.activeNetwork ?: return false
val capabilities = cm.getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
val networkState: Flow<Boolean> = callbackFlow {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { trySend(true) }
override fun onLost(network: Network) { trySend(false) }
}
cm.registerDefaultNetworkCallback(callback)
trySend(isOnline)
awaitClose { cm.unregisterNetworkCallback(callback) }
}.distinctUntilChanged()
}
Demo 代码:chapter05
kotlin
// chapter05/DataLayerDemo.kt
package com.example.androiddemos.chapter05
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
// 简化演示:模拟数据层
data class DemoProduct(val id: Int, val name: String, val price: Double)
class DemoRepository {
private val localCache = MutableStateFlow<List<DemoProduct>>(emptyList())
fun observeProducts(): Flow<List<DemoProduct>> = localCache.asStateFlow()
suspend fun refresh(): Result<Unit> {
delay(1500) // 模拟网络请求
val newProducts = (1..10).map { i ->
DemoProduct(i, "产品 #$i", (i * 29.9))
}
localCache.value = newProducts
return Result.success(Unit)
}
}
@Composable
fun Chapter05DataDemo() {
val repository = remember { DemoRepository() }
val products by repository.observeProducts().collectAsState(initial = emptyList())
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
isLoading = true
repository.refresh()
.onSuccess { isLoading = false }
.onFailure { e ->
isLoading = false
errorMessage = e.message
}
}
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text("数据持久化 Demo", style = MaterialTheme.typography.headlineSmall)
Text("离线优先架构:本地缓存 → UI,网络刷新 → 缓存", style = MaterialTheme.typography.bodySmall)
Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = {
isLoading = true
errorMessage = null
},
modifier = Modifier.weight(1f)
) { Text("刷新数据") }
}
Spacer(Modifier.height(16.dp))
when {
isLoading -> Box(Modifier.fillMaxWidth(), Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator()
Spacer(Modifier.height(8.dp))
Text("正在从服务器加载...")
}
}
errorMessage != null -> Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)
) {
Text(
"错误: $errorMessage",
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.onErrorContainer
)
}
else -> LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(products, key = { it.id }) { product ->
Card(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(product.name, style = MaterialTheme.typography.bodyLarge)
Text("¥${product.price}", color = MaterialTheme.colorScheme.error)
}
}
}
}
}
}
}
章节总结
| 知识点 | 必掌握程度 | 面试频率 |
|---|---|---|
| OkHttp 拦截器链 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Token 自动刷新(无感刷新) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Retrofit 协程支持 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Room Entity/DAO/Migration | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Room Flow 自动通知 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Room Transaction | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| DataStore vs SharedPreferences | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Paging 3 基础 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| RemoteMediator 离线缓存 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 离线优先架构(SSOT) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
👉 下一章:第六章------主题、样式与国际化