安卓 第五章:网络与数据持久化

本章深入 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) ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

👉 下一章:第六章------主题、样式与国际化

相关推荐
Kapaseker2 小时前
介绍一个新的 Compose 控件 — 浮动菜单
android·kotlin
空中海2 小时前
第二章:UI 开发——View 系统与 Jetpack Compose
android·ui
墨神谕2 小时前
NAT、TUN、DR三种模式
网络
末日汐2 小时前
网络层IP
服务器·网络·tcp/ip
fengci.2 小时前
php反序列化(复习)(第五章)
android·开发语言·学习·php
美狐美颜sdk2 小时前
视频平台如何实现实时美颜?Android/iOS直播APP美颜SDK接入指南
android·前端·人工智能·ios·音视频·第三方美颜sdk·视频美颜sdk
XiaoLeisj2 小时前
Android 短视频项目实战:从登录态回流、设置页动作分发到缓存清理、协议页复用与密码重置的完整实现个人中心与设置模块
android·mvvm·webview·arouter
@encryption2 小时前
计算机网络 --- 动态路由
网络