一文读懂 Kotlin 数据流 Flow 的使用

📱 这是一份为 Android 开发团队精心准备的 Kotlin Flow 完全指南

目标:让每个开发者都能从"听说过"进阶到"能用好"


🎯 为什么我们要学 Kotlin Flow?

现状痛点

在学 Flow 之前,你可能遇到过这些问题:

问题 表现 Flow 如何解决
回调地狱 多层网络请求嵌套混乱 链式调用,代码线性可读
内存泄漏 后台任务没取消导致 OOM 自动生命周期管理
异常处理难 异常被吞掉或崩溃 统一的异常处理机制
线程切换复杂 手动管理 Handler、Thread Dispatcher 自动切换
状态同步困难 数据更新 UI 不及时 响应式自动同步

Flow 的核心价值

  • ✅ 简化异步代码
  • ✅ 自动生命周期管理
  • ✅ 完整的异常处理
  • ✅ 实时数据响应(代替 LiveData)
  • ✅ 与 Coroutine 完美结合

💡 核心概念速览(10 分钟理解)

概念 1:Flow 是什么?

简单说:Flow 是一条"数据传送带"

现实例子:快递物流

  • 📦 数据源 = 发货仓库
  • 🏭 中间加工 = 流转中心(分拣、扫描)
  • 📍 最终消费者 = 你的家门口

概念 2:Cold Flow vs Hot Flow(重要!)

❄️ Cold Flow(冷流)= 懒人快递

scss 复制代码
// 使用 flow { } 创建的 Flow 是冷流
val coldFlow = flow {
    println("开始加工快递")  // 有人下单才执行
    emit(1)
}

// 第一个订阅者
coldFlow.collect { println("用户1收到: $it") }
// 输出: 开始加工快递 → 用户1收到: 1

// 第二个订阅者
coldFlow.collect { println("用户2收到: $it") }
// 输出: 开始加工快递 → 用户2收到: 1
// ⚠️ 执行了两次!每个订阅都是独立的

特点:

  • ✅ 没人订阅就不执行(节省资源)
  • ❌ 重复订阅会重复执行
  • 🎯 适用于:网络请求、数据库查询

🔥 Hot Flow(热流)= 直播广播

scss 复制代码
// 使用 MutableSharedFlow/StateFlow 创建的是热流
val hotFlow = MutableSharedFlow<Int>()

// 发送者不等订阅者,直接广播
launch {
    hotFlow.emit(1)
    hotFlow.emit(2)
    hotFlow.emit(3)
}

// 晚到的订阅者错过前面的值
delay(100)
hotFlow.collect { println("迟到的用户: $it") }
// 输出: 迟到的用户: 2, 迟到的用户: 3
// ❌ 错过了第一个值 1

特点:

  • ✅ 无论有没有订阅都执行
  • ✅ 所有订阅者共享同一条流
  • ❌ 资源消耗更多
  • 🎯 适用于:事件通知、用户交互

快速判断

css 复制代码
需要维护当前状态?          → StateFlow(热流)
只需要发送事件通知?        → SharedFlow(热流)
想要每次都重新执行?        → Flow(冷流)
想要实时响应数据库变化?    → Room Flow(冷流)

🛠️ 实战:最常见的 5 个场景

场景 1:单个网络请求(最基础)

业务需求:点击按钮加载用户信息

kotlin 复制代码
// ViewModel 中
class UserViewModel : ViewModel() {

    // 将网络请求包装成 Flow
    fun fetchUser(userId: String) = flow {
        try {
            val user = apiService.getUser(userId)
            emit(user)  // 发送数据
        } catch (e: Exception) {
            throw e  // 异常会传给 UI 层处理
        }
    }
}

// Activity/Fragment 中
viewModel.fetchUser("123")
    .catch { exception ->
        Toast.show("加载失败: ${exception.message}")
    }
    .collect { user ->
        // 拿到数据,更新 UI
        updateUserUI(user)
    }

优点

  • ✅ 代码简洁,易读易维护
  • ✅ 统一的异常处理
  • ✅ 自动线程切换

场景 2:顺序网络请求(依赖关系)

业务需求:先获取用户信息,再根据用户 ID 获取他的文章列表

