「高级 Android 架构师成长路线」的第 1 阶段 —— 强化体系与架构思维(Clean Architecture 实战)

🧭 阶段 1:强化体系与架构思维(为期 4 周)

🎯 目标

从 "写模块" → "设计架构"。

你将掌握 Clean Architecture 的思维方式,能独立设计 MVVM + UseCase + Repository 的分层结构,打造企业级架构模板。


🧩 一、Clean Architecture 思维结构图

复制代码
📱 UI 层(View / Compose)
   ↓ 事件、状态
🧠 ViewModel 层(State + Event + Effect)
   ↓ 调用 UseCase
⚙️ Domain 层(UseCase)
   ↓ 访问数据接口
💾 Data 层(Repository + DataSource)
   ↙️ 本地 LocalDataSource(Room / MMKV)
   ↘️ 网络 RemoteDataSource(Retrofit / WebSocket)

关键原则:

  • ViewModel 不直接依赖 Data 层,只依赖 UseCase(接口)

  • UseCase 只做单一业务逻辑

  • Repository 负责数据源合并(网络 + 本地)

  • 各层通信通过 Kotlin 协程 + Flow

  • 数据流单向:UI → VM → UseCase → Repo → 回流至 UI


📆 二、4 周学习计划(含每日任务)

第 1 周:架构基础与 Kotlin 强化

🎯 理解分层思想 + Kotlin 能力打牢

日期 学习任务
周一 阅读《Clean Architecture 原则》概要(理解 Domain / Data / UI 分离)
周二 Kotlin 高级语法复习:sealed class、Flow、inline、crossinline
周三 理解「单向数据流(UDF)」:UIState / UIEvent / UIEffect 模型
周四 实战:封装 BaseViewModel<State, Event, Effect> 基类
周五 实战:创建 UiState 管理类,配合 StateFlow 驱动 UI
周末 项目练习:把你一个旧页面改造成 UDF 架构(比如录音界面)

第 2 周:Repository + UseCase 分层实践

🎯 将 ViewModel 逻辑下沉为业务用例(UseCase)

日期 学习任务
周一 理解 UseCase 概念:单一职责、依赖倒置
周二 实战:编写 GetUserInfoUseCaseUpdateUserInfoUseCase
周三 封装 Repository 接口与实现类(含本地与远程数据源)
周四 编写 LocalDataSource(Room / MMKV)
周五 编写 RemoteDataSource(Retrofit + Coroutine)
周末 实战整合:实现一个 "用户资料中心" 模块(含离线缓存)

第 3 周:模块化与依赖管理

🎯 将架构结构扩展到多模块项目,掌握依赖注入思想。

日期 学习任务
周一 Gradle 多模块结构讲解(app / core / feature / data)
周二 学习 Hilt(依赖注入)与 Repository 注入实践
周三 拆分 "网络模块" 与 "本地存储模块"
周四 使用 Hilt 绑定 Repository 实现到接口
周五 编写一个跨模块的 UseCase(比如 UploadVoiceUseCase)
周末 练习:将语音录制模块改造成独立 feature module,可独立注入使用

第 4 周:统一 UI 架构与可复用模板

🎯 打造自己的「架构脚手架」。

日期 学习任务
周一 整理项目结构与包命名规范(domain/data/ui)
周二 封装通用的 BaseRepositoryResult(Success/Error/Loading)模型
周三 扩展 BaseViewModel 支持加载状态、错误捕获、重试机制
周四 创建模板工程脚本(通过 Gradle 自动生成新模块结构)
周五 优化 UIState 模型,支持多页面复用
周末 实战项目验收:创建完整模块(录音上传 + 播放 + 状态流 + Repository)

💡 三、实战项目建议

项目名称 实战目标
🎙️ VoiceManager Clean Demo 把之前的语音录制模块完全重构成 Clean 架构:UI → ViewModel → UseCase → Repository。
💬 IM Session 模块重构 对聊天列表模块实现 UseCase 分层,Room 缓存 + WebSocket 同步。
📷 Media Gallery 模块 拆分数据源(本地图库 + 网络资源),展示图片加载优化。

每个模块最后都形成:

复制代码
domain/
    usecase/
data/
    repository/
    datasource/
ui/
    viewmodel/
    activity/

📚 四、推荐学习资料

类型 名称 说明
📖 官方文档 Guide to app architecture (Google) 官方架构最佳实践
📘 教程 「Android Clean Architecture with Kotlin」by MindOrks 实战导向的 Clean 架构入门
📹 视频 Philipp Lackner - Clean Architecture 系列(YouTube) 讲解清晰、含 Flow + Hilt
📗 源码 nowinandroid (Google 官方 App) 最完整的 Clean + Compose 实践
🧠 文章 Medium: "Why UseCase is key in MVVM" 深入理解 UseCase 在大型项目的作用

🏁 阶段完成标志

完成以下 3 项后,你将顺利晋升到「架构中级」阶段👇:

  1. 拥有可复用的 BaseViewModel + UIState 模板

  2. 任意模块(如语音录制、聊天列表)实现完整的 Clean 架构

  3. 能清晰画出项目层次依赖图并独立设计新的模块结构


我接下来可以帮你生成:

  • 🧱 第 1 周「架构基础 + BaseViewModel + UIState 实战模板」的完整代码 Demo

  • 包含:BaseViewModelUiStateUiEventUiEffectViewModel 使用样例

是否要我直接生成?(✅ 是的,生成第 1 周完整 Demo 模板)

您说:

ChatGPT 说:

太好了 👍

我们现在正式进入 阶段 1·第 1 周:架构基础 + BaseViewModel + UIState 实战模板 🚀

这一周的目标是构建一个能在任意项目中通用的「基础架构骨架」:

MVVM + UDF 单向数据流模式(State/Event/Effect)

让你的 ViewModel 层结构化、可维护、可复用。


🧩 一、结构预览

我们先建立以下包结构(推荐):

