【Jetpack 内核实战 02】ViewModel:生命周期管理与数据持久化核心实战

ViewModel:生命周期管理与数据持久化核心实战

在上一篇中,我们完成了 Jetpack 专栏的环境搭建与工程初始化,统一了 Kotlin + Gradle Kotlin DSL + Version Catalog 的现代开发规范。从本篇开始,我们将正式进入核心组件的深度实战。

屏幕旋转一下数据就全丢了?onSaveInstanceState 只能存点基本类型?如果你还在被这些问题困扰,那么 ViewModel 就是你要找的解药。本文将带你从原理到源码彻底吃透 ViewModel,亲手解决配置变更数据丢失的经典痛点,并掌握 ViewModelFactory 与 Fragment 间数据共享的标准写法,所有代码均采用地道 Kotlin 规范且经过编译验证。


一、ViewModel 诞生背景:为何我们需要它?

在 ViewModel 出现之前,Android 开发者面对配置变更(屏幕旋转、语言切换、分屏模式、字体缩放等)导致的数据丢失问题,主要有两种传统应对方式,但都存在明显缺陷。

1. 传统方案一:onSaveInstanceState + onRestoreInstanceState

这是系统提供的原生方案,但它的能力边界非常有限:

  • 只能存储基本类型和可序列化的轻量对象,无法保存 List 集合、Bitmap、正在执行的网络请求与耗时任务
  • 数据存取需要经历序列化 / 反序列化过程,存在性能开销
  • 样板代码多,复杂状态管理起来非常繁琐

它只能作为页面状态的兜底备份,无法承担核心业务数据的持久化。

2. 传统方案二:静态变量 / 单例手动暂存

部分开发者会用静态变量或全局单例来暂存页面数据,但弊端同样明显:

  • 无法感知界面生命周期,数据清理完全依赖手动管理,极易造成内存泄漏
  • 多页面复用容易出现数据错乱,状态管理混乱
  • 不符合架构解耦的设计思想,代码可维护性差

3. ViewModel 的解决方案

ViewModel 被设计用来以生命周期感知的方式存储和管理 UI 相关数据。它的核心价值在于:让数据脱离 Activity/Fragment 的生命周期而存在,当配置变更发生时,ViewModel 实例不会被销毁,数据自然也就不会丢失。

同时 ViewModel 配合官方架构规范,可以将业务逻辑从界面代码中剥离,从根源上降低内存泄漏风险,提升代码可维护性。


二、ViewModel 生命周期详解

理解 ViewModel 的生命周期,是正确使用它的第一步,也是区分它与普通数据类的核心。

1. 生命周期范围

ViewModel 的生命周期与它绑定的 ViewModelStoreOwner(通常是 Activity 或 Fragment)的生命周期并不一致,完整分为三个阶段:

阶段 触发时机 对应行为
创建 首次通过 ViewModelProvider 请求 ViewModel 时 执行构造函数,初始化内部数据
存活 从首次请求直到绑定的 Activity/Fragment 彻底销毁 实例保持不变,持续持有数据,配置变更不会中断
销毁 Activity 被 finish / Fragment 被 detach 时 回调 onCleared() 方法,释放内部资源

核心结论:ViewModel 不会在屏幕旋转时被销毁。系统在配置变更时只是重建了 Activity 实例,但 ViewModel 的存储容器会被保留,因此数据可以无缝延续。

2. 需要警惕的特殊情况

ViewModel 并非万能。如果 App 因内存不足被系统在后台 Kill 掉,ViewModel 实例也会随之被清除,数据会丢失。

这种场景下,需要结合 SavedStateHandle 来保存关键状态,实现「进程被杀也能恢复」的兜底能力,我们会在后续进阶篇中详细讲解。


三、源码级原理剖析:ViewModel 是如何做到数据不丢失的?