复制代码
用户输入 ID → 查询用户信息 → 获取该用户的文章 → 展示文章列表
kotlin 复制代码
// 错误方式 ❌(回调地狱)
apiService.getUser("123") { user ->
    apiService.getPosts(user.id) { posts ->
        updateUI(user, posts)
    }
}

// 正确方式 ✅(Flow 方式)
fun getUserWithPosts(userId: String) = flow {
    val user = apiService.getUser(userId)
    val posts = apiService.getPosts(user.id)
    emit(UserWithPosts(user, posts))
}

// 使用
viewModel.getUserWithPosts("123")
    .catch { Toast.show("加载失败") }
    .collect { data ->
        updateUI(data.user, data.posts)
    }

为什么用 Flow?

  • 📊 从"嵌套金字塔"变成"线性流水线"
  • 🎯 依赖关系一目了然
  • 🛡️ 异常处理统一

场景 3:并行网络请求(独立关系)

业务需求:首页需要同时加载用户、推荐、广告等数据(它们互不依赖)

ini 复制代码
┌─ 获取用户信息 ─┐
│                ├─ 合并 → 展示首页
├─ 获取推荐文章 ─┤
│                │
└─ 获取广告数据 ─┘

时间对比:
顺序执行: 1s + 1s + 1s = 3s  ❌
并行执行: max(1s, 1s, 1s) = 1s  ✅
scss 复制代码
// 使用 combine 操作符(并行加载)
fun getHomeData(): Flow<HomeData> = combine(
    apiService.getUser(),        // 流1
    apiService.getRecommends(),  // 流2
    apiService.getAds()          // 流3
) { user, recommends, ads ->
    // 所有数据都到达了,才执行这里
    HomeData(user, recommends, ads)
}

// 使用
viewModel.getHomeData()
    .catch { Toast.show("加载失败") }
    .collect { homeData ->
        updateHomeUI(homeData)
    }

性能对比

  • 🐢 顺序执行:需要 3 秒
  • 🚀 并行执行:只需 1 秒(快 3 倍!)

场景 4:实时搜索(防抖 + 去重 + 实时请求)

业务需求:搜索框边打字边搜索(但要避免频繁请求服务器)

makefile 复制代码
用户输入: k → ko → kot → kotl → kotlin
         ↓    ↓    ↓     ↓      ↓
      请求? 请求? 请求? 请求?  最后请求

问题分析

  • ❌ 用户每输入一个字就请求?太浪费流量
  • ❌ 新的搜索来了,还在处理旧的搜索结果?太慢

解决方案

kotlin 复制代码
fun search(queryFlow: Flow<String>): Flow<List<SearchResult>> = queryFlow
    .debounce(300)              // 延迟 300ms(停止输入 300ms 后才搜索)
    .filter { it.isNotEmpty() } // 过滤空字符串
    .distinctUntilChanged()     // 去重(相同词不重复搜索)
    .flatMapLatest { query ->   // 关键!新搜索来了,取消旧搜索
        apiService.search(query)
    }
    .catch { emptyFlow() }      // 搜索出错显示空列表

实际效果

makefile 复制代码
用户输入: k k o t l i n (间隔 100ms)
         ↓
debounce ✗ ✗ ✗ ✗ ✗ ✗ → 触发搜索(用户停止后)
distinctUntilChanged → "kotlin" (去重)
flatMapLatest → 仅处理最新的搜索
结果: 只发出 1 个网络请求!

场景 5:数据库 + 实时监听

业务需求:实时显示本地数据库的内容,数据库一改马上更新 UI

kotlin 复制代码
// Room DAO 层
@Dao
interface TodoDao {
    @Query("SELECT * FROM todos ORDER BY id DESC")
    fun getAllTodos(): Flow<List<Todo>>  // ← 返回 Flow!

    suspend fun insertTodo(todo: Todo)
}

// ViewModel
class TodoViewModel(private val dao: TodoDao) : ViewModel() {

    val todos: Flow<List<Todo>> = dao.getAllTodos()
        .catch { exception ->
            Log.e("TodoVM", "加载失败", exception)
            emitAll(emptyFlow())
        }
}

// Activity
viewModel.todos.collect { todoList ->
    // 每当数据库改变,这里自动执行
    updateTodoList(todoList)
}

优点

  • ✅ 无需手动查询数据库
  • ✅ 数据库一改,UI 自动更新
  • ✅ 消除了 LiveData 的"粘性值"问题