复制代码
base/
 ├─ state/        # 状态定义(State, Event, Effect)
 ├─ viewmodel/    # 通用 BaseViewModel
 └─ utils/        # 扩展工具类

🧠 二、核心概念复习

名称 作用 举例
UiState 描述当前界面状态(数据类) 当前录音时长、播放状态等
UiEvent 来自用户的操作事件 点击按钮、滑动、删除等
UiEffect 一次性副作用事件 Toast、导航、弹窗等
BaseViewModel 管理 State、处理 Event、发送 Effect 负责业务逻辑和状态流转

⚙️ 三、完整代码模板

1️⃣ UiState.kt

Kotlin 复制代码
package com.hatio.base.state

/**
 * UI 层状态基类
 * 所有页面状态都应继承自 UiState
 */
interface UiState

2️⃣ UiEvent.kt

Kotlin 复制代码
package com.hatio.base.state

/**
 * 用户事件:点击、输入、滑动等交互操作
 */
interface UiEvent

3️⃣ UiEffect.kt

Kotlin 复制代码
package com.hatio.base.state

/**
 * 一次性副作用事件:Toast、导航、弹窗等
 */
interface UiEffect

4️⃣ BaseViewModel.kt

Kotlin 复制代码
package com.hatio.base.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.hatio.base.state.UiEffect
import com.hatio.base.state.UiEvent
import com.hatio.base.state.UiState
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

/**
 * 通用的 BaseViewModel
 * 使用 StateFlow 管理 UI 状态,Channel 发送一次性事件
 */
abstract class BaseViewModel<S : UiState, E : UiEvent, F : UiEffect> : ViewModel() {

    // UI 状态流
    private val _uiState: MutableStateFlow<S> by lazy { MutableStateFlow(initialState()) }
    val uiState: StateFlow<S> = _uiState.asStateFlow()

    // 一次性事件通道(如 Toast / 导航)
    private val _uiEffect = Channel<F>(Channel.BUFFERED)
    val uiEffect = _uiEffect.receiveAsFlow()

    // 初始化默认状态
    abstract fun initialState(): S

    // 更新状态:局部更新(不可变数据拷贝)
    protected fun setState(reducer: S.() -> S) {
        _uiState.value = _uiState.value.reducer()
    }

    // 发送副作用事件(一次性)
    protected fun sendEffect(builder: () -> F) {
        viewModelScope.launch { _uiEffect.send(builder()) }
    }

    // 处理用户事件
    abstract fun handleEvent(event: E)
}

5️⃣ 示例:实现一个录音状态的 ViewModel

VoiceUiState.kt
Kotlin 复制代码
package com.hatio.feature.voice

import com.hatio.base.state.UiState

data class VoiceUiState(
    val isRecording: Boolean = false,
    val isPlaying: Boolean = false,
    val recordTime: String = "00:00",
    val filePath: String? = null
) : UiState

VoiceUiEvent.kt
Kotlin 复制代码
package com.hatio.feature.voice import com.hatio.base.state.UiEvent sealed class VoiceUiEvent : UiEvent { object OnRecordClicked : VoiceUiEvent() object OnStopRecord : VoiceUiEvent() object OnPlayClicked : VoiceUiEvent() object OnPausePlay : VoiceUiEvent() }

VoiceUiEffect.kt
Kotlin 复制代码
package com.hatio.feature.voice

import com.hatio.base.state.UiEffect

sealed class VoiceUiEffect : UiEffect {
    data class ShowToast(val message: String) : VoiceUiEffect()
}

VoiceViewModel.kt
Kotlin 复制代码
package com.hatio.feature.voice

import androidx.lifecycle.viewModelScope
import com.hatio.base.viewmodel.BaseViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

/**
 * 示例 ViewModel:控制录音和播放的状态流转
 */
class VoiceViewModel : BaseViewModel<VoiceUiState, VoiceUiEvent, VoiceUiEffect>() {

    override fun initialState() = VoiceUiState()

    override fun handleEvent(event: VoiceUiEvent) {
        when (event) {
            VoiceUiEvent.OnRecordClicked -> startRecording()
            VoiceUiEvent.OnStopRecord -> stopRecording()
            VoiceUiEvent.OnPlayClicked -> startPlaying()
            VoiceUiEvent.OnPausePlay -> pausePlaying()
        }
    }

    private fun startRecording() {
        setState { copy(isRecording = true, recordTime = "00:00") }
        sendEffect { VoiceUiEffect.ShowToast("开始录音") }

        // 模拟计时
        viewModelScope.launch {
            var seconds = 0
            while (uiState.value.isRecording) {
                delay(1000)
                seconds++
                val time = String.format("%02d:%02d", seconds / 60, seconds % 60)
                setState { copy(recordTime = time) }
            }
        }
    }

    private fun stopRecording() {
        setState { copy(isRecording = false, filePath = "/path/to/record.aac") }
        sendEffect { VoiceUiEffect.ShowToast("录音已停止") }
    }

    private fun startPlaying() {
        setState { copy(isPlaying = true) }
        sendEffect { VoiceUiEffect.ShowToast("开始播放") }
    }

    private fun pausePlaying() {
        setState { copy(isPlaying = false) }
        sendEffect { VoiceUiEffect.ShowToast("暂停播放") }
    }
}

VoiceActivity.kt(简化版)
Kotlin 复制代码
package com.hatio.feature.voice

import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.hatio.feature.voice.databinding.ActivityVoiceBinding
import kotlinx.coroutines.flow.collectLatest

class VoiceActivity : ComponentActivity() {

    private val binding by lazy { ActivityVoiceBinding.inflate(layoutInflater) }
    private val viewModel by viewModels<VoiceViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        lifecycleScope.launchWhenStarted {
            viewModel.uiState.collectLatest { state ->
                binding.btnRecord.text = if (state.isRecording) "停止" else "录音"
                binding.tvTime.text = state.recordTime
            }
        }