光知道「怎么用」还不够,搞懂「为什么」才能让你遇到问题时游刃有余。下面我们从源码层面拆解 ViewModel 的核心运作机制。

1. 核心三剑客:ViewModelStore、ViewModelStoreOwner、ViewModelProvider

ViewModel 的整个运作体系由三个核心角色协作完成,各司其职。

① ViewModelStore ------ 存储容器

ViewModelStore 是 ViewModel 的存储容器,我们看它的核心源码(精简后):

java 复制代码
public class ViewModelStore {
    private final HashMap<String, ViewModel> mMap = new HashMap<>();
    
    final void put(String key, ViewModel viewModel) {
        mMap.put(key, viewModel);
    }
    
    final ViewModel get(String key) {
        return mMap.get(key);
    }
    
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear(); // 内部回调 onCleared()
        }
        mMap.clear();
    }
}

本质上就是一个 HashMap,以字符串为 key、ViewModel 实例为 value,负责存储和管理所有关联的 ViewModel 对象。

② ViewModelStoreOwner ------ 容器的拥有者

这是一个接口,ComponentActivity 和 Fragment 都默认实现了它。 每个 Activity / Fragment 都对应一个 Owner,持有一个独立的 ViewModelStore 配置变更时,新的界面实例会继承旧实例的 Owner 身份,关联同一个 ViewModelStore

③ ViewModelProvider ------ 创建与获取的入口

它是 ViewModel 的统一获取工具,负责「有则返回、无则创建」。我们日常写的:

kotlin 复制代码
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)

内部逻辑就是:接收 Owner → 拿到 Store → 查找实例 → 不存在则用 Factory 创建。

2. 获取 ViewModel 的完整流程

当你调用 ViewModelProvider(this).get(...) 时,背后完整执行了 5 步:

  1. 构造 Provider:传入的 Activity/Fragment 作为 Owner,Provider 从中获取 ViewModelStore 和默认工厂
  2. 查找缓存:以 ViewModel 类名为 key,从 ViewModelStore 的 HashMap 中查找已有实例
  3. 命中返回:如果找到实例,直接返回,不会重复创建
  4. 未命中创建:如果没找到,使用 Factory 通过反射创建新的 ViewModel 实例
  5. 存入缓存:将新实例存入 ViewModelStore,供后续复用

3. 为什么屏幕旋转后数据还在?

核心秘密就在 ViewModelStore 的归属上:

  • ViewModelStore 不属于某个具体的 Activity 实例,而是属于 ViewModelStoreOwner 这个「逻辑身份」
  • 屏幕旋转时,旧 Activity 被销毁,新 Activity 被创建,但系统会将新 Activity 与同一个 Owner 关联
  • 因此新 Activity 通过 Provider 获取 ViewModel 时,从同一个 Store 里拿到的还是原来的实例,数据自然完整保留

4. onCleared ():资源清理的最后防线

当绑定的 Activity/Fragment 真正销毁时,ViewModelStore 会执行 clear() 方法,遍历所有 ViewModel 并回调 onCleared()。 这是防止内存泄漏的最后一道防线,我们可以在这里:

  • 取消未完成的网络请求
  • 释放大对象引用
  • 取消监听器与订阅

注意:viewModelScope 协程作用域会在 onCleared() 时自动取消,无需手动处理。


四、前置准备:统一开发规范

按照专栏约定,我们全程使用 ViewBinding + Version Catalog + 地道 Kotlin 的开发规范,先确认基础配置。

1. 开启 ViewBinding

在 app/build.gradle.kts 中开启视图绑定:

kotlin 复制代码
android {
    namespace = "com.example.jetpacknote"
    compileSdk = 34

    buildFeatures {
        viewBinding = true
    }
    // ... 其余配置保持不变
}

2. 补充 Fragment KTX 依赖

activityViewModels() 等扩展来自 fragment-ktx,已在 Version Catalog 中统一配置:

libs.versions.toml 中已包含:

toml 复制代码
[versions]
fragment = "1.6.2"

[libraries]
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragment" }

app/build.gradle.kts 中引入:

kotlin 复制代码
dependencies {
    implementation(libs.androidx.fragment.ktx)
}

五、基础实战:计数器 + 屏幕旋转验证

理论讲完,我们动手编码。本节实现经典计数器案例,验证屏幕旋转时数据不丢失。

1. 创建计数器 ViewModel

在 viewmodel 包下创建 CounterViewModel.kt:

kotlin 复制代码
package com.example.jetpacknote.viewmodel

import androidx.lifecycle.ViewModel

class CounterViewModel : ViewModel() {

    // 对外只读、对内可写,遵循数据封装原则
    var count = 0
        private set

    fun increment() {
        count++
    }

    fun decrement() {
        count--
    }

    fun reset() {
        count = 0
    }
}

2. 编写布局文件

修改 activity_main.xml,采用垂直线性布局,包含显示文本与三个操作按钮:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="32dp">

    <TextView
        android:id="@+id/tvCounter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="64sp"
        android:textStyle="bold" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="32dp">

        <Button
            android:id="@+id/btnDecrement"
            android:layout_width="80dp"
            android:layout_height="56dp"
            android:text="-" />

        <Button
            android:id="@+id/btnReset"
            android:layout_width="80dp"
            android:layout_height="56dp"
            android:layout_marginStart="16dp"
            android:text="重置" />

        <Button
            android:id="@+id/btnIncrement"
            android:layout_width="80dp"
            android:layout_height="56dp"
            android:layout_marginStart="16dp"
            android:text="+" />
    </LinearLayout>

</LinearLayout>

3. Activity 中使用 ViewModel + ViewBinding

修改 MainActivity.kt,采用 KTX 属性委托 + ViewBinding 标准写法:

kotlin 复制代码
package com.example.jetpacknote

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.viewModels
import com.example.jetpacknote.databinding.ActivityMainBinding
import com.example.jetpacknote.viewmodel.CounterViewModel

class MainActivity : AppCompatActivity() {

    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    private val viewModel: CounterViewModel by viewModels()

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

        initClickListeners()
        updateCounterDisplay()
    }

    private fun initClickListeners() = with(binding) {
        btnIncrement.setOnClickListener {
            viewModel.increment()
            updateCounterDisplay()
        }
        btnDecrement.setOnClickListener {
            viewModel.decrement()
            updateCounterDisplay()
        }
        btnReset.setOnClickListener {
            viewModel.reset()
            updateCounterDisplay()
        }
    }

    private fun updateCounterDisplay() {
        binding.tvCounter.text = viewModel.count.toString()
    }
}

4. 验证屏幕旋转数据保留

运行 App,点击「+」将计数增加到任意值(比如 5),然后旋转屏幕:

  • ✅ 预期结果:数值保持不变,没有重置为 0
  • ❌ 异常排查:如果数据丢失,检查是否直接 CounterViewModel() 手动创建实例,必须通过 viewModels() 委托获取

六、进阶实战:带参 ViewModel 与 Factory 封装

实际项目中,ViewModel 经常需要接收外部参数,比如 Intent 传入的 ID、Repository 依赖、初始值配置。默认工厂只能创建无参实例,带参场景必须自定义 ViewModelProvider.Factory。

1. 为什么需要自定义 Factory

ViewModel 的实例由 Provider 通过反射创建,默认工厂只会调用无参构造函数。如果构造函数带参数,直接使用 by viewModels() 会抛出 NoSuchMethodException 运行时异常。 自定义 Factory 的作用就是:接管 ViewModel 的创建逻辑,手动传入构造参数。

2. 自定义 Factory 标准实现

步骤 1:创建带参 ViewModel

kotlin 复制代码
package com.example.jetpacknote.viewmodel

import androidx.lifecycle.ViewModel