🎨 Flow 操作符完全手册

常用操作符速查表

📝 数据变换类

操作符 功能 使用场景 代码示例
map 数据转换 DTO → UI Model .map { it.toUIModel() }
filter 数据过滤 只要满足条件的 .filter { it.age > 18 }
transform 自定义变换 复杂转换 .transform { emit(it * 2) }

🔗 流合并类

操作符 特点 使用场景
zip 等待全部完成 多个独立请求,都需要
combine 实时组合 多个响应式数据源
flatMapConcat 顺序合并 依赖关系的任务
flatMapLatest 取最新值 搜索、自动完成

🛡️ 异常处理类

方式 使用场景 特点
catch 通用异常处理 可发送替代值
retryWhen 重试策略 支持条件重试
onCompletion 清理资源 成功/失败都执行

⚠️ 常见的坑(做过的人都踩过)

坑 1:忘记订阅 Flow

scss 复制代码
// ❌ 错误:Flow 不会执行
viewModel.fetchUser("123")
// 什么都不会发生!

// ✅ 正确:必须调用 collect
viewModel.fetchUser("123")
    .collect { user ->
        updateUI(user)
    }

记住: Flow 需要有"消费者"(collect)才会执行,这叫"冷流"。


坑 2:Flow 在主线程执行但没切换线程

scss 复制代码
// ❌ 错误:这样会阻塞主线程!
viewModel.getUserFromDatabase()
    .collect { user ->  // ← 在主线程等待数据库查询
        updateUI(user)
    }

// ✅ 正确:指定线程
viewModel.getUserFromDatabase()
    .flowOn(Dispatchers.IO)  // ← 数据库操作放在 IO 线程
    .collect { user ->
        updateUI(user)  // ← 这里还是主线程
    }

关键字flowOn(Dispatchers.IO) 上游异步,collect 自动回到主线程。


坑 3:重复 collect 导致重复执行

scss 复制代码
// ❌ 错误:订阅两次,执行两次网络请求
val userFlow = viewModel.fetchUser("123")

userFlow.collect { user1 ->
    updateProfile(user1)
}

userFlow.collect { user2 ->
    updateHeader(user2)
}
// 结果: 发出了 2 个相同的网络请求!浪费流量

// ✅ 正确方式 1:使用 SharedFlow(热流,共享结果)
val userFlow = viewModel.fetchUser("123")
    .shareIn(viewModelScope, SharingStarted.Lazily, replay = 1)

userFlow.collect { updateProfile(it) }
userFlow.collect { updateHeader(it) }
// 结果: 只发出 1 个网络请求

// ✅ 正确方式 2:使用 StateFlow
private val _user = MutableStateFlow<User?>(null)
val user = _user.asStateFlow()
// 只需要 collect 一次状态值

通俗理解

  • 🧊 Flow = 冷流,每次订阅都重新执行(如:每次点击都重新查询)
  • 🔥 StateFlow/SharedFlow = 热流,所有订阅者共享结果(如:用户登录后所有页面都知道)

坑 4:忘记处理异常

scss 复制代码
// ❌ 错误:异常被吞掉或导致崩溃
flow {
    throw Exception("网络错误")
}.collect { data ->
    updateUI(data)  // ← 异常时永远不会执行
}

// ✅ 正确:用 catch 处理
flow {
    throw Exception("网络错误")
}
.catch { exception ->
    showError(exception.message)
}
.collect { data ->
    updateUI(data)
}

最佳实践 :永远不要忘记 .catch { }


坑 5:ViewModel 卸载后 Flow 还在执行

scss 复制代码
// ❌ 错误:ViewModel 被销毁,Flow 还在执行
flow {
    repeat(100) {
        delay(1000)
        emit(it)
    }
}.collect { value ->
    println(value)  // ← ViewModel 销毁后仍然执行,内存泄漏!
}

// ✅ 正确:使用 viewModelScope
viewModelScope.launch {
    flow {
        repeat(100) {
            delay(1000)
            emit(it)
        }
    }.collect { value ->
        println(value)  // ← ViewModel 销毁时自动取消
    }
}

记住viewModelScope.launch { } 会自动取消任务。


📊 StateFlow vs SharedFlow 选择指南

场景决策树

markdown 复制代码
你想维护一个状态值?
├─ 是 → StateFlow
│   └─ 用户信息、表单数据、UI 状态等
└─ 否 → SharedFlow
    └─ 导航事件、消息提示等