        lifecycleScope.launchWhenStarted {
            viewModel.uiEffect.collectLatest { effect ->
                when (effect) {
                    is VoiceUiEffect.ShowToast -> Toast.makeText(this@VoiceActivity, effect.message, Toast.LENGTH_SHORT).show()
                }
            }
        }

        binding.btnRecord.setOnClickListener {
            if (viewModel.uiState.value.isRecording)
                viewModel.handleEvent(VoiceUiEvent.OnStopRecord)
            else
                viewModel.handleEvent(VoiceUiEvent.OnRecordClicked)
        }
    }
}

🧱 四、总结与阶段产物

到这里你已经拥有了一个 完全可复用的 UDF 架构模板

模块 文件 功能
base/state UiState, UiEvent, UiEffect 定义三种层次数据流
base/viewmodel BaseViewModel 统一管理 StateFlow + Effect
feature/voice VoiceViewModel + 状态类 业务逻辑示例

ChatGPT 说:

是否希望我接下来直接生成「第 2 周:Repository + UseCase 架构模板与 Demo(含网络与本地数据源)」?

您说:

是,希望

ChatGPT 说:

太棒了 ✅ Jeled!这一步正是从「写功能」→「写架构」的关键转折。

我们马上进入你计划的下一篇实战内容:


🚀 第 2 周实战主题

🧱 Repository + UseCase 架构模板与 Demo

目标:实现一个可扩展、可测试、支持网络与本地数据源共存的 Repository 层,并结合 UseCase 规范业务逻辑。


🗂️ 一、项目架构图

复制代码
app/
 ├── data/
 │    ├── local/         ← Room / DataStore / MMKV
 │    ├── remote/        ← Retrofit / OkHttp
 │    ├── repository/    ← 数据聚合层(核心)
 │    └── model/         ← DTO + Entity
 │
 ├── domain/
 │    ├── model/         ← 业务模型(UI 层使用)
 │    └── usecase/       ← 用例逻辑(UseCase)
 │
 ├── ui/
 │    ├── viewmodel/     ← ViewModel 调用 UseCase
 │    └── screen/        ← Compose / Fragment / Activity
 │
 └── di/                 ← Hilt 模块

🧠 二、架构理念

层级 职责 示例
RemoteDataSource 负责网络请求 Retrofit 调用
LocalDataSource 负责本地缓存 Room / MMKV
Repository 聚合数据源,隐藏数据实现细节 从缓存或网络读取
UseCase 业务逻辑抽象(单一职责) GetUserProfileUseCase
ViewModel 控制 UI 状态 调用 UseCase 并暴露 StateFlow

🧩 三、核心示例:获取用户资料(含本地缓存 + 网络回源)

1️⃣ RemoteDataSource.kt

Kotlin 复制代码
class UserRemoteDataSource @Inject constructor(
    private val api: UserApi
) {
    suspend fun fetchUser(userId: String): ApiResult<UserDto> {
        return try {
            val response = api.getUser(userId)
            ApiResult.Success(response)
        } catch (e: Exception) {
            ApiResult.Error(e)
        }
    }
}

2️⃣ LocalDataSource.kt

Kotlin 复制代码
class UserLocalDataSource @Inject constructor(
    private val dao: UserDao
) {
    suspend fun getUser(userId: String): UserEntity? = dao.getUserById(userId)

    suspend fun saveUser(user: UserEntity) {
        dao.insert(user)
    }
}

3️⃣ Repository.kt

Kotlin 复制代码
class UserRepository @Inject constructor(
    private val local: UserLocalDataSource,
    private val remote: UserRemoteDataSource
) {
    suspend fun getUser(userId: String): Flow<Resource<User>> = flow {
        emit(Resource.Loading)

        // 优先读取本地缓存
        local.getUser(userId)?.let { localUser ->
            emit(Resource.Success(localUser.toDomain()))
        }

        // 同步远程数据
        when (val result = remote.fetchUser(userId)) {
            is ApiResult.Success -> {
                val entity = result.data.toEntity()
                local.saveUser(entity)
                emit(Resource.Success(entity.toDomain()))
            }
            is ApiResult.Error -> emit(Resource.Error(result.exception))
        }
    }
}

4️⃣ UseCase.kt

Kotlin 复制代码
class GetUserProfileUseCase @Inject constructor(
    private val repository: UserRepository
) {
    operator fun invoke(userId: String): Flow<Resource<User>> {
        return repository.getUser(userId)
    }
}

5️⃣ ViewModel.kt

Kotlin 复制代码
@HiltViewModel
class UserViewModel @Inject constructor(
    private val getUserProfileUseCase: GetUserProfileUseCase
) : ViewModel() {

    private val _userState = MutableStateFlow<Resource<User>>(Resource.Loading)
    val userState: StateFlow<Resource<User>> = _userState.asStateFlow()

    fun loadUser(userId: String) {
        viewModelScope.launch {
            getUserProfileUseCase(userId).collect { result ->
                _userState.value = result
            }
        }
    }
}

6️⃣ UI 示例(Compose)

Kotlin 复制代码
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
    val state by viewModel.userState.collectAsState()

    when (state) {
        is Resource.Loading -> CircularProgressIndicator()
        is Resource.Success -> {
            val user = (state as Resource.Success).data
            Text("Hello, ${user.name}")
        }
        is Resource.Error -> Text("加载失败: ${(state as Resource.Error).throwable.message}")
    }
}

⚙️ 四、辅助类定义

Kotlin 复制代码
// Resource.kt
sealed class Resource<out T> {
    object Loading : Resource<Nothing>()
    data class Success<T>(val data: T) : Resource<T>()
    data class Error(val throwable: Throwable) : Resource<Nothing>()
}

// ApiResult.kt
sealed class ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>()
    data class Error(val exception: Exception) : ApiResult<Nothing>()
}

