<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Architecture on zed的博客</title><link>https://www.dust-zed.site/android/architecture/</link><description>Recent content in Architecture on zed的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Fri, 02 Jan 2026 21:00:00 +0800</lastBuildDate><atom:link href="https://www.dust-zed.site/android/architecture/index.xml" rel="self" type="application/rss+xml"/><item><title>从 MVVM 到 MVI：架构的「意图驱动」演进</title><link>https://www.dust-zed.site/android/mvi-consumer-driven/</link><pubDate>Fri, 02 Jan 2026 21:00:00 +0800</pubDate><guid>https://www.dust-zed.site/android/mvi-consumer-driven/</guid><description>&lt;p>在云游戏客户端的开发初期，我们采用了经典的 &lt;strong>MVVM (Model-View-ViewModel)&lt;/strong> 架构。然而，随着业务复杂度的指数级上升——虚拟手柄状态、WebRTC 信令状态、视频流媒体状态、网络质量监控等多数据源的交织，MVVM 逐渐显露出疲态。&lt;/p>
&lt;p>State 分散在多个 &lt;code>LiveData/StateFlow&lt;/code> 中，View 层充斥着各种状态的监听和组合逻辑，&amp;ldquo;状态&amp;quot;变得难以追踪和预测。这促使我们向 &lt;strong>MVI (Model-View-Intent)&lt;/strong> 架构迈出了关键的一步。&lt;/p>
&lt;h2 id="核心结论">核心结论
&lt;/h2>&lt;ol>
&lt;li>MVVM 在状态少时足够轻量，但复杂页面容易出现状态分散和监听组合混乱。&lt;/li>
&lt;li>MVI 的关键是把用户操作抽象成 &lt;code>Intent&lt;/code>，把页面渲染抽象成单一 &lt;code>State&lt;/code>。&lt;/li>
&lt;li>一次性事件不要塞进 State，应通过 SideEffect 单独处理。&lt;/li>
&lt;li>Consumer-Driven Design 的重点是先设计上层想要的接口，再倒逼底层实现。&lt;/li>
&lt;li>架构升级的目标不是换名词，而是让状态流向更可预测。&lt;/li>
&lt;/ol>
&lt;h2 id="1-核心概念心智模型的重构">1. 核心概念：心智模型的重构
&lt;/h2>&lt;p>MVI 并不仅仅是一个新的架构缩写，它本质上是一种&lt;strong>心智模型 (Mental Model)&lt;/strong> 的重构。我们将 UI 交互抽象为一种 &lt;strong>&amp;ldquo;请求-响应&amp;rdquo; (Request-Response)&lt;/strong> 机制。&lt;/p>
&lt;h3 id="intent--request-意图即请求">Intent ≈ Request (意图即请求)
&lt;/h3>&lt;p>在 MVVM 中，View 直接调用 ViewModel 的方法（例如 &lt;code>vm.playGame()&lt;/code>）。而在 MVI 中，我们将这些操作封装为 &lt;strong>Intent&lt;/strong> 数据结构。View 不再发号施令，而是&lt;strong>表达意图&lt;/strong>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 定义用户意图
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">sealed&lt;/span> &lt;span class="k">class&lt;/span> &lt;span class="nc">GameIntent&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">data&lt;/span> &lt;span class="k">object&lt;/span> &lt;span class="nc">Start&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">GameIntent&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">data&lt;/span> &lt;span class="k">object&lt;/span> &lt;span class="nc">Pause&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">GameIntent&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">data&lt;/span> &lt;span class="k">class&lt;/span> &lt;span class="nc">ChangeQuality&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">val&lt;/span> &lt;span class="py">quality&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">VideoQuality&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">GameIntent&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">data&lt;/span> &lt;span class="k">class&lt;/span> &lt;span class="nc">TouchEvent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">val&lt;/span> &lt;span class="py">x&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Float&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">val&lt;/span> &lt;span class="py">y&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Float&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">GameIntent&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// ViewModel 处理意图
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">fun&lt;/span> &lt;span class="nf">dispatch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">intent&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">GameIntent&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">when&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">intent&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">is&lt;/span> &lt;span class="nc">GameIntent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Start&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="n">handleStart&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">is&lt;/span> &lt;span class="nc">GameIntent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ChangeQuality&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="n">updateStreamQuality&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">intent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">quality&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="state--response-状态即响应">State ≈ Response (状态即响应)
&lt;/h3>&lt;p>ViewModel 经过处理后，不是分散地更新变量，而是发射一个新的、不可变的 &lt;strong>State&lt;/strong> 对象。这就像服务器返回了一个完整的 HTML/JSON 响应。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 定义单一状态源 (Single Source of Truth)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">data&lt;/span> &lt;span class="k">class&lt;/span> &lt;span class="nc">GameUiState&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">isPlaying&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Boolean&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">isLoading&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Boolean&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">streamQuality&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">VideoQuality&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nc">VideoQuality&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">H720P&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">debugInfo&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">String&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">error&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Throwable&lt;/span>&lt;span class="p">?&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">null&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这种 &lt;code>Intent -&amp;gt; ViewModel -&amp;gt; State&lt;/code> 的单向数据流 (Unidirectional Data Flow)，使得数据的流向清晰可追溯，极大地降低了调试难度。&lt;/p>
&lt;h2 id="2-sideeffect处理那些一次性的烦恼">2. SideEffect：处理那些&amp;quot;一次性&amp;quot;的烦恼
&lt;/h2>&lt;p>在 Android 开发中，一个经典的问题是：&lt;strong>&amp;ldquo;屏幕旋转后，Toast 重复弹出&amp;rdquo;&lt;/strong>。
这是因为 Toast 消息往往被包含在 State 中，View 重建后重新订阅 State，再次收到了包含旧错误信息的 State。&lt;/p>
&lt;p>MVI 引入了 &lt;strong>SideEffect (副作用)&lt;/strong> 概念来解决这个问题。SideEffect 专门用于处理&lt;strong>一次性事件&lt;/strong>，如弹 Toast、页面导航、播放音效等。&lt;/p>
&lt;p>我们通常使用 Channel 来实现 SideEffect，因为 Channel 具有&amp;quot;热流&amp;quot;且&amp;quot;消费后即消失&amp;quot;的特性。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">sealed&lt;/span> &lt;span class="k">class&lt;/span> &lt;span class="nc">GameSideEffect&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">data&lt;/span> &lt;span class="k">class&lt;/span> &lt;span class="nc">ShowToast&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">val&lt;/span> &lt;span class="py">message&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">String&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">GameSideEffect&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">data&lt;/span> &lt;span class="k">object&lt;/span> &lt;span class="nc">NavigateToHome&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">GameSideEffect&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// ViewModel
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">private&lt;/span> &lt;span class="k">val&lt;/span> &lt;span class="py">_effect&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">Channel&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">GameSideEffect&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="nc">Channel&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">BUFFERED&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">val&lt;/span> &lt;span class="py">effect&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">_effect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">receiveAsFlow&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 发送副作用
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">viewModelScope&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">launch&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_effect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nc">GameSideEffect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ShowToast&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;连接断开&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// UI 层消费
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">LaunchedEffect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Unit&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">viewModel&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">effect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">collect&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">effect&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">when&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">effect&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">is&lt;/span> &lt;span class="nc">GameSideEffect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ShowToast&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nc">Toast&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">makeText&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">effect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">..&lt;/span>&lt;span class="p">.).&lt;/span>&lt;span class="n">show&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这彻底解决了 MVVM 中 LiveData 倒灌 (&amp;ldquo;Sticky Event&amp;rdquo;) 的老大难问题。&lt;/p>
&lt;h2 id="3-消费者驱动设计-consumer-driven-design">3. 消费者驱动设计 (Consumer-Driven Design)
&lt;/h2>&lt;p>在重构过程中，我深刻体会到了 &lt;strong>Consumer-Driven Design&lt;/strong> 的威力。这是一种&lt;strong>反直觉&lt;/strong>的开发哲学。&lt;/p>
&lt;h3 id="错误姿势从底向上">错误姿势：从底向上
&lt;/h3>&lt;p>传统的开发往往是：&lt;/p>
&lt;ol>
&lt;li>先看底层 SDK 提供了什么 API。&lt;/li>
&lt;li>写 Repository 封装 SDK。&lt;/li>
&lt;li>写 ViewModel 调用 Repository。&lt;/li>
&lt;li>写 UI 适配 ViewModel。&lt;/li>
&lt;/ol>
&lt;p>这种方式导致 UI 层总是受制于底层的实现细节，代码充满了胶水逻辑。&lt;/p>
&lt;h3 id="正确姿势从顶向下">正确姿势：从顶向下
&lt;/h3>&lt;p>Consumer-Driven 主张：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>先写业务层 (Activity/ViewModel)&lt;/strong>：假装有一个完美的底层引擎接口。&lt;/li>
&lt;li>&lt;strong>设计最爽的调用方式&lt;/strong>：根据 UI 的交互需求，定义出最自然、最符合业务语义的 &lt;code>Intent&lt;/code> 和 &lt;code>State&lt;/code>。&lt;/li>
&lt;li>&lt;strong>倒逼底层实现&lt;/strong>：再去写 Repository 和 Engine 层，去实现这个&amp;quot;完美的接口&amp;rdquo;。&lt;/li>
&lt;/ol>
&lt;p>例如，在云游戏场景中，我们需要一个&amp;quot;切换码率&amp;quot;的功能。
我先在 ViewModel 中写下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">engine&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">setBitrate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">5000&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 假设接口是这样，简单明了
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后再去底层实现这个 &lt;code>setBitrate&lt;/code>。如果发现底层 SDK 需要复杂的协商流程（暂停流 -&amp;gt; 发送重协商信令 -&amp;gt; 等待响应 -&amp;gt; 恢复流），我就在 Repository 层把这些脏活累活封装掉，&lt;strong>绝不让这些实现细节泄露到 ViewModel 层&lt;/strong>。&lt;/p>
&lt;p>这种&amp;quot;假装编程&amp;quot;的方法，确保了上层代码的纯净和高可读性，让架构真正服务于业务，而不是服务于底层库。&lt;/p>
&lt;h2 id="总结">总结
&lt;/h2>&lt;p>从 MVVM 到 MVI，不仅是架构的升级，更是思维方式的转变。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Intent&lt;/strong> 让我们关注&amp;quot;用户想做什么&amp;quot;，而不是&amp;quot;如何操作对象&amp;quot;。&lt;/li>
&lt;li>&lt;strong>State&lt;/strong> 让我们关注&amp;quot;当前是什么样子&amp;quot;，而不是&amp;quot;哪些变量变了&amp;quot;。&lt;/li>
&lt;li>&lt;strong>Consumer-Driven&lt;/strong> 让我们关注&amp;quot;业务需要什么&amp;quot;，而不是&amp;quot;底层提供了什么&amp;quot;。&lt;/li>
&lt;/ul>
&lt;p>这套组合拳，成为了驾驭云游戏这种高复杂度客户端应用的坚实基石。&lt;/p></description></item><item><title>Now in Android：现代 Android 应用架构实践</title><link>https://www.dust-zed.site/android/now-in-android-practices/</link><pubDate>Sun, 09 Nov 2025 10:45:37 +0800</pubDate><guid>https://www.dust-zed.site/android/now-in-android-practices/</guid><description>&lt;p>Now in Android 不只是一个 Compose 示例，它更像是一份现代 Android 工程样板：UI、数据流、模块化、依赖注入、离线优先和 Gradle 构建组织都能在里面找到参考。&lt;/p>
&lt;h2 id="核心结论">核心结论
&lt;/h2>&lt;ol>
&lt;li>UI 层只消费 &lt;code>UiState&lt;/code>，用户操作通过事件向上发送。&lt;/li>
&lt;li>数据层通过 Repository 屏蔽网络、本地数据库和同步策略。&lt;/li>
&lt;li>模块化结构让 feature 与 core 边界清晰，避免功能模块互相依赖。&lt;/li>
&lt;li>Hilt 负责对象装配，但架构稳定性来自清晰的接口边界。&lt;/li>
&lt;li>&lt;code>build-logic&lt;/code> 和 Version Catalog 是大型项目保持构建一致性的关键。&lt;/li>
&lt;/ol>
&lt;h2 id="响应式架构">响应式架构
&lt;/h2>&lt;p>Now in Android 的核心心智模型是：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Repository / UseCase
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; Flow
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; ViewModel
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; UiState
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; Compose UI
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>UI 不主动拉取数据，而是观察状态流。状态变化后，Compose 自动重组对应区域。&lt;/p>
&lt;p>值得学习的点：&lt;/p>
&lt;ol>
&lt;li>&lt;code>StateFlow&lt;/code> 暴露稳定的 UI 状态；&lt;/li>
&lt;li>&lt;code>combine&lt;/code> 聚合多个数据源；&lt;/li>
&lt;li>&lt;code>map&lt;/code> 把领域模型转换为 UI 模型；&lt;/li>
&lt;li>UI 只负责展示状态和发送事件；&lt;/li>
&lt;li>数据流方向稳定，调试路径清晰。&lt;/li>
&lt;/ol>
&lt;h2 id="模块化结构">模块化结构
&lt;/h2>&lt;p>典型层次：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">:app
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:feature:*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:core:*
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>职责划分：&lt;/p>
&lt;ol>
&lt;li>&lt;code>:app&lt;/code> 负责应用入口、导航容器和全局装配；&lt;/li>
&lt;li>&lt;code>:feature:*&lt;/code> 负责具体业务页面；&lt;/li>
&lt;li>&lt;code>:core:model&lt;/code> 提供基础模型；&lt;/li>
&lt;li>&lt;code>:core:data&lt;/code> 提供 Repository 实现；&lt;/li>
&lt;li>&lt;code>:core:database&lt;/code>、&lt;code>:core:network&lt;/code> 分别处理本地和网络数据源。&lt;/li>
&lt;/ol>
&lt;p>关键原则：&lt;/p>
&lt;blockquote>
&lt;p>feature 模块不要相互依赖，公共能力下沉到 core。&lt;/p>&lt;/blockquote>
&lt;p>这样可以避免功能之间形成横向耦合，也更利于并行开发和增量构建。&lt;/p>
&lt;h2 id="依赖注入">依赖注入
&lt;/h2>&lt;p>Hilt 的价值不是“省掉 new 对象”，而是把对象创建和依赖关系集中管理。&lt;/p>
&lt;p>重点理解：&lt;/p>
&lt;ol>
&lt;li>&lt;code>@HiltViewModel&lt;/code>：让 ViewModel 进入 Hilt 图；&lt;/li>
&lt;li>&lt;code>@Inject&lt;/code>：声明构造函数依赖；&lt;/li>
&lt;li>&lt;code>@Module&lt;/code>：提供无法构造注入的对象；&lt;/li>
&lt;li>&lt;code>@Binds&lt;/code>：把接口绑定到实现；&lt;/li>
&lt;li>&lt;code>@Provides&lt;/code>：手动创建对象；&lt;/li>
&lt;li>&lt;code>@InstallIn&lt;/code>：声明对象所在组件和生命周期。&lt;/li>
&lt;/ol>
&lt;p>架构上更重要的是：ViewModel 依赖接口，而不是直接依赖具体数据源。&lt;/p>
&lt;h2 id="数据层和离线优先">数据层和离线优先
&lt;/h2>&lt;p>Repository 是数据层的统一入口。它不只是简单转发网络请求，而是负责协调：&lt;/p>
&lt;ol>
&lt;li>本地数据库；&lt;/li>
&lt;li>网络接口；&lt;/li>
&lt;li>同步策略；&lt;/li>
&lt;li>缓存策略；&lt;/li>
&lt;li>数据模型转换。&lt;/li>
&lt;/ol>
&lt;p>离线优先的基本思路：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">UI 优先观察本地数据库
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; 后台同步网络数据
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; 写入数据库
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; 数据库变化自动推动 UI 更新
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这种结构的优点是：UI 响应快，弱网下仍有可用数据，数据来源也更稳定。&lt;/p>
&lt;h2 id="构建组织">构建组织
&lt;/h2>&lt;p>Now in Android 里值得重点学习 &lt;code>build-logic&lt;/code> 和 &lt;code>libs.versions.toml&lt;/code>。&lt;/p>
&lt;p>&lt;code>build-logic&lt;/code> 解决重复 Gradle 配置：&lt;/p>
&lt;ol>
&lt;li>Android application/library 公共配置；&lt;/li>
&lt;li>Kotlin 编译配置；&lt;/li>
&lt;li>Compose 配置；&lt;/li>
&lt;li>Hilt 配置；&lt;/li>
&lt;li>测试配置。&lt;/li>
&lt;/ol>
&lt;p>Version Catalog 解决依赖版本集中管理，让多模块项目不用到处硬编码版本号。&lt;/p>
&lt;h2 id="回看清单">回看清单
&lt;/h2>&lt;ol>
&lt;li>现代 Android 架构重点是稳定数据流和清晰模块边界。&lt;/li>
&lt;li>Compose 只是 UI 表达方式，真正的复杂度在状态和数据层。&lt;/li>
&lt;li>Repository 应该屏蔽数据源细节。&lt;/li>
&lt;li>feature 模块不要互相依赖。&lt;/li>
&lt;li>构建逻辑也需要架构设计，不能只堆在每个模块脚本里。&lt;/li>
&lt;/ol></description></item><item><title>Android 项目组织结构：从单模块到可维护工程</title><link>https://www.dust-zed.site/android/project-structure/</link><pubDate>Fri, 17 Oct 2025 09:28:07 +0800</pubDate><guid>https://www.dust-zed.site/android/project-structure/</guid><description>&lt;p>项目结构的目标不是显得“标准”，而是让代码边界清晰、依赖方向稳定、构建配置可维护。小项目可以简单，大项目则需要明确模块职责和构建约定。&lt;/p>
&lt;h2 id="核心结论">核心结论
&lt;/h2>&lt;ol>
&lt;li>单模块项目先保持简单，不要一开始就过度拆分。&lt;/li>
&lt;li>多模块项目通常按 &lt;code>app&lt;/code>、&lt;code>feature&lt;/code>、&lt;code>core&lt;/code> 分层。&lt;/li>
&lt;li>依赖应该从上层指向下层，避免 feature 之间互相依赖。&lt;/li>
&lt;li>版本和插件声明集中管理，业务模块保持轻量。&lt;/li>
&lt;li>库按需求引入，不存在所有项目都“必需”的 UI/架构套件。&lt;/li>
&lt;/ol>
&lt;h2 id="单模块结构">单模块结构
&lt;/h2>&lt;p>适合学习项目、小工具或业务复杂度较低的应用。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">project/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── app/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── build.gradle.kts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── src/main/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── AndroidManifest.xml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── java/com/example/app/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── res/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── gradle/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── libs.versions.toml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── build.gradle.kts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── settings.gradle.kts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── gradle.properties
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>单模块的重点是目录清楚：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">ui/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">domain/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">common/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>即使不拆 Gradle 模块，也可以先在包结构里建立边界。&lt;/p>
&lt;h2 id="多模块结构">多模块结构
&lt;/h2>&lt;p>当业务变多、编译变慢、多人协作明显增加时，可以考虑多模块。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">project/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── app/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── feature/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── home/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── detail/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── settings/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── core/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── model/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── data/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── network/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── database/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── ui/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── build-logic/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── gradle/libs.versions.toml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── settings.gradle.kts
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>职责建议：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>模块&lt;/th>
&lt;th>职责&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>app&lt;/code>&lt;/td>
&lt;td>应用入口、全局导航、依赖装配&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>feature:*&lt;/code>&lt;/td>
&lt;td>具体业务功能&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>core:model&lt;/code>&lt;/td>
&lt;td>基础领域模型&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>core:data&lt;/code>&lt;/td>
&lt;td>Repository 和数据协调&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>core:network&lt;/code>&lt;/td>
&lt;td>网络请求和 DTO&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>core:database&lt;/code>&lt;/td>
&lt;td>本地数据库和 DAO&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>core:ui&lt;/code>&lt;/td>
&lt;td>公共 UI 组件&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>build-logic&lt;/code>&lt;/td>
&lt;td>构建约定插件&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="依赖方向">依赖方向
&lt;/h2>&lt;p>推荐方向：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">app
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; feature
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; core
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>避免：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">feature:home -&amp;gt; feature:detail
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">feature:detail -&amp;gt; feature:home
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果两个 feature 需要共享模型、工具或 UI，优先下沉到 &lt;code>core&lt;/code>。&lt;/p>
&lt;h2 id="依赖按需引入">依赖按需引入
&lt;/h2>&lt;p>常见选择：&lt;/p>
&lt;ol>
&lt;li>Kotlin 扩展：&lt;code>androidx.core:core-ktx&lt;/code>&lt;/li>
&lt;li>传统 View：Material Components、ConstraintLayout、AppCompat&lt;/li>
&lt;li>Compose：Compose BOM、Material3、Activity Compose&lt;/li>
&lt;li>生命周期：ViewModel、Lifecycle Runtime&lt;/li>
&lt;li>异步：Kotlin Coroutines&lt;/li>
&lt;li>数据库：Room&lt;/li>
&lt;li>网络：Retrofit/OkHttp/Ktor&lt;/li>
&lt;li>依赖注入：Hilt/Koin&lt;/li>
&lt;/ol>
&lt;p>原则：&lt;/p>
&lt;blockquote>
&lt;p>先根据项目架构和 UI 技术栈选择依赖，不要把模板里的库全部搬进来。&lt;/p>&lt;/blockquote>
&lt;h2 id="version-catalog">Version Catalog
&lt;/h2>&lt;p>&lt;code>gradle/libs.versions.toml&lt;/code> 负责集中管理依赖版本：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">versions&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">agp&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;8.7.3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">kotlin&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;2.1.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">composeBom&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;2024.12.01&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">libraries&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">androidx-core-ktx&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">module&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;androidx.core:core-ktx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">version&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;1.15.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">androidx-compose-bom&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">module&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;androidx.compose:compose-bom&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">version&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ref&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;composeBom&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">plugins&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">android-application&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">id&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;com.android.application&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">version&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ref&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">kotlin-android&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">id&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;org.jetbrains.kotlin.android&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">version&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ref&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;kotlin&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>模块中使用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">plugins&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">alias&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">libs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">plugins&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">android&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">application&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">alias&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">libs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">plugins&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">kotlin&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">android&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">dependencies&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">implementation&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">libs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">androidx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">core&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ktx&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="为什么根项目常用-apply-false">为什么根项目常用 apply false
&lt;/h2>&lt;p>根项目通常只声明插件版本，不直接应用 Android 插件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">plugins&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">alias&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">libs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">plugins&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">android&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">application&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">apply&lt;/span> &lt;span class="k">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">alias&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">libs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">plugins&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">android&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">library&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">apply&lt;/span> &lt;span class="k">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">alias&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">libs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">plugins&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">kotlin&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">android&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">apply&lt;/span> &lt;span class="k">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>原因：&lt;/p>
&lt;ol>
&lt;li>根项目不是 Android App，不应该应用 Android Application 插件；&lt;/li>
&lt;li>子模块可以复用统一插件版本；&lt;/li>
&lt;li>插件声明集中，模块使用时更清晰。&lt;/li>
&lt;/ol>
&lt;p>可以把根项目理解为插件货架：根项目负责把插件放好，具体模块按需取用。&lt;/p>
&lt;h2 id="回看清单">回看清单
&lt;/h2>&lt;ol>
&lt;li>小项目先简单，复杂度上来后再拆模块。&lt;/li>
&lt;li>多模块重点是依赖方向，不是模块数量。&lt;/li>
&lt;li>feature 不互相依赖，共享能力下沉 core。&lt;/li>
&lt;li>Version Catalog 管版本，Convention Plugins 管构建约定。&lt;/li>
&lt;li>依赖按需引入，不要让模板决定架构。&lt;/li>
&lt;/ol></description></item><item><title>MMKV 与 SharedPreferences：轻量级键值存储选型</title><link>https://www.dust-zed.site/android/mmkv-vs-sharedpreferences/</link><pubDate>Tue, 01 Jul 2025 15:49:53 +0800</pubDate><guid>https://www.dust-zed.site/android/mmkv-vs-sharedpreferences/</guid><description>&lt;p>Android 中的 &lt;code>SharedPreferences&lt;/code> 和 &lt;code>MMKV&lt;/code> 都适合存储轻量级键值数据，例如开关、用户偏好、少量业务状态和本地缓存标记。它们看起来解决的是同一个问题，但底层模型完全不同，因此性能、可靠性和适用场景也不同。&lt;/p>
&lt;p>一句话结论：&lt;/p>
&lt;blockquote>
&lt;p>少量、低频、单进程配置可以继续使用 &lt;code>SharedPreferences&lt;/code>；高频读写、多进程访问或对启动性能敏感的场景，更适合使用 &lt;code>MMKV&lt;/code>。&lt;/p>&lt;/blockquote>
&lt;h2 id="核心对比">核心对比
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>维度&lt;/th>
&lt;th>SharedPreferences&lt;/th>
&lt;th>MMKV&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>存储格式&lt;/td>
&lt;td>XML 文件&lt;/td>
&lt;td>Protobuf 编码&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>读写模型&lt;/td>
&lt;td>内存 Map + 文件持久化&lt;/td>
&lt;td>&lt;code>mmap&lt;/code> 内存映射 + 文件持久化&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>写入方式&lt;/td>
&lt;td>&lt;code>apply()&lt;/code> 异步，&lt;code>commit()&lt;/code> 同步&lt;/td>
&lt;td>写入内存映射区域，由系统负责刷盘&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>性能特点&lt;/td>
&lt;td>小数据、低频写入足够简单&lt;/td>
&lt;td>高频读写更有优势&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>多进程&lt;/td>
&lt;td>&lt;code>MODE_MULTI_PROCESS&lt;/code> 已不推荐&lt;/td>
&lt;td>原生支持多进程模式&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>接入成本&lt;/td>
&lt;td>系统内置，无需依赖&lt;/td>
&lt;td>需要引入第三方库&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>适合场景&lt;/td>
&lt;td>简单配置、低频偏好&lt;/td>
&lt;td>高频配置、跨进程状态、启动路径读写&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="sharedpreferences-的模型">SharedPreferences 的模型
&lt;/h2>&lt;p>&lt;code>SharedPreferences&lt;/code> 是 Android 官方提供的轻量级键值存储 API，文件通常位于：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">/data/data/&amp;lt;package_name&amp;gt;/shared_prefs/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>它的核心模型可以理解为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">XML 文件
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; 首次读取时加载到内存 Map
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; 修改内存数据
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; apply/commit 持久化回 XML
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="sharedpreferences-的优点">SharedPreferences 的优点
&lt;/h3>&lt;p>&lt;code>SharedPreferences&lt;/code> 最大的优势是简单、稳定、系统内置。对于低频配置，例如是否首次启动、主题开关、简单用户偏好，它的使用成本很低。&lt;/p>
&lt;p>常见写入方式有两个：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">preferences&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">edit&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">putBoolean&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;first_open&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">false&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">apply&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>apply()&lt;/code> 会先更新内存，再异步写入磁盘，日常更常用。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">val&lt;/span> &lt;span class="py">success&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">preferences&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">edit&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">putString&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;token&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">token&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">commit&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>commit()&lt;/code> 会同步等待写入结果，因此可以拿到成功或失败，但不适合在主线程频繁调用。&lt;/p>
&lt;h3 id="主要问题">主要问题
&lt;/h3>&lt;p>&lt;code>SharedPreferences&lt;/code> 的问题通常不是“不能用”，而是容易被用在不适合的场景。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>频繁写入成本较高&lt;/strong>&lt;/p>
&lt;p>它以 XML 作为持久化格式。数据变多后，序列化、反序列化和文件写入成本都会上升。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>&lt;code>apply()&lt;/code> 也可能影响主线程&lt;/strong>&lt;/p>
&lt;p>&lt;code>apply()&lt;/code> 虽然是异步写入，但未完成的写入任务会进入 &lt;code>QueuedWork&lt;/code>。在 Activity 停止等生命周期阶段，系统可能等待这些任务完成。如果积压了大量写入，仍然可能拖慢主线程。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>多进程场景不可靠&lt;/strong>&lt;/p>
&lt;p>&lt;code>MODE_MULTI_PROCESS&lt;/code> 已经不推荐使用。多个进程同时读写同一个配置文件时，很难保证状态实时一致。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>不适合存储大对象&lt;/strong>&lt;/p>
&lt;p>它只适合基础类型和字符串集合。复杂对象需要自行序列化，这会让边界变得模糊：配置存储逐渐变成小型数据库，但又没有数据库的事务、查询和迁移能力。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="mmkv-的模型">MMKV 的模型
&lt;/h2>&lt;p>&lt;code>MMKV&lt;/code> 是微信开源的高性能键值存储组件。它的核心机制是 &lt;code>mmap&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">文件
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; mmap 映射到进程虚拟内存
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; 读写像操作内存一样进行
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; 操作系统负责把脏页刷回磁盘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>它还使用 Protobuf 进行编码，相比 XML 更紧凑，也更适合频繁读写。&lt;/p>
&lt;h3 id="mmkv-的优点">MMKV 的优点
&lt;/h3>&lt;ol>
&lt;li>
&lt;p>&lt;strong>读写性能更好&lt;/strong>&lt;/p>
&lt;p>高频读写时，&lt;code>mmap&lt;/code> 可以减少传统文件 I/O 的数据拷贝和系统调用成本。对于启动路径上需要读取多个配置项的场景，MMKV 通常更合适。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>支持多进程&lt;/strong>&lt;/p>
&lt;p>MMKV 提供多进程模式，内部通过文件锁等机制处理并发访问，比 &lt;code>SharedPreferences&lt;/code> 的多进程方案可靠。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>编码更紧凑&lt;/strong>&lt;/p>
&lt;p>Protobuf 编码通常比 XML 更省空间，也减少了解析文本格式的开销。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>API 仍然接近键值存储&lt;/strong>&lt;/p>
&lt;p>它并没有引入复杂数据库模型，迁移成本相对可控。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="需要注意的边界">需要注意的边界
&lt;/h3>&lt;p>MMKV 性能更强，但不意味着所有本地数据都应该塞进去。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>它仍然是键值存储&lt;/strong>&lt;/p>
&lt;p>如果数据需要复杂查询、关系建模、分页读取或事务保证，应该考虑 Room、SQLite 或专门的数据层方案。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>跨语言或跨版本数据要关注兼容性&lt;/strong>&lt;/p>
&lt;p>存储结构升级时，要考虑旧 key、默认值和异常数据。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>第三方依赖需要纳入工程治理&lt;/strong>&lt;/p>
&lt;p>引入 MMKV 后，要关注版本升级、初始化位置、多进程模式配置和崩溃兜底。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="选型建议">选型建议
&lt;/h2>&lt;h3 id="继续使用-sharedpreferences">继续使用 SharedPreferences
&lt;/h3>&lt;p>适合这些场景：&lt;/p>
&lt;ol>
&lt;li>数据量很小；&lt;/li>
&lt;li>写入频率低；&lt;/li>
&lt;li>只在单进程访问；&lt;/li>
&lt;li>不在启动关键路径上频繁读写；&lt;/li>
&lt;li>不需要额外引入依赖。&lt;/li>
&lt;/ol>
&lt;p>典型例子：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">是否首次启动
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">是否开启深色模式
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">用户选择的语言
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">简单实验开关
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="优先考虑-mmkv">优先考虑 MMKV
&lt;/h3>&lt;p>适合这些场景：&lt;/p>
&lt;ol>
&lt;li>启动阶段需要读取多个配置项；&lt;/li>
&lt;li>配置更新比较频繁；&lt;/li>
&lt;li>App 存在多进程访问；&lt;/li>
&lt;li>对配置读写性能比较敏感；&lt;/li>
&lt;li>原本的 &lt;code>SharedPreferences&lt;/code> 已经出现卡顿或 ANR 风险。&lt;/li>
&lt;/ol>
&lt;p>典型例子：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">启动配置缓存
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">跨进程共享开关
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">音视频或云游戏场景中的运行参数
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">频繁变化的轻量状态
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="迁移思路">迁移思路
&lt;/h2>&lt;p>从 &lt;code>SharedPreferences&lt;/code> 迁移到 MMKV 时，不建议一次性全量替换。更稳妥的方式是按风险分层：&lt;/p>
&lt;ol>
&lt;li>先迁移高频读写或启动路径上的配置；&lt;/li>
&lt;li>保留旧 key 的读取逻辑，首次读取后写入 MMKV；&lt;/li>
&lt;li>给默认值和异常值兜底；&lt;/li>
&lt;li>观察一段时间后再清理旧数据；&lt;/li>
&lt;li>避免在迁移过程中改变业务语义。&lt;/li>
&lt;/ol>
&lt;p>伪代码示例：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fun&lt;/span> &lt;span class="nf">getBoolean&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">String&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">defaultValue&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Boolean&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="n">Boolean&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">mmkv&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">containsKey&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">mmkv&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">decodeBool&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">defaultValue&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">oldValue&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">sharedPreferences&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">getBoolean&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">defaultValue&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mmkv&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">encode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">oldValue&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">oldValue&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="回看清单">回看清单
&lt;/h2>&lt;ol>
&lt;li>&lt;code>SharedPreferences&lt;/code> 简单可靠，但不适合大文件、高频写入和多进程同步。&lt;/li>
&lt;li>&lt;code>apply()&lt;/code> 不是完全没有主线程风险，生命周期阶段可能等待未完成写入。&lt;/li>
&lt;li>&lt;code>MMKV&lt;/code> 的优势来自 &lt;code>mmap&lt;/code> 和 Protobuf，适合性能敏感的轻量级键值数据。&lt;/li>
&lt;li>MMKV 不是数据库，不应该承担复杂数据模型和查询职责。&lt;/li>
&lt;li>迁移时优先处理高风险 key，保持旧数据兜底，避免一次性重构引入新问题。&lt;/li>
&lt;/ol></description></item></channel></rss>