完整对比

kotlin 复制代码
// StateFlow:维护当前状态
private val _userState = MutableStateFlow<User?>(null)
val userState: StateFlow<User?> = _userState.asStateFlow()

fun login(email: String) {
    viewModelScope.launch {
        _userState.value = apiService.login(email)  // ← 更新状态
    }
}

// 使用
viewModel.userState.collect { user ->
    if (user != null) {
        showMainUI()  // 用户已登录
    } else {
        showLoginUI()  // 用户未登录
    }
}

---

// SharedFlow:发送一次性事件
private val _navigationEvent = MutableSharedFlow<NavigationEvent>()
val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent.asSharedFlow()

fun goToDetail(itemId: Int) {
    viewModelScope.launch {
        _navigationEvent.emit(NavigationEvent.GoToDetail(itemId))  // ← 发送事件
    }
}

// 使用
viewModel.navigationEvent.collect { event ->
    when (event) {
        is NavigationEvent.GoToDetail -> {
            navigator.openDetailPage(event.itemId)
        }
    }
}

🔄 处理异常和重试的正确姿势

场景:网络请求失败时自动重试

方式 1:简单重试(重试 N 次)

kotlin 复制代码
fun fetchUser(id: String) = flow {
    emit(apiService.getUser(id))
}
.retry(3)  // ← 失败重试 3 次
.catch { exception ->
    Toast.show("重试 3 次后仍然失败: ${exception.message}")
}

问题:立即重试,没有延迟,容易 DDoS 服务器。


方式 2:延迟重试(指数退避)

kotlin 复制代码
fun fetchUser(id: String) = flow {
    emit(apiService.getUser(id))
}
.retryWhen { exception, attempt ->
    // 第一次失败: 延迟 1秒
    // 第二次失败: 延迟 2秒
    // 第三次失败: 延迟 4秒
    val delayMillis = (1000L shl attempt)  // 2^attempt * 1000
    delay(delayMillis)
    attempt < 3  // ← 最多重试 3 次
}
.catch { exception ->
    showError("网络连接失败,请检查网络")
}

输出时间线

scss 复制代码
第 1 次请求  ────► 失败 ──(等1秒)─→ 第 2 次请求  ────► 失败
  ──(等2秒)─→ 第 3 次请求  ────► 失败  ──(等4秒)─→ 第 4 次请求

方式 3:条件化重试(只重试某些错误)

kotlin 复制代码
fun fetchUser(id: String) = flow {
    emit(apiService.getUser(id))
}
.retryWhen { exception, attempt ->
    when {
        // 服务器错误,重试
        exception is HttpException &&
        exception.code() >= 500 -> {
            delay(1000 * (attempt + 1))
            true
        }
        // 客户端错误,不重试(肯定是请求格式错误)
        exception is HttpException &&
        exception.code() >= 400 -> {
            false
        }
        // 网络错误,重试
        exception is IOException -> {
            delay(1000 * (attempt + 1))
            attempt < 3
        }
        else -> false
    }
}

🧪 单元测试:如何测试你的 Flow

快速 Flow 测试框架

kotlin 复制代码
// 1. 添加依赖
dependencies {
    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test"
    testImplementation "app.cash.turbine:turbine"  // ← Flow 测试神器
}

// 2. 编写测试
@Test
fun testFetchUserSuccess() = runTest {
    val viewModel = UserViewModel(mockApi)

    viewModel.fetchUser("123").test {
        // 验证第一个值
        val user = awaitItem()
        assertEquals("john", user.name)

        // 验证流完成
        awaitComplete()
    }
}

@Test
fun testFetchUserError() = runTest {
    val viewModel = UserViewModel(mockApiWithError)

    viewModel.fetchUser("123").test {
        val exception = awaitError()
        assertTrue(exception is NetworkException)
    }
}

Turbine 库的好处

  • ✅ 简洁的 API,易于理解
  • ✅ 自动处理时间控制
  • ✅ 支持多个 Flow 的并行测试

🚀 快速参考:常用代码片段

场景 1:加载列表(带加载状态)

kotlin 复制代码
sealed class UiState<T> {
    class Loading<T> : UiState<T>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error<T>(val message: String) : UiState<T>()
}