🧩 五、扩展与思考

  • ✅ 可添加 刷新策略(如 CachePolicy)

  • ✅ 支持 分页加载(Paging3)

  • ✅ 结合 Room + Flow 实现实时数据更新

  • ✅ 与 Hilt 结合实现模块化依赖注入


🎯 六、实践任务

周目标 说明
🧱 第 2 周 基于上面模板实现一个「用户资料模块」Demo(含 Repository + UseCase + Flow)
🔄 扩展任务 再实现一个「消息列表模块」,演示分页 + 缓存策略
🧩 加分项 使用 Hilt 完成依赖注入 + 添加单元测试

ChatGPT 说:

是否希望我接下来直接生成「第 3 周:模块化与依赖管理 」?

✅ 是的,继续下一阶段

🚫 暂时不用,先巩固第 2 周

您说:

是的,继续下一阶段

ChatGPT 说:

太棒了 ✅ 我们正式进入 ------ 第 3 周:模块化与依赖管理(进阶架构阶段) 🚀


🧭 第 3 周目标

让项目从单模块演进为「可扩展、可复用、低耦合」的多模块架构。

核心目标是理解 模块划分原则依赖反转公共库管理组件间通信


📦 第 3 周学习计划总览

阶段 模块 目标 关键技术
第 1 天 模块化基础 理解模块划分原则与 Gradle 结构 multi-module, Gradle buildSrc
第 2 天 公共依赖统一管理 学会集中管理依赖版本与库 buildSrc + Versions.kt
第 3 天 公共基础模块封装 封装 base, common, network, core 模块 Kotlin + Hilt + Retrofit
第 4 天 模块间通信 学习组件间解耦通信方案 ARouter + LiveEventBus
第 5 天 动态特性模块 使用 feature module 与动态加载 Dynamic Feature / Instant App
第 6 天 架构整合 Demo 整合成完整多模块项目 MVVM + Clean Architecture
第 7 天 总结 & 项目实战 拆分实际 App 架构 模块化设计文档编写

🧩 模块划分建议

你的项目(例如聊天/社交 App)可这样划分:

复制代码
app/                     // 主入口
core/                    // 核心配置、基础依赖
base/                    // BaseActivity, BaseFragment, BaseViewModel
common/                  // 通用工具类(网络、UI、扩展函数)
feature_login/           // 登录注册模块
feature_message/         // 聊天模块
feature_profile/         // 个人资料模块
feature_discover/        // 发现页模块
lib_network/             // 网络层 Retrofit 封装
lib_image/               // 图片加载库封装(Glide/Coil/Fresco)
lib_audio/               // 音视频封装(ExoPlayer/IjkPlayer)

⚙️ 核心内容一:统一依赖管理(buildSrc)

📁 结构

复制代码
buildSrc/
 ├── build.gradle.kts
 └── src/main/kotlin/
      ├── Libs.kt
      ├── Versions.kt
      └── Configs.kt

🔹 buildSrc/build.gradle.kts

Kotlin 复制代码
plugins {
    `kotlin-dsl`
}
repositories {
    google()
    mavenCentral()
}

🔹 Versions.kt

Kotlin 复制代码
object Versions {
    const val kotlin = "1.9.0"
    const val gradle = "7.4.2"
    const val coreKtx = "1.10.1"
    const val appcompat = "1.6.1"
    const val lifecycle = "2.6.1"
    const val retrofit = "2.9.0"
    const val hilt = "2.48"
    const val coil = "2.4.0"
}

🔹 Libs.kt

Kotlin 复制代码
object Libs {
    const val coreKtx = "androidx.core:core-ktx:${Versions.coreKtx}"
    const val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"
    const val lifecycle = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}"
    const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}"
    const val hilt = "com.google.dagger:hilt-android:${Versions.hilt}"
    const val coil = "io.coil-kt:coil:${Versions.coil}"
}

然后在任意模块中直接:

Groovy 复制代码
dependencies {
    implementation(Libs.retrofit)
    implementation(Libs.hilt)
}

🧠 模块间通信推荐方案

场景 推荐方案 示例
页面跳转 ARouter /login/LoginActivity
事件分发 LiveEventBus / FlowBus EventBus.post("logout")
数据共享 ViewModel (sharedViewModel) activityViewModels()
接口调用 接口 + ServiceLocator decoupled contract pattern

📘 实战目标

✅ 创建多模块项目(含 app、base、common、feature_login、feature_message)

✅ 使用 buildSrc 统一依赖

✅ 使用 ARouter 实现模块间跳转

✅ 使用 Hilt + ViewModel 构建依赖体系

✅ 最终生成模块化架构 Demo(结构清晰,可复用)


是否希望我帮你直接生成这个 多模块模板(含依赖管理 + 模块跳转 + MVVM 基础结构)

如果是,我可以生成完整的 Android Studio 可运行示例。

🟩 是的,请生成模板

🟥 暂时不用,我先看理论

您说:

是的,请生成模板

ChatGPT 说:

太好了 ✅ 我将为你生成一个 完整的可运行 Android 多模块架构模板,包含:

🔹 模块化结构 + MVVM + Hilt + Retrofit + ARouter + 统一依赖管理(buildSrc)


🧩 一、项目结构总览

复制代码
k8appnew/
 ├── build.gradle(根配置)
 ├── settings.gradle(仓库配置)
 ├── buildSrc/                ← 统一管理依赖版本
 │    ├── build.gradle.kts
 │    └── src/main/kotlin/
 │         ├── Configs.kt
 │         ├── Versions.kt
 │         └── Libs.kt
 ├── app/                     ← 主入口模块(仅导航)
 ├── base/                    ← BaseActivity/BaseViewModel/BaseFragment
 ├── common/                  ← 通用工具类(Log、扩展函数、资源管理)
 ├── lib_network/             ← Retrofit + OkHttp 网络封装
 ├── feature_login/           ← 登录模块
 ├── feature_message/         ← 聊天模块

⚙️ 二、settings.gradle(仓库与模块声明)

