「高级 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
相关推荐
张拭心6 小时前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
张拭心6 小时前
Android 17 来了!新特性介绍与适配建议
android·前端
Kapaseker8 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴9 小时前
Android17 为什么重写 MessageQueue
android
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android