class ListViewModel : ViewModel() {

    fun loadItems(): Flow<UiState<List<Item>>> = flow {
        emit(UiState.Loading())
        try {
            val items = apiService.getItems()
            emit(UiState.Success(items))
        } catch (e: Exception) {
            emit(UiState.Error(e.message ?: "未知错误"))
        }
    }
}

// UI 层
viewModel.loadItems().collect { state ->
    when (state) {
        is UiState.Loading -> showProgressBar()
        is UiState.Success -> showItems(state.data)
        is UiState.Error -> showError(state.message)
    }
}

场景 2:搜索 + 分页

kotlin 复制代码
fun searchItems(query: Flow<String>, page: Int): Flow<List<Item>> = query
    .debounce(300)
    .distinctUntilChanged()
    .flatMapLatest { searchTerm ->
        flow {
            val results = apiService.search(searchTerm, page)
            emit(results)
        }
    }
    .catch { emitAll(emptyFlow()) }

场景 3:定时任务

scss 复制代码
fun startHeartbeat(): Flow<Long> = flow {
    while (true) {
        emit(System.currentTimeMillis())
        delay(5000)  // 每 5 秒发送一次心跳
    }
}

// 使用
viewModel.startHeartbeat()
    .take(10)  // 只取前 10 个
    .collect { timestamp ->
        sendHeartbeatToServer(timestamp)
    }

📱 在 Android 项目中集成 Flow 的最佳实践

项目架构推荐

kotlin 复制代码
app/
├── ui/
│   ├── activities/
│   │   └── MainActivity.kt
│   ├── fragments/
│   └── viewmodels/
│       └── UserViewModel.kt  ← 使用 Flow 和 StateFlow
├── data/
│   ├── api/
│   │   └── ApiService.kt  ← 返回 Flow 或 suspend fun
│   ├── database/
│   │   └── UserDao.kt  ← 返回 Flow
│   ├── repository/
│   │   └── UserRepository.kt  ← 组合多个 Flow
│   └── models/
│       └── User.kt
└── di/
    └── AppModule.kt  ← 依赖注入

各层的职责

kotlin 复制代码
// 📡 API 层(网络请求)
interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: String): User
}

// 🗂️ DAO 层(数据库)
@Dao
interface UserDao {
    @Query("SELECT * FROM users WHERE id = :id")
    fun getUser(id: String): Flow<User>
}

// 📦 Repository 层(数据聚合)
class UserRepository(
    private val apiService: ApiService,
    private val userDao: UserDao
) {
    fun getUser(id: String): Flow<User> = flow {
        try {
            // 优先返回本地数据
            userDao.getUser(id).collect { localUser ->
                emit(localUser)
            }

            // 然后从网络更新
            val remoteUser = apiService.getUser(id)
            userDao.insertUser(remoteUser)  // 更新本地
            emit(remoteUser)
        } catch (e: Exception) {
            throw e
        }
    }
}

// 🎯 ViewModel 层(UI 数据管理)
class UserViewModel(private val repository: UserRepository) : ViewModel() {

    fun fetchUser(id: String): Flow<User> = repository.getUser(id)
        .catch { exception ->
            println("加载失败: ${exception.message}")
            emitAll(emptyFlow())
        }
}

// 📱 UI 层(展示)
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.fetchUser("123").collect { user ->
                    updateUI(user)
                }
            }
        }
    }
}

💼 团队协作建议

1. 统一规范

kotlin 复制代码
// ❌ 不同的返回方式,容易混乱
fun getUserA(): User { }                    // 同步
fun getUserB(callback: (User) -> Unit) { } // 回调
fun getUserC(): Flow<User> { }             // Flow

// ✅ 统一用 Flow(或 suspend)
suspend fun getUser(id: String): User { }  // 网络请求
fun getUser(id: String): Flow<User> { }   // 数据库/长时间任务

建议

  • 网络请求用 suspend 函数
  • 数据库查询返回 Flow
  • 长时间任务用 Flow
  • ViewModel 暴露 StateFlow<T>Flow<T>

2. Code Review 检查清单

arduino 复制代码
□ Flow 有没有被订阅(collect)?
□ 异常处理完了吗(catch)?
□ 在正确的线程执行吗(flowOn)?
□ ViewModel 销毁会自动取消吗(viewModelScope)?
□ 重复订阅会导致重复执行吗?
□ 单元测试覆盖了吗?