Kotlin 复制代码
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
        maven { url = uri("https://jitpack.io") }
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}
rootProject.name = "k8appnew"
include(":app", ":base", ":common", ":lib_network", ":feature_login", ":feature_message")

📦 三、buildSrc/build.gradle.kts

Kotlin 复制代码
plugins { `kotlin-dsl` }

repositories {
    google()
    mavenCentral()
}

📘 四、buildSrc/src/main/kotlin/Configs.kt

Kotlin 复制代码
object Configs {
    const val compileSdk = 34
    const val minSdk = 24
    const val targetSdk = 34
    const val versionCode = 1
    const val versionName = "1.0.0"
    const val applicationId = "com.hatio.chat"
}

📘 五、buildSrc/src/main/kotlin/Versions.kt

Kotlin 复制代码
object Versions {
    const val kotlin = "1.9.0"
    const val gradle = "7.4.2"
    const val hilt = "2.48"
    const val lifecycle = "2.6.2"
    const val retrofit = "2.9.0"
    const val okHttp = "4.11.0"
    const val coil = "2.4.0"
    const val arouter = "1.5.2"
}

📘 六、buildSrc/src/main/kotlin/Libs.kt

Kotlin 复制代码
object Libs {
    // AndroidX
    const val coreKtx = "androidx.core:core-ktx:1.12.0"
    const val appcompat = "androidx.appcompat:appcompat:1.6.1"
    const val material = "com.google.android.material:material:1.11.0"

    // Lifecycle
    const val lifecycle = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}"
    const val viewModel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycle}"

    // Hilt
    const val hilt = "com.google.dagger:hilt-android:${Versions.hilt}"
    const val hiltCompiler = "com.google.dagger:hilt-compiler:${Versions.hilt}"

    // Retrofit & OkHttp
    const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}"
    const val gsonConverter = "com.squareup.retrofit2:converter-gson:${Versions.retrofit}"
    const val okHttp = "com.squareup.okhttp3:okhttp:${Versions.okHttp}"
    const val logging = "com.squareup.okhttp3:logging-interceptor:${Versions.okHttp}"

    // ARouter
    const val arouter = "com.alibaba:arouter-api:${Versions.arouter}"
    const val arouterCompiler = "com.alibaba:arouter-compiler:${Versions.arouter}"
}

🏗 七、app 模块(主壳模块)

app/build.gradle

Groovy 复制代码
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'dagger.hilt.android.plugin'
    id 'kotlin-kapt'
}

android {
    namespace Configs.applicationId
    compileSdk Configs.compileSdk

    defaultConfig {
        applicationId Configs.applicationId
        minSdk Configs.minSdk
        targetSdk Configs.targetSdk
        versionCode Configs.versionCode
        versionName Configs.versionName
    }

    buildFeatures {
        viewBinding true
    }
}

dependencies {
    implementation project(':base')
    implementation project(':common')
    implementation project(':lib_network')
    implementation project(':feature_login')
    implementation project(':feature_message')

    implementation(Libs.coreKtx)
    implementation(Libs.appcompat)
    implementation(Libs.material)
    implementation(Libs.hilt)
    kapt(Libs.hiltCompiler)
}

app/src/main/java/com/hatio/chat/MainActivity.kt

Kotlin 复制代码
package com.hatio.chat

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.alibaba.android.arouter.launcher.ARouter

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ARouter.getInstance().build("/login/LoginActivity").navigation()
        finish()
    }
}

🔧 八、base 模块

Groovy 复制代码
plugins {
    id 'com.android.library'
    id 'org.jetbrains.kotlin.android'
}

android {
    compileSdk Configs.compileSdk
    defaultConfig { minSdk Configs.minSdk }
}

dependencies {
    implementation(Libs.coreKtx)
    implementation(Libs.lifecycle)
    implementation(Libs.viewModel)
}

示例:

Kotlin 复制代码
abstract class BaseViewModel : ViewModel()

🌐 九、lib_network 模块(Retrofit 封装)

Groovy 复制代码
plugins {
    id 'com.android.library'
    id 'org.jetbrains.kotlin.android'
}

android {
    compileSdk Configs.compileSdk
    defaultConfig { minSdk Configs.minSdk }
}

dependencies {
    implementation(Libs.retrofit)
    implementation(Libs.gsonConverter)
    implementation(Libs.okHttp)
    implementation(Libs.logging)
}
Kotlin 复制代码
object ApiServiceFactory {
    fun <T> create(service: Class<T>): T {
        val client = OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            }).build()

        return Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(service)
    }
}

🔐 十、feature_login 模块

Groovy 复制代码
plugins {
    id 'com.android.library'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}

android {
    compileSdk Configs.compileSdk
    defaultConfig { minSdk Configs.minSdk }
}

dependencies {
    implementation project(':base')
    implementation project(':common')
    implementation(Libs.arouter)
    kapt(Libs.arouterCompiler)
}
Kotlin 复制代码
@Route(path = "/login/LoginActivity")
class LoginActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        println("进入登录页")
    }
}

💬 十一、feature_message 模块

Kotlin 复制代码
@Route(path = "/message/MessageActivity")
class MessageActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        println("进入消息页")
    }
}

🚀 十二、初始化(在 Application 中)

Kotlin 复制代码
@HiltAndroidApp
class App : Application() {
    override fun onCreate() {
        super.onCreate()
        ARouter.openLog()
        ARouter.openDebug()
        ARouter.init(this)
    }
}

✅ 运行效果

  1. 启动 App → 自动跳转至 feature_login 登录模块

  2. 可通过:

    Kotlin 复制代码
    ARouter.getInstance().build("/message/MessageActivity").navigation()

    直接跳转消息模块。


是否希望我 帮你打包成完整 zip 工程目录(Android Studio 可直接运行)

如果是,我会生成所有文件结构 + 可直接导入的 Gradle 构建配置。

🟩 是的,请生成 zip