class ParamCounterViewModel(
    private val initialCount: Int
) : ViewModel() {

    var count = initialCount
        private set

    fun increment() { count++ }
    fun decrement() { count-- }
    fun reset() { count = initialCount }
}

步骤 2:自定义 ViewModelFactory

kotlin 复制代码
package com.example.jetpacknote.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

class ParamCounterViewModelFactory(
    private val initialCount: Int
) : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T = when {
        modelClass.isAssignableFrom(ParamCounterViewModel::class.java) ->
            ParamCounterViewModel(initialCount) as T
        else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
    }
}

步骤 3:Activity 中使用

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    
    // 传入工厂,指定初始值为 10
    private val viewModel: ParamCounterViewModel by viewModels {
        ParamCounterViewModelFactory(10)
    }

    // ... 其余逻辑不变
}

3. 进阶提示:CreationExtras 新写法

从 lifecycle-viewmodel 2.5.0 开始,Google 推荐使用 CreationExtras 方式传递参数,类型更安全、写法更简洁。 考虑到入门学习成本,本专栏前期以 Factory 标准写法为主,后续进阶篇会详细讲解 CreationExtras 的企业级用法。


七、场景实战:Fragment 间共享数据

多个 Fragment 共享数据是高频开发需求。传统做法通过 Activity 中转、接口回调层层传递,代码臃肿且难以维护。ViewModel 提供了更优雅的方案:同一 Activity 下的多个 Fragment 共享同一个 Activity 级 ViewModel 实例。

1. 两种作用域委托的区别

委托方式 作用域 适用场景
by viewModels() 当前 Fragment 自身 管理 Fragment 私有状态
by activityViewModels() 所属 Activity 同一 Activity 下多 Fragment 共享数据

⚠️ 注意:如果 ViewModel 是带参构造,不能直接用 by activityViewModels(),否则默认无参工厂无法创建实例,会导致崩溃。下文采用全版本兼容的显式传工厂写法,可直接运行。

2. 完整实战实现

我们实现两个 Fragment:SenderFragment 发送消息,ReceiverFragment 接收并展示,通过共享 ViewModel 通信。

步骤 1:创建共享 ViewModel

kotlin 复制代码
package com.example.jetpacknote.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class SharedDataViewModel : ViewModel() {

    // 私有可变,外部只暴露不可变 LiveData,保证数据流向唯一
    private val _message = MutableLiveData<String>()
    val message: LiveData<String> = _message

    fun sendMessage(content: String) {
        _message.value = content
    }

    fun clearMessage() {
        _message.value = null
    }
}

步骤 2:Activity 暴露工厂实例

Activity 作为宿主,统一提供工厂实例,保证所有 Fragment 使用同一个 ViewModel:

kotlin 复制代码
package com.example.jetpacknote

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.viewModels
import com.example.jetpacknote.databinding.ActivityMainBinding
import com.example.jetpacknote.viewmodel.SharedDataViewModel
import com.example.jetpacknote.viewmodel.SharedDataViewModelFactory

class MainActivity : AppCompatActivity() {

    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

    // 公开工厂,供子 Fragment 复用
    val sharedViewModelFactory = SharedDataViewModelFactory()

    private val sharedViewModel: SharedDataViewModel by viewModels { sharedViewModelFactory }

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

说明:本例 ViewModel 无参,工厂仅做演示;带参场景下,所有 Fragment 都会复用 Activity 中的同一个工厂实例,确保数据一致。

步骤 3:SenderFragment 发送端

kotlin 复制代码
package com.example.jetpacknote.fragment

import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.example.jetpacknote.MainActivity
import com.example.jetpacknote.R
import com.example.jetpacknote.databinding.FragmentSenderBinding
import com.example.jetpacknote.viewmodel.SharedDataViewModel

class SenderFragment : Fragment(R.layout.fragment_sender) {

    private var _binding: FragmentSenderBinding? = null
    private val binding get() = _binding!!