3. 常见的代码评审问题

scss 复制代码
// ❌ Review 驳回:没有异常处理
viewModel.fetchUser("123").collect { user ->
    updateUI(user)
}

// ✅ Review 通过:加上异常处理
viewModel.fetchUser("123")
    .catch { exception ->
        showError(exception.message)
    }
    .collect { user ->
        updateUI(user)
    }

---

// ❌ Review 驳回:没有指定线程
val users = databaseDao.getAllUsers()
    .collect { users ->
        updateUI(users)  // 可能在 IO 线程!
    }

// ✅ Review 通过:指定了线程
val users = databaseDao.getAllUsers()
    .flowOn(Dispatchers.IO)  // 数据库操作放在 IO 线程
    .collect { users ->
        updateUI(users)  // 自动切回主线程
    }

📚 推荐学习资源

官方资源

深入学习

测试工具

  • Turbine:Flow 测试库
  • kotlinx-coroutines-test:协程测试框架

🔥 核心速记表

Flow 操作符一览

scss 复制代码
创建: flow { }, flowOf(), MutableSharedFlow(), MutableStateFlow()
转换: map(), filter(), transform()
合并: zip(), combine(), flatMapConcat(), flatMapLatest()
处理: catch(), retry(), retryWhen(), onCompletion()
限流: debounce(), throttle(), distinctUntilChanged()
消费: collect(), launchIn(), collectAsState()

快速决策

问题 答案
每次订阅都要重新执行吗? 是 → Flow,否 → StateFlow/SharedFlow
需要维护当前状态吗? 是 → StateFlow,否 → SharedFlow
担心网络请求失败吗? .catch { }
线程管理问题吗? .flowOn(Dispatchers.IO)
需要重试吗? .retry(n).retryWhen { }

❓ FAQ(常见问题)

Q1: Flow 会比 LiveData 多占用多少内存?

A: 基本没区别,Flow 更轻量。区别在于功能强大程度。

Q2: 什么时候该用 suspend 函数而不是 Flow?

A:

  • 一次性网络请求 → suspend fun
  • 持续监听数据库/数据源 → Flow<T>

Q3: 如何取消一个正在执行的 Flow?

A : 使用 viewModelScope 会自动取消,或手动调用 cancel()

Q4: Flow 对性能有影响吗?

A: 反而更优化!避免了多层回调和线程频繁切换。

Q5: 老项目全是 RxJava,要全部迁移吗?

A: 不用全部迁移,新代码用 Flow,逐步替换即可。


🎯 总结:为什么 Flow 是未来

特性 RxJava LiveData Flow
学习曲线 陡 😰 平 😊 平 😊
功能强大 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐
与 Coroutine 融合 中等 ✅ 完美
官方推荐 渐淡 ✅ 重点
心智成本

结论

Flow 是 Kotlin 异步编程的未来。不仅功能强大,而且与官方 Coroutines 完美配套。现在是学习 Flow 的最好时机。

祝你 Flow 学习之旅顺利!

一旦掌握 Flow,你会发现异步编程原来可以这么优雅。

相关推荐
雨白3 小时前
Kotlin Flow 入门:构建响应式异步数据流
android·kotlin
阿里云云原生4 小时前
告别手动埋点!Android 无侵入式数据采集方案深度解析
android·云原生
Tlaster4 小时前
使用KMP实现原生UI + Compose混合的社交客户端
android·ios·开源
袁煦丞 cpolar内网穿透实验室5 小时前
安卓旧机变服务器,KSWEB部署Typecho博客并实现远程访问:cpolar内网穿透实验室第645个成功挑战
android·运维·服务器·远程工作·内网穿透·cpolar
游戏开发爱好者85 小时前
iOS 26 App 查看电池寿命技巧,多工具组合实践指南
android·macos·ios·小程序·uni-app·cocoa·iphone
用户41659673693555 小时前
基于Jetpack Compose 实现列表嵌套滚动联动机制 (完整源码解析)
android
林栩link5 小时前
【车载Android】使用自定义插件实现多语言自动化适配
android
消失的旧时光-194310 小时前
Flutter 响应式 + Clean Architecture / MVU 模式 实战指南
android·flutter·架构
404未精通的狗10 小时前
(数据结构)栈和队列
android·数据结构