🟥 不用,我自己复制

您说:

不用,我自己复制

ChatGPT 说:

非常好 👍

那你自己复制即可,这个模板已经是 可直接落地的模块化工程骨架,适合你之后:

  • 🧱 拆分更多业务模块(如 feature_homefeature_user 等)

  • 🔄 在 buildSrc 中统一依赖和版本

  • 🧩 使用 ARouter 在模块间导航

  • 💉 使用 Hilt 注入跨模块依赖(例如 Repository


如果你接下来想进入

「第 4 周:网络层封装与数据流(Retrofit + Flow + Repository)」

我可以帮你基于当前架构直接加上:

  • ✅ 基础 ApiService 模板

  • ✅ Repository + UseCase 层结构

  • ✅ Flow 状态封装(UiStateResult

  • ✅ 统一异常处理

是否要我继续生成 第 4 周模板:网络层与数据流架构

您说:

ChatGPT 说:

太棒了 ✅ 我会直接给你 第 4 周:网络层封装与数据流(Retrofit + OkHttp + Flow + Repository + UseCase + 统一异常处理 + Hilt 注入) 的完整可落地模板代码片段和说明------你可以直接把这些文件拷进之前的模块化项目(lib_network / base / feature_*)中使用。

第 4 周目标(概览)

  • 统一网络层:Retrofit + OkHttp + 日志/鉴权/重试 拦截器

  • 使用 Flow 作为数据流传递(Repository -> ViewModel -> UI)

  • Result / UiState/ Resource 封装加载/成功/失败状态

  • 统一异常映射(网络/解析/业务)

  • 分离 Repository 与 UseCase(可测、可复用)

  • 用 Hilt 做依赖注入


依赖(build.gradle 中加)

(放到 lib_network 或公共 base 模块的 gradle)

Groovy 复制代码
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2"
implementation "com.google.dagger:hilt-android:2.48"
kapt "com.google.dagger:hilt-compiler:2.48"

目录建议(模块内)

复制代码
lib_network/
 ├─ di/                # Hilt module: 提供 Retrofit/OkHttp/ApiService
 ├─ model/             # DTO / Response wrappers
 ├─ network/           # 拦截器、ExceptionMapper
 ├─ repository/        # 网络仓库接口/实现
 └─ usecase/           # UseCase(业务用例)

一、统一的 Response / Resource / UiState 封装

Resource.kt

Kotlin 复制代码
sealed class Resource<out T> {
    object Loading : Resource<Nothing>()
    data class Success<T>(val data: T) : Resource<T>()
    data class Error(val throwable: Throwable, val code: Int? = null, val message: String? = null) : Resource<Nothing>()
}

UiState.kt(在 UI 层可选)

Kotlin 复制代码
data class UiState<T>(
    val loading: Boolean = false,
    val data: T? = null,
    val error: String? = null
)

二、统一异常映射器(将各种异常转换为友好错误)

NetworkException.kt

Kotlin 复制代码
sealed class NetworkException(message: String? = null, val code: Int? = null) : Exception(message) {
    class HttpError(code: Int, message: String?) : NetworkException(message, code)
    class Timeout(message: String? = null) : NetworkException(message)
    class NoNetwork(message: String? = null) : NetworkException(message)
    class ParseError(message: String? = null) : NetworkException(message)
    class Unknown(message: String? = null) : NetworkException(message)
}

ExceptionMapper.kt

Kotlin 复制代码
object ExceptionMapper {
    fun map(e: Throwable): NetworkException {
        return when (e) {
            is retrofit2.HttpException -> NetworkException.HttpError(e.code(), e.message())
            is java.net.SocketTimeoutException -> NetworkException.Timeout(e.message)
            is java.io.IOException -> NetworkException.NoNetwork(e.message)
            is com.google.gson.JsonParseException, is java.lang.IllegalStateException -> NetworkException.ParseError(e.message)
            else -> NetworkException.Unknown(e.message)
        }
    }
}

三、OkHttp 拦截器(鉴权、重试、日志)

AuthInterceptor.kt

Kotlin 复制代码
class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        val builder = original.newBuilder()
        tokenProvider()?.let { token ->
            builder.header("Authorization", "Bearer $token")
        }
        return chain.proceed(builder.build())
    }
}

Logging 使用 HttpLoggingInterceptor(不展示代码,直接用库)


四、Retrofit & OkHttp 提供(Hilt Module)

NetworkModule.kt

Kotlin 复制代码
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideOkHttpClient(tokenProvider: TokenProvider): OkHttpClient {
        val logging = HttpLoggingInterceptor()
        logging.level = HttpLoggingInterceptor.Level.BODY

        return OkHttpClient.Builder()
            .addInterceptor(AuthInterceptor { tokenProvider.getToken() })
            .addInterceptor(logging)
            .callTimeout(30, TimeUnit.SECONDS)
            .connectTimeout(15, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(client: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .client(client)
            .build()
    }

    @Provides
    @Singleton
    fun provideYourApi(retrofit: Retrofit): YourApiService = retrofit.create(YourApiService::class.java)
}

TokenProvider 是你自己用 Hilt 注入的接口,用来提供当前 token。)


五、ApiService 示例

YourApiService.kt

Kotlin 复制代码
interface YourApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: String): Response<UserDto>

    @POST("login")
    suspend fun login(@Body req: LoginRequest): Response<LoginResponse>
}

注意:上面用的是 suspend fun + Response<T>,便于你检查 HTTP code 并统一处理。


六、Repository 层(接口 + 实现)

UserRepository.kt(接口)

Kotlin 复制代码
interface UserRepository {
    fun getUserFlow(id: String): Flow<Resource<UserDto>>
    suspend fun login(username: String, password: String): Resource<LoginResponse>
}

UserRepositoryImpl.kt

Kotlin 复制代码
class UserRepositoryImpl @Inject constructor(private val api: YourApiService) : UserRepository {