    // 显式传入 Activity 中的工厂,保证实例唯一
    private val sharedViewModel: SharedDataViewModel by activityViewModels {
        (requireActivity() as MainActivity).sharedViewModelFactory
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        _binding = FragmentSenderBinding.bind(view)

        binding.btnSend.setOnClickListener {
            val content = binding.etMessage.text.toString().trim()
            if (content.isNotEmpty()) {
                sharedViewModel.sendMessage(content)
                binding.etMessage.text?.clear()
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

步骤 4:ReceiverFragment 接收端

kotlin 复制代码
package com.example.jetpacknote.fragment

import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.example.jetpacknote.MainActivity
import com.example.jetpacknote.R
import com.example.jetpacknote.databinding.FragmentReceiverBinding
import com.example.jetpacknote.viewmodel.SharedDataViewModel

class ReceiverFragment : Fragment(R.layout.fragment_receiver) {

    private var _binding: FragmentReceiverBinding? = null
    private val binding get() = _binding!!

    private val sharedViewModel: SharedDataViewModel by activityViewModels {
        (requireActivity() as MainActivity).sharedViewModelFactory
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        _binding = FragmentReceiverBinding.bind(view)

        // 观察数据变化,自动更新 UI
        sharedViewModel.message.observe(viewLifecycleOwner) { message ->
            binding.tvReceivedMessage.text = message ?: "暂无消息"
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

步骤 5:布局引入两个 Fragment

在 activity_main.xml 中通过 FragmentContainerView 加载两个 Fragment 即可,运行后发送消息,接收端会自动更新,旋转屏幕数据不丢失。


八、AndroidViewModel:需要 Context 时的正确选择

很多开发者会疑惑:ViewModel 不能持有 Context,但业务中确实需要用到 Context(比如读资源、取系统服务),该怎么办?答案是使用 AndroidViewModel。

1. ViewModel 与 AndroidViewModel 对比

特性 ViewModel AndroidViewModel
构造参数 无参 接收 Application 实例
是否持有 Context 否 持有 Application Context
生命周期 跟随 Owner 跟随 Owner,Application 无泄漏风险
适用场景 绝大多数业务场景 需要访问系统服务、全局资源时

2. 使用示例

kotlin 复制代码
import android.app.Application
import android.content.Context
import androidx.lifecycle.AndroidViewModel

class MyAndroidViewModel(application: Application) : AndroidViewModel(application) {

    private val prefs = application.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)

    fun saveData(key: String, value: String) {
        prefs.edit().putString(key, value).apply()
    }
}

3. 重要红线

  • ✅ 允许:持有 Application Context(全局生命周期,不会造成泄漏)
  • ❌ 禁止:持有 Activity Context、View、Fragment 引用(生命周期不一致,必然泄漏)

九、项目预集成:记事本 NoteViewModel 初版

结合专栏的记事本实战项目,我们提前搭建笔记列表 ViewModel 的骨架,为后续 Room 数据库接入做准备:

kotlin 复制代码
package com.example.jetpacknote.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

/**
 * 笔记列表 UI 状态封装
 */
data class NoteUiState(
    val notes: List<String> = emptyList(), // 后续替换为 Note 实体类
    val isLoading: Boolean = false,
    val error: String? = null
)

class NoteViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(NoteUiState())
    val uiState: StateFlow<NoteUiState> = _uiState.asStateFlow()

    /**
     * 加载笔记列表
     * 目前模拟数据,后续对接 Room 数据库
     */
    fun loadNotes() {
        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(isLoading = true)
            delay(1000) // 模拟网络/数据库耗时
            _uiState.value = _uiState.value.copy(
                isLoading = false,
                notes = listOf("示例笔记 1", "示例笔记 2", "示例笔记 3")
            )
        }
    }

    /**
     * 添加笔记
     */
    fun addNote(content: String) {
        val currentNotes = _uiState.value.notes.toMutableList()
        currentNotes.add(content)
        _uiState.value = _uiState.value.copy(notes = currentNotes)
    }
}

这里我们先用 StateFlow 做状态管理,下一篇讲解 LiveData 后会同步提供 LiveData 版本的实现,并讲解两者的选型与迁移。


十、FAQ / 避坑指南

Q1:ViewModel 为何能跨越配置变更保留数据?

A:ViewModel 实例存储在 ViewModelStore 中,而 Store 归属于 ViewModelStoreOwner(逻辑身份),而非 Activity 实例本身。配置变更时,系统重建 Activity 实例,但新实例关联的是同一个 Owner,因此从 Store 中取出的仍是原来的 ViewModel,数据自然保留。

Q2:ViewModel 为什么禁止持有 Context / View 引用?

A:因为 ViewModel 的生命周期长于 Activity/Fragment。配置变更时 Activity 销毁,但 ViewModel 依然存活,如果持有界面引用,会阻止 Activity 被 GC 回收,造成内存泄漏。 必须用 Context 时请使用 AndroidViewModel,它持有的 Application Context 生命周期与 App 一致,无泄漏风险。

Q3:Fragment 共享带参 ViewModel 直接崩溃怎么办?

A:崩溃原因是 by activityViewModels() 默认使用系统无参工厂,无法创建带参实例。 正确做法:在 Activity 中定义公开的工厂实例,所有 Fragment 获取 ViewModel 时显式传入该工厂,确保大家使用同一个工厂、同一个实例。

Q4:onCleared () 什么时候调用?里面该做什么?

A:只有当 Activity 被 finish / Fragment 被 detach 时才会调用,屏幕旋转不会触发。 推荐在里面做:取消未完成的网络请求、释放大对象、清理监听器订阅。注意 viewModelScope 会自动取消,无需手动处理。

Q5:ViewModel 可以做全局跨页面数据共享吗?

A:不推荐。ViewModel 与 Owner 一一绑定,不同 Activity 对应不同的 Store,无法共享实例。全局数据共享应该使用 Repository 单例、全局数据仓库等方案。


写在最后

本篇我们从背景、生命周期、源码原理、基础实战、带参工厂、Fragment 共享、AndroidViewModel 到项目预集成,系统地吃透了 ViewModel 的核心知识与企业级用法。

但目前的实现中,数据变化后还需要手动刷新 UI,还没有真正实现「数据驱动 UI」。下一篇我们将正式进入 LiveData 的学习,探索响应式编程的核心优势,重点攻克粘性事件、数据倒灌等经典痛点,让 ViewModel 中的数据变化可以自动、安全地更新界面。

🌟 点赞 + 收藏 + 关注,第一时间收到更新推送 💬 评论区留言:你在使用 ViewModel 时遇到过哪些坑?或者对 LiveData 有什么疑问?欢迎留言交流!

相关推荐
Ehtan_Zheng1 小时前
Kotlin 伴生块:Android 项目怎么用,迁移时要注意什么
kotlin
撩得Android一次心动8 小时前
Jetpack Compose 知识点整理3【个人用】
android·笔记·学习·kotlin·android compose
Kapaseker8 小时前
别再只会写 <T>:Kotlin 泛型的实战用法
android·kotlin
alexhilton1 天前
Room Fts4:全文搜索替代 SQL LIKE
android·kotlin·android jetpack
hai_android1 天前
把 Android App 打包成系统应用(System App)
android·kotlin·android jetpack
JMchen1 天前
【Jetpack 内核实战 01】开篇:Jetpack 组件全家桶核心逻辑与专栏完整规划
android·kotlin·android jetpack
mmsx1 天前
Android 静默失败实录:五个没有日志的线上事故
android·kotlin
hai_android1 天前
从零实现一个支持跨进程(IPC)的自定义 Toast
android·kotlin·android jetpack