    override fun getUserFlow(id: String): Flow<Resource<UserDto>> = flow {
        emit(Resource.Loading)
        try {
            val resp = api.getUser(id)
            if (resp.isSuccessful) {
                val body = resp.body()
                if (body != null) {
                    emit(Resource.Success(body))
                } else {
                    emit(Resource.Error(NetworkException.ParseError("Empty body")))
                }
            } else {
                emit(Resource.Error(NetworkException.HttpError(resp.code(), resp.message())))
            }
        } catch (e: Throwable) {
            emit(Resource.Error(ExceptionMapper.map(e)))
        }
    }.flowOn(Dispatchers.IO)

    override suspend fun login(username: String, password: String): Resource<LoginResponse> {
        return try {
            val resp = api.login(LoginRequest(username, password))
            if (resp.isSuccessful && resp.body() != null) {
                Resource.Success(resp.body()!!)
            } else {
                Resource.Error(NetworkException.HttpError(resp.code(), resp.message()))
            }
        } catch (e: Throwable) {
            Resource.Error(ExceptionMapper.map(e))
        }
    }
}

Hilt 绑定(RepositoryModule):

Kotlin 复制代码
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds
    abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}

七、UseCase(可选层,便于测试/复用)

GetUserUseCase.kt

Kotlin 复制代码
class GetUserUseCase @Inject constructor(private val repo: UserRepository) {
    operator fun invoke(userId: String): Flow<Resource<UserDto>> {
        return repo.getUserFlow(userId)
    }
}

LoginUseCase.kt

Kotlin 复制代码
class LoginUseCase @Inject constructor(private val repo: UserRepository) {
    suspend operator fun invoke(username: String, password: String): Resource<LoginResponse> {
        return repo.login(username, password)
    }
}

八、ViewModel(Flow -> State)

UserViewModel.kt

Kotlin 复制代码
@HiltViewModel
class UserViewModel @Inject constructor(private val getUserUseCase: GetUserUseCase) : ViewModel() {

    private val _userState = MutableStateFlow<Resource<UserDto>>(Resource.Loading)
    val userState: StateFlow<Resource<UserDto>> = _userState

    fun loadUser(id: String) {
        getUserUseCase(id)
            .onEach { _userState.value = it }
            .catch { throwable -> _userState.value = Resource.Error(ExceptionMapper.map(throwable)) }
            .launchIn(viewModelScope)
    }
}

在 Activity/Fragment 中观察:

Kotlin 复制代码
lifecycleScope.launchWhenStarted {
    viewModel.userState.collect { resource ->
        when (resource) {
            is Resource.Loading -> showLoading()
            is Resource.Success -> showData(resource.data)
            is Resource.Error -> showError(resource.message ?: "未知错误")
        }
    }
}

九、统一错误展示(UI 层建议)

  • NetworkException.HttpError 可根据 code 做不同处理(401 -> 跳登录,500 -> 服务错误)

  • NoNetwork 提示检查网络并提供重试

  • ParseError 记录日志并提示「数据异常」


十、分页 / 列表建议(Flow + Paging 可结合)

若需要列表分页,强烈建议使用 Android Paging3(与 Flow 很好配合)。此处不展开,如需我可以补分页模板。


十一、缓存策略(选配)

  • 简单:在 Repository 内先读本地缓存(Room / SharedPref),然后 emit 缓存,随后从网络刷新并 emit 新值。

  • 复杂:使用 NetworkBoundResource 模式(经典实现,能优雅处理缓存/刷新)。

简易示例(伪代码):

Kotlin 复制代码
flow {
  emit(Resource.Loading)
  val cached = local.query(id)
  if (cached != null) emit(Resource.Success(cached))
  val network = api.getUser(id)
  if (network.isSuccessful) {
     local.insert(network.body())
     emit(Resource.Success(network.body()!!))
  }
}.flowOn(Dispatchers.IO)

十二、测试建议

  • UseCase / Repository 的单元测试 -> 用 MockWebServer 模拟后端,断言 Flow 发射的 Resource 顺序(Loading -> Success/Error)。

  • ViewModel -> 使用 TurbinerunBlockingTest 收集 StateFlow。

最后(交付内容)

我已经把核心模板代码给出,接下来你可以选择:

  1. 我帮你把这些文件 生成成完整工程结构并打包为 zip(我会立即生成)。

  2. 你把上面代码直接拷贝到项目中,我再帮你调整/修复集成问题。

  3. 我再补充:Paging3 模板 / NetworkBoundResource 缓存实现 / MockWebServer 单元测试示例。

你想先要哪个?

您说:

Paging3 模板 + NetworkBoundResource 缓存实现 + MockWebServer 单元测试示例

ChatGPT 说:

非常棒 🔥 --- 你现在正式进入第 4 周进阶阶段:

「分页 + 网络缓存 + 单元测试」完整架构模板

以下是为你量身定制的 Kotlin + Paging3 + NetworkBoundResource + MockWebServer 架构模板,遵循 MVVM + Repository + Flow + Hilt 架构,可直接落地使用。


🧱 一、项目结构

复制代码
app/
 ├─ data/
 │   ├─ local/
 │   │   ├─ AppDatabase.kt
 │   │   └─ UserDao.kt
 │   ├─ remote/
 │   │   ├─ ApiService.kt
 │   │   └─ MockInterceptor.kt
 │   ├─ repository/
 │   │   ├─ UserRepository.kt
 │   │   └─ NetworkBoundResource.kt
 │   └─ model/
 │       └─ User.kt
 ├─ ui/
 │   ├─ UserListViewModel.kt
 │   └─ UserListFragment.kt
 ├─ di/
 │   └─ AppModule.kt
 ├─ MainActivity.kt
 └─ test/
     └─ UserRepositoryTest.kt

⚙️ 二、Gradle 依赖(build.gradle)

Groovy 复制代码
dependencies {
    // Jetpack
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.6"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6"

    // Paging 3
    implementation "androidx.paging:paging-runtime:3.3.2"

    // Room
    implementation "androidx.room:room-runtime:2.6.1"
    kapt "androidx.room:room-compiler:2.6.1"
    implementation "androidx.room:room-ktx:2.6.1"

    // Retrofit
    implementation "com.squareup.retrofit2:retrofit:2.11.0"
    implementation "com.squareup.retrofit2:converter-gson:2.11.0"
    implementation "com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12"

    // Hilt
    implementation "com.google.dagger:hilt-android:2.52"
    kapt "com.google.dagger:hilt-compiler:2.52"

    // Coroutine
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"

    // Test
    testImplementation "junit:junit:4.13.2"
    testImplementation "androidx.arch.core:core-testing:2.2.0"
    testImplementation "com.squareup.okhttp3:mockwebserver:5.0.0-alpha.12"
    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0"
}

🧩 三、NetworkBoundResource.kt

Kotlin 复制代码
package com.example.data.repository

import kotlinx.coroutines.flow.*

/**
 * 通用的网络 + 本地缓存逻辑
 */
inline fun <ResultType, RequestType> networkBoundResource(
    crossinline query: () -> Flow<ResultType>,
    crossinline fetch: suspend () -> RequestType,
    crossinline saveFetchResult: suspend (RequestType) -> Unit,
    crossinline shouldFetch: (ResultType?) -> Boolean = { true }
): Flow<ResultType> = flow {
    val data = query().firstOrNull()
    val flow = if (shouldFetch(data)) {
        try {
            val remote = fetch()
            saveFetchResult(remote)
            query()
        } catch (t: Throwable) {
            query()
        }
    } else {
        query()
    }
    emitAll(flow)
}

🧠 四、UserRepository.kt

Kotlin 复制代码
package com.example.data.repository

import androidx.paging.*
import com.example.data.local.UserDao
import com.example.data.remote.ApiService
import com.example.data.model.User
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class UserRepository @Inject constructor(
    private val api: ApiService,
    private val dao: UserDao
) {

    fun getUsersPaged(): Flow<PagingData<User>> {
        return Pager(
            config = PagingConfig(pageSize = 20, enablePlaceholders = false),
            pagingSourceFactory = { dao.pagingSource() }
        ).flow
    }

    fun getUsersWithCache() = networkBoundResource(
        query = { dao.getAllFlow() },
        fetch = { api.getUsers() },
        saveFetchResult = { dao.insertAll(it) }
    )
}

💾 五、Room 数据层

AppDatabase.kt

Kotlin 复制代码
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

UserDao.kt

Kotlin 复制代码
@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAllFlow(): Flow<List<User>>

    @Query("SELECT * FROM user")
    fun pagingSource(): PagingSource<Int, User>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(users: List<User>)
}

🌐 六、ApiService.kt

Kotlin 复制代码
interface ApiService {
    @GET("users")
    suspend fun getUsers(): List<User>
}

🧰 七、ViewModel + UI

UserListViewModel.kt

Kotlin 复制代码
@HiltViewModel
class UserListViewModel @Inject constructor(
    private val repo: UserRepository
) : ViewModel() {

    val userPaging = repo.getUsersPaged().cachedIn(viewModelScope)

    val userCacheFlow = repo.getUsersWithCache()
}

UserListFragment.kt

Kotlin 复制代码
@AndroidEntryPoint
class UserListFragment : Fragment(R.layout.fragment_user_list) {

    private val viewModel: UserListViewModel by viewModels()
    private val adapter = UserPagingAdapter()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerView)
        recyclerView.adapter = adapter

        lifecycleScope.launchWhenStarted {
            viewModel.userPaging.collectLatest { adapter.submitData(it) }
        }
    }
}

🧪 八、MockWebServer 单元测试

UserRepositoryTest.kt

Kotlin 复制代码
@ExperimentalCoroutinesApi
class UserRepositoryTest {

    private lateinit var mockWebServer: MockWebServer
    private lateinit var api: ApiService

    @Before
    fun setup() {
        mockWebServer = MockWebServer()
        mockWebServer.start()
        api = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }

    @Test
    fun testFetchUsers() = runTest {
        val mockResponse = MockResponse()
            .setResponseCode(200)
            .setBody("""[{"id":1,"name":"Alice"}]""")
        mockWebServer.enqueue(mockResponse)

        val result = api.getUsers()
        assert(result.first().name == "Alice")
    }

    @After
    fun teardown() {
        mockWebServer.shutdown()
    }
}

✅ 九、总结

功能 实现
分页加载 Paging3 + Room
缓存机制 NetworkBoundResource
网络请求 Retrofit
测试 MockWebServer
架构模式 MVVM + Repository + Flow + Hilt
相关推荐
vir023 小时前
P1928 外星密码(dfs)
java·数据结构·算法·深度优先·1024程序员节
旋转小马3 小时前
XGBoost完整学习指南:从数据清洗到模型调参
机器学习·scikit-learn·xgboost·1024程序员节
大米粥哥哥3 小时前
Qt QProcess基于Linux的命令管道符号无效问题【已解决】
linux·qt·shell·qprocess·1024程序员节·管道符号
KAI丶3 小时前
【Https】Received fatal alert: internal_error
https·1024程序员节
摇滚侠3 小时前
全面掌握PostgreSQL关系型数据库,备份和恢复,笔记46和笔记47
java·数据库·笔记·postgresql·1024程序员节
喜欢吃燃面4 小时前
数据结构算法题:list
开发语言·c++·学习·算法·1024程序员节
Dlkoiw4 小时前
Slotted Aloha
matlab·1024程序员节·aloha·slotted aloha
。TAT。4 小时前
C++ - 多态
开发语言·c++·学习·1024程序员节
DreamLife☼4 小时前
Node-RED革命性实践:从智能家居网关到二次开发,全面重新定义可视化编程
mqtt·网关·低代码·智能家居·iot·1024程序员节·node-red