<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://jelychow.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jelychow.github.io/" rel="alternate" type="text/html" /><updated>2025-07-31T10:08:13+00:00</updated><id>https://jelychow.github.io/feed.xml</id><title type="html">Software developer / Android</title><subtitle>love coding</subtitle><author><name>CaptainZ</name><email>jelychow@gmail.com</email></author><entry><title type="html">使用工厂模式和接口封装实验性API的好处</title><link href="https://jelychow.github.io/posts/encapsulating_experimental_apis/" rel="alternate" type="text/html" title="使用工厂模式和接口封装实验性API的好处" /><published>2025-07-31T00:00:00+00:00</published><updated>2025-07-31T00:00:00+00:00</updated><id>https://jelychow.github.io/posts/encapsulating_experimental_apis</id><content type="html" xml:base="https://jelychow.github.io/posts/encapsulating_experimental_apis/"><![CDATA[<div class="language-selector">
  <span class="lang-link active">中文</span> | 
  <a href="/posts/encapsulating_experimental_apis/en/" class="lang-link">English</a>
</div>

<h1 id="使用工厂模式和接口封装实验性api的好处">使用工厂模式和接口封装实验性API的好处</h1>

<p>最近在一个多平台项目中，我遇到了一个让人头疼的问题：项目中大量使用了标记为”实验性”的API，导致代码库中充斥着各种 <code class="language-plaintext highlighter-rouge">@OptIn</code> 注解。每次升级依赖库，总会有几个API变动，需要修改大量代码。这种情况下，我开始思考如何更优雅地处理这些实验性API，最终通过工厂模式和接口封装找到了解决方案。今天就来分享一下这个实践经验。</p>

<h3 id="实验性api到底有什么问题">实验性API到底有什么问题？</h3>

<p>在深入解决方案之前，先聊聊为什么实验性API会成为一个问题。以Kotlin的<code class="language-plaintext highlighter-rouge">kotlinx-datetime</code>库为例，使用它时我们会遇到这些麻烦：</p>

<ol>
  <li><strong>注解污染</strong>：代码中到处都是<code class="language-plaintext highlighter-rouge">@OptIn(ExperimentalTime::class)</code>，看着就烦</li>
  <li><strong>编译器唠叨</strong>：忘记加注解？编译器立马警告或报错</li>
  <li><strong>不稳定性</strong>：实验性API随时可能变化，一个小版本升级可能导致大量代码需要修改</li>
  <li><strong>耦合度高</strong>：业务逻辑直接依赖实验性API，导致后续难以替换或升级</li>
  <li><strong>测试困难</strong>：直接使用实验性API的代码往往难以进行单元测试</li>
</ol>

<p>这些问题在小项目中可能不明显，但在我们的多平台项目中，随着代码库增长，这些问题逐渐放大，最终变成了一个不得不解决的技术债。</p>

<h3 id="封装的核心思想">封装的核心思想</h3>

<p>解决这个问题的关键在于”隔离变化”。我们需要将那些可能变化的实验性API隔离到一个地方，而不是散布在整个代码库中。具体做法是：</p>

<ol>
  <li>定义稳定的接口，表达我们真正需要的功能</li>
  <li>在单一实现类中使用实验性API</li>
  <li>通过工厂模式提供接口实例</li>
</ol>

<p>这样，当实验性API变化时，我们只需修改实现类，而不必触碰业务代码。</p>

<h3 id="实战案例封装时间相关api">实战案例：封装时间相关API</h3>

<p>下面是我在项目中实际使用的例子，展示如何封装<code class="language-plaintext highlighter-rouge">kotlinx-datetime</code>的实验性API：</p>

<h4 id="1-定义稳定接口">1. 定义稳定接口</h4>

<p>首先，我们需要定义一个清晰的接口，只包含我们真正需要的功能：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * 时间提供者接口
 * 封装时间相关操作，避免直接使用实验性的时间API
 */</span>
<span class="kd">interface</span> <span class="nc">TimeProvider</span> <span class="p">{</span>
    <span class="cm">/**
     * 获取当前时间的毫秒时间戳
     */</span>
    <span class="k">fun</span> <span class="nf">getCurrentTimeMillis</span><span class="p">():</span> <span class="nc">Long</span>

    <span class="cm">/**
     * 格式化时间戳为日期时间字符串
     * 格式：yyyy-MM-dd HH:mm
     */</span>
    <span class="k">fun</span> <span class="nf">formatDateTime</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">String</span>

    <span class="cm">/**
     * 格式化时间戳为日期字符串
     * 格式：yyyy年MM月dd日
     */</span>
    <span class="k">fun</span> <span class="nf">formatDate</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">String</span>

    <span class="cm">/**
     * 获取当前系统默认时区
     */</span>
    <span class="k">fun</span> <span class="nf">getCurrentTimeZoneId</span><span class="p">():</span> <span class="nc">String</span>

    <span class="cm">/**
     * 判断时间戳是否已过期
     * @param timestamp 过期时间的时间戳（毫秒）
     * @param bufferMillis 缓冲时间（毫秒），默认为5000毫秒
     */</span>
    <span class="k">fun</span> <span class="nf">isExpired</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">,</span> <span class="n">bufferMillis</span><span class="p">:</span> <span class="nc">Long</span> <span class="p">=</span> <span class="mi">5000</span><span class="p">):</span> <span class="nc">Boolean</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这个接口设计得很干净，没有暴露任何实验性API的细节，只关注我们的业务需求。</p>

<h4 id="2-实现类所有实验性api的集中营">2. 实现类：所有实验性API的”集中营”</h4>

<p>接下来，创建一个实现类，所有的实验性API使用都被限制在这个类中：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">@</span><span class="n">file</span><span class="p">:</span><span class="nc">OptIn</span><span class="p">(</span><span class="nc">ExperimentalTime</span><span class="o">::</span><span class="k">class</span><span class="p">)</span>

<span class="cm">/**
 * TimeProvider的默认实现
 * 使用kotlinx-datetime库实现时间相关功能
 */</span>
<span class="kd">class</span> <span class="nc">DefaultTimeProvider</span> <span class="p">:</span> <span class="nc">TimeProvider</span> <span class="p">{</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">getCurrentTimeMillis</span><span class="p">():</span> <span class="nc">Long</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nc">Clock</span><span class="p">.</span><span class="nc">System</span><span class="p">.</span><span class="nf">now</span><span class="p">().</span><span class="nf">toEpochMilliseconds</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">formatDateTime</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">String</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">instant</span> <span class="p">=</span> <span class="nc">Instant</span><span class="p">.</span><span class="nf">fromEpochMilliseconds</span><span class="p">(</span><span class="n">timestamp</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">localDateTime</span> <span class="p">=</span> <span class="n">instant</span><span class="p">.</span><span class="nf">toLocalDateTime</span><span class="p">(</span><span class="nc">TimeZone</span><span class="p">.</span><span class="nf">currentSystemDefault</span><span class="p">())</span>
        <span class="k">return</span> <span class="s">"${localDateTime.year}-${localDateTime.monthNumber.toString().padStart(2, '0')}-${localDateTime.dayOfMonth.toString().padStart(2, '0')} "</span> <span class="p">+</span>
                <span class="s">"${localDateTime.hour.toString().padStart(2, '0')}:${localDateTime.minute.toString().padStart(2, '0')}"</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">formatDate</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">String</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">instant</span> <span class="p">=</span> <span class="nc">Instant</span><span class="p">.</span><span class="nf">fromEpochMilliseconds</span><span class="p">(</span><span class="n">timestamp</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">localDateTime</span> <span class="p">=</span> <span class="n">instant</span><span class="p">.</span><span class="nf">toLocalDateTime</span><span class="p">(</span><span class="nc">TimeZone</span><span class="p">.</span><span class="nf">currentSystemDefault</span><span class="p">())</span>
        <span class="k">return</span> <span class="s">"${localDateTime.year}年${localDateTime.monthNumber}月${localDateTime.dayOfMonth}日"</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">getCurrentTimeZoneId</span><span class="p">():</span> <span class="nc">String</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nc">TimeZone</span><span class="p">.</span><span class="nf">currentSystemDefault</span><span class="p">().</span><span class="n">id</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">isExpired</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">,</span> <span class="n">bufferMillis</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">Boolean</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nc">Clock</span><span class="p">.</span><span class="nc">System</span><span class="p">.</span><span class="nf">now</span><span class="p">().</span><span class="nf">toEpochMilliseconds</span><span class="p">()</span> <span class="p">&gt;</span> <span class="p">(</span><span class="n">timestamp</span> <span class="p">-</span> <span class="n">bufferMillis</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>注意，所有实验性API的使用都被限制在这个单一的文件中，<code class="language-plaintext highlighter-rouge">@OptIn</code>注解只需要在文件级别添加一次。</p>

<h4 id="3-工厂提供接口实例">3. 工厂：提供接口实例</h4>

<p>最后，创建一个工厂类来管理实例：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="cm">/**
 * 时间提供者工厂
 * 用于获取TimeProvider实例
 */</span>
<span class="kd">object</span> <span class="nc">TimeProviderFactory</span> <span class="p">{</span>
    <span class="c1">// 默认实例，可以在测试中替换</span>
    <span class="k">private</span> <span class="kd">var</span> <span class="py">instance</span><span class="p">:</span> <span class="nc">TimeProvider</span> <span class="p">=</span> <span class="nc">DefaultTimeProvider</span><span class="p">()</span>

    <span class="cm">/**
     * 获取TimeProvider实例
     */</span>
    <span class="k">fun</span> <span class="nf">getInstance</span><span class="p">():</span> <span class="nc">TimeProvider</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">instance</span>
    <span class="p">}</span>

    <span class="cm">/**
     * 设置自定义TimeProvider实例
     * 主要用于测试
     */</span>
    <span class="k">fun</span> <span class="nf">setInstance</span><span class="p">(</span><span class="n">customInstance</span><span class="p">:</span> <span class="nc">TimeProvider</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">instance</span> <span class="p">=</span> <span class="n">customInstance</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="在代码中使用这种模式">在代码中使用这种模式</h3>

<p>现在，我们可以在代码中使用工厂来获取TimeProvider实例，而不是直接使用实验性API：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">UserViewModel</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">repository</span><span class="p">:</span> <span class="nc">UserRepository</span><span class="p">,</span>
    <span class="c1">// 依赖可以注入，也可以从工厂获取</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">timeProvider</span><span class="p">:</span> <span class="nc">TimeProvider</span> <span class="p">=</span> <span class="nc">TimeProviderFactory</span><span class="p">.</span><span class="nf">getInstance</span><span class="p">()</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">ViewModel</span><span class="p">()</span> <span class="p">{</span>
    
    <span class="k">fun</span> <span class="nf">formatLastLoginTime</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">String</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">timeProvider</span><span class="p">.</span><span class="nf">formatDateTime</span><span class="p">(</span><span class="n">timestamp</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="k">fun</span> <span class="nf">checkSessionExpired</span><span class="p">(</span><span class="n">sessionExpiryTime</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">Boolean</span> <span class="p">{</span>
        <span class="c1">// 这里不需要任何实验性API注解！</span>
        <span class="k">return</span> <span class="n">timeProvider</span><span class="p">.</span><span class="nf">isExpired</span><span class="p">(</span><span class="n">sessionExpiryTime</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>业务代码现在变得干净，不需要任何实验性API注解。</p>

<h3 id="测试变得简单">测试变得简单</h3>

<p>这种模式的一个最大好处是简化了测试。我们可以轻松创建一个模拟实现：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MockTimeProvider</span> <span class="p">:</span> <span class="nc">TimeProvider</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="py">currentTimeMillis</span> <span class="p">=</span> <span class="mi">0L</span>
    
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">getCurrentTimeMillis</span><span class="p">():</span> <span class="nc">Long</span> <span class="p">=</span> <span class="n">currentTimeMillis</span>
    
    <span class="c1">// 其他方法的简单实现...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>通过工厂的<code class="language-plaintext highlighter-rouge">setInstance</code>方法，我们可以轻松替换实现，实现对时间的完全控制，测试变得简单而可靠。</p>

<h3 id="升级依赖时的惊喜">升级依赖时的惊喜</h3>

<p>这种模式的价值在我们升级依赖时体现得最为明显。记得有一次，我们将<code class="language-plaintext highlighter-rouge">kotlinx-datetime</code>从0.2.1升级到0.3.0，API有一些变化。在以前，这意味着要修改散布在各处的代码；而现在，我们只需要更新<code class="language-plaintext highlighter-rouge">DefaultTimeProvider</code>一个类，其他代码完全不受影响。</p>

<p>整个升级过程从原来的”噩梦”变成了”小菜一碟”，这种感觉真的很爽！</p>

<h3 id="总结值得推广的最佳实践">总结：值得推广的最佳实践</h3>

<p>通过实际项目经验，我认为这种封装实验性API的模式值得在更多项目中推广：</p>

<ol>
  <li><strong>隔离变化</strong>：将不稳定的API限制在单一实现类中</li>
  <li><strong>接口稳定</strong>：为业务代码提供稳定的接口</li>
  <li><strong>测试友好</strong>：便于在测试中替换实现</li>
  <li><strong>升级平滑</strong>：依赖升级时只需修改实现类</li>
  <li><strong>代码整洁</strong>：业务代码中没有实验性API的痕迹</li>
</ol>

<p>这种模式不仅适用于时间相关API，也适用于任何标记为实验性或不稳定的API。如果你的项目中也面临类似的问题，不妨试试这种方法，相信会有不错的效果。</p>

<p>你有没有遇到过类似的问题？或者有其他处理实验性API的方法？欢迎在评论区分享你的经验！</p>]]></content><author><name>CaptainZ</name><email>jelychow@gmail.com</email></author><category term="API Design" /><category term="Factory Pattern" /><category term="Kotlin" /><category term="Best Practices" /><summary type="html"><![CDATA[中文 | English]]></summary></entry><entry><title type="html">Encapsulating Experimental APIs with Factory Pattern and Interfaces</title><link href="https://jelychow.github.io/posts/encapsulating_experimental_apis/en/" rel="alternate" type="text/html" title="Encapsulating Experimental APIs with Factory Pattern and Interfaces" /><published>2025-07-31T00:00:00+00:00</published><updated>2025-07-31T00:00:00+00:00</updated><id>https://jelychow.github.io/posts/encapsulating_experimental_apis/encapsulating_experimental_apis_en</id><content type="html" xml:base="https://jelychow.github.io/posts/encapsulating_experimental_apis/en/"><![CDATA[<div class="language-selector">
  <a href="/posts/encapsulating_experimental_apis/" class="lang-link">中文</a> | 
  <span class="lang-link active">English</span>
</div>

<h1 id="encapsulating-experimental-apis-with-factory-pattern-and-interfaces">Encapsulating Experimental APIs with Factory Pattern and Interfaces</h1>

<p>Recently, I ran into a frustrating problem in a multiplatform project: our codebase was littered with <code class="language-plaintext highlighter-rouge">@OptIn</code> annotations due to heavy use of experimental APIs. Every dependency upgrade meant changing code in dozens of places. After some headaches, I found a cleaner solution using the Factory Pattern and interfaces to encapsulate these experimental APIs. Let me share this approach that saved our project from annotation hell.</p>

<h3 id="the-problem-with-experimental-apis">The Problem with Experimental APIs</h3>

<p>Before diving into the solution, let’s talk about why experimental APIs are problematic in the first place. Taking Kotlin’s <code class="language-plaintext highlighter-rouge">kotlinx-datetime</code> library as an example:</p>

<ol>
  <li><strong>Annotation Pollution</strong>: Code filled with <code class="language-plaintext highlighter-rouge">@OptIn(ExperimentalTime::class)</code> annotations everywhere</li>
  <li><strong>Compiler Warnings</strong>: Forget an annotation? Get ready for warnings or errors</li>
  <li><strong>API Instability</strong>: Experimental APIs can change between versions, breaking your code</li>
  <li><strong>Tight Coupling</strong>: Business logic directly depends on unstable APIs</li>
  <li><strong>Testing Headaches</strong>: Code using experimental APIs directly is often harder to test</li>
</ol>

<p>These issues might seem minor in small projects, but they compound quickly in larger codebases. In our multiplatform project, this became a significant pain point that we couldn’t ignore anymore.</p>

<h3 id="the-core-idea-isolation">The Core Idea: Isolation</h3>

<p>The key insight is to “isolate change.” We need to contain potentially changing experimental APIs in one place, rather than spreading them throughout the codebase. The approach is:</p>

<ol>
  <li>Define stable interfaces that express what we actually need</li>
  <li>Use experimental APIs in a single implementation class</li>
  <li>Provide interface instances through a factory pattern</li>
</ol>

<p>This way, when experimental APIs change, we only need to modify the implementation class without touching business code.</p>

<h3 id="real-world-example-encapsulating-time-apis">Real-World Example: Encapsulating Time APIs</h3>

<p>Here’s a real example from my project showing how to encapsulate the experimental APIs from <code class="language-plaintext highlighter-rouge">kotlinx-datetime</code>:</p>

<h4 id="1-define-a-stable-interface">1. Define a Stable Interface</h4>

<p>First, we define a clean interface that only includes what we really need:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * Time provider interface
 * Encapsulates time-related operations to avoid direct use of experimental time APIs
 */</span>
<span class="kd">interface</span> <span class="nc">TimeProvider</span> <span class="p">{</span>
    <span class="cm">/**
     * Get current time in milliseconds
     */</span>
    <span class="k">fun</span> <span class="nf">getCurrentTimeMillis</span><span class="p">():</span> <span class="nc">Long</span>

    <span class="cm">/**
     * Format timestamp to date-time string
     * Format: yyyy-MM-dd HH:mm
     */</span>
    <span class="k">fun</span> <span class="nf">formatDateTime</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">String</span>

    <span class="cm">/**
     * Format timestamp to date string
     * Format: yyyy-MM-dd
     */</span>
    <span class="k">fun</span> <span class="nf">formatDate</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">String</span>

    <span class="cm">/**
     * Get current system default timezone ID
     */</span>
    <span class="k">fun</span> <span class="nf">getCurrentTimeZoneId</span><span class="p">():</span> <span class="nc">String</span>

    <span class="cm">/**
     * Check if timestamp is expired
     * @param timestamp Expiration timestamp in milliseconds
     * @param bufferMillis Buffer time in milliseconds, default is 5000ms
     */</span>
    <span class="k">fun</span> <span class="nf">isExpired</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">,</span> <span class="n">bufferMillis</span><span class="p">:</span> <span class="nc">Long</span> <span class="p">=</span> <span class="mi">5000</span><span class="p">):</span> <span class="nc">Boolean</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This interface is clean and doesn’t expose any experimental API details - it only focuses on our business needs.</p>

<h4 id="2-implementation-where-all-experimental-apis-live">2. Implementation: Where All Experimental APIs Live</h4>

<p>Next, create an implementation class where all experimental API usage is contained:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">@</span><span class="n">file</span><span class="p">:</span><span class="nc">OptIn</span><span class="p">(</span><span class="nc">ExperimentalTime</span><span class="o">::</span><span class="k">class</span><span class="p">)</span>
<span class="cm">/**
 * Default implementation of TimeProvider
 * Uses kotlinx-datetime library to implement time-related functions
 */</span>
<span class="kd">class</span> <span class="nc">DefaultTimeProvider</span> <span class="p">:</span> <span class="nc">TimeProvider</span> <span class="p">{</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">getCurrentTimeMillis</span><span class="p">():</span> <span class="nc">Long</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nc">Clock</span><span class="p">.</span><span class="nc">System</span><span class="p">.</span><span class="nf">now</span><span class="p">().</span><span class="nf">toEpochMilliseconds</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">formatDateTime</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">String</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">instant</span> <span class="p">=</span> <span class="nc">Instant</span><span class="p">.</span><span class="nf">fromEpochMilliseconds</span><span class="p">(</span><span class="n">timestamp</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">localDateTime</span> <span class="p">=</span> <span class="n">instant</span><span class="p">.</span><span class="nf">toLocalDateTime</span><span class="p">(</span><span class="nc">TimeZone</span><span class="p">.</span><span class="nf">currentSystemDefault</span><span class="p">())</span>
        <span class="k">return</span> <span class="s">"${localDateTime.year}-${localDateTime.monthNumber.toString().padStart(2, '0')}-${localDateTime.dayOfMonth.toString().padStart(2, '0')} "</span> <span class="p">+</span>
                <span class="s">"${localDateTime.hour.toString().padStart(2, '0')}:${localDateTime.minute.toString().padStart(2, '0')}"</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">formatDate</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">String</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">instant</span> <span class="p">=</span> <span class="nc">Instant</span><span class="p">.</span><span class="nf">fromEpochMilliseconds</span><span class="p">(</span><span class="n">timestamp</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">localDateTime</span> <span class="p">=</span> <span class="n">instant</span><span class="p">.</span><span class="nf">toLocalDateTime</span><span class="p">(</span><span class="nc">TimeZone</span><span class="p">.</span><span class="nf">currentSystemDefault</span><span class="p">())</span>
        <span class="k">return</span> <span class="s">"${localDateTime.year}-${localDateTime.monthNumber.toString().padStart(2, '0')}-${localDateTime.dayOfMonth.toString().padStart(2, '0')}"</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">getCurrentTimeZoneId</span><span class="p">():</span> <span class="nc">String</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nc">TimeZone</span><span class="p">.</span><span class="nf">currentSystemDefault</span><span class="p">().</span><span class="n">id</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">isExpired</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">,</span> <span class="n">bufferMillis</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">Boolean</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nc">Clock</span><span class="p">.</span><span class="nc">System</span><span class="p">.</span><span class="nf">now</span><span class="p">().</span><span class="nf">toEpochMilliseconds</span><span class="p">()</span> <span class="p">&gt;</span> <span class="p">(</span><span class="n">timestamp</span> <span class="p">-</span> <span class="n">bufferMillis</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Notice that all experimental API usage is contained in this single file, with the <code class="language-plaintext highlighter-rouge">@OptIn</code> annotation only needed once at the file level.</p>

<h4 id="3-factory-providing-access-to-the-implementation">3. Factory: Providing Access to the Implementation</h4>

<p>Finally, create a factory to manage instances:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="cm">/**
 * Time provider factory
 * Used to get TimeProvider instances
 */</span>
<span class="kd">object</span> <span class="nc">TimeProviderFactory</span> <span class="p">{</span>
    <span class="c1">// Default instance, can be replaced for testing</span>
    <span class="k">private</span> <span class="kd">var</span> <span class="py">instance</span><span class="p">:</span> <span class="nc">TimeProvider</span> <span class="p">=</span> <span class="nc">DefaultTimeProvider</span><span class="p">()</span>

    <span class="cm">/**
     * Get TimeProvider instance
     */</span>
    <span class="k">fun</span> <span class="nf">getInstance</span><span class="p">():</span> <span class="nc">TimeProvider</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">instance</span>
    <span class="p">}</span>

    <span class="cm">/**
     * Set custom TimeProvider instance
     * Mainly used for testing
     */</span>
    <span class="k">fun</span> <span class="nf">setInstance</span><span class="p">(</span><span class="n">customInstance</span><span class="p">:</span> <span class="nc">TimeProvider</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">instance</span> <span class="p">=</span> <span class="n">customInstance</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="using-the-pattern-in-your-code">Using the Pattern in Your Code</h3>

<p>Now, instead of directly using experimental APIs throughout your codebase, you can use the factory:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">UserViewModel</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">repository</span><span class="p">:</span> <span class="nc">UserRepository</span><span class="p">,</span>
    <span class="c1">// Dependency can be injected, or obtained from factory</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">timeProvider</span><span class="p">:</span> <span class="nc">TimeProvider</span> <span class="p">=</span> <span class="nc">TimeProviderFactory</span><span class="p">.</span><span class="nf">getInstance</span><span class="p">()</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">ViewModel</span><span class="p">()</span> <span class="p">{</span>
    
    <span class="k">fun</span> <span class="nf">formatLastLoginTime</span><span class="p">(</span><span class="n">timestamp</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">String</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">timeProvider</span><span class="p">.</span><span class="nf">formatDateTime</span><span class="p">(</span><span class="n">timestamp</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="k">fun</span> <span class="nf">checkSessionExpired</span><span class="p">(</span><span class="n">sessionExpiryTime</span><span class="p">:</span> <span class="nc">Long</span><span class="p">):</span> <span class="nc">Boolean</span> <span class="p">{</span>
        <span class="c1">// No experimental API annotations needed here!</span>
        <span class="k">return</span> <span class="n">timeProvider</span><span class="p">.</span><span class="nf">isExpired</span><span class="p">(</span><span class="n">sessionExpiryTime</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The business code is now clean, with no experimental API annotations needed.</p>

<h3 id="testing-made-easy">Testing Made Easy</h3>

<p>One of the biggest benefits of this pattern is how it simplifies testing. You can easily create a mock implementation:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MockTimeProvider</span> <span class="p">:</span> <span class="nc">TimeProvider</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="py">currentTimeMillis</span> <span class="p">=</span> <span class="mi">0L</span>
    
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">getCurrentTimeMillis</span><span class="p">():</span> <span class="nc">Long</span> <span class="p">=</span> <span class="n">currentTimeMillis</span>
    
    <span class="c1">// Simple implementations of other methods...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Through the factory’s <code class="language-plaintext highlighter-rouge">setInstance</code> method, we can easily swap implementations, gaining complete control over time for testing purposes.</p>

<h3 id="the-upgrade-surprise">The Upgrade Surprise</h3>

<p>The value of this pattern became most evident when upgrading dependencies. I remember when we upgraded <code class="language-plaintext highlighter-rouge">kotlinx-datetime</code> from 0.2.1 to 0.3.0, which had some API changes. Previously, this would have meant modifying code scattered throughout the codebase; now, we only needed to update the <code class="language-plaintext highlighter-rouge">DefaultTimeProvider</code> class, leaving all other code untouched.</p>

<p>The entire upgrade process went from “nightmare” to “piece of cake” - that feeling was priceless!</p>

<h3 id="conclusion-a-best-practice-worth-spreading">Conclusion: A Best Practice Worth Spreading</h3>

<p>Based on real project experience, I believe this pattern of encapsulating experimental APIs is worth adopting in more projects:</p>

<ol>
  <li><strong>Isolate Change</strong>: Contain unstable APIs in a single implementation class</li>
  <li><strong>Stable Interfaces</strong>: Provide stable interfaces for business code</li>
  <li><strong>Testing Friendly</strong>: Easy to swap implementations for testing</li>
  <li><strong>Smooth Upgrades</strong>: Only need to modify the implementation class when dependencies change</li>
  <li><strong>Clean Code</strong>: No experimental API traces in business code</li>
</ol>

<p>This pattern isn’t just for time-related APIs - it works for any API marked as experimental or unstable. If you’re facing similar issues in your project, give this approach a try. I think you’ll be pleasantly surprised by the results.</p>

<p>Have you encountered similar problems? Or do you have other approaches to handling experimental APIs? I’d love to hear your experiences in the comments!</p>]]></content><author><name>CaptainZ</name><email>jelychow@gmail.com</email></author><category term="API Design" /><category term="Factory Pattern" /><category term="Kotlin" /><category term="Best Practices" /><summary type="html"><![CDATA[中文 | English]]></summary></entry><entry><title type="html">Compose Dsl 与 Kotlin 高阶函数特性</title><link href="https://jelychow.github.io/Compose-DSL-%E4%B8%8E-Kotlin-%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0%E7%89%B9%E6%80%A7/" rel="alternate" type="text/html" title="Compose Dsl 与 Kotlin 高阶函数特性" /><published>2025-07-13T00:00:00+00:00</published><updated>2025-07-13T00:00:00+00:00</updated><id>https://jelychow.github.io/Compose-DSL-%E4%B8%8E-Kotlin-%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0%E7%89%B9%E6%80%A7</id><content type="html" xml:base="https://jelychow.github.io/Compose-DSL-%E4%B8%8E-Kotlin-%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0%E7%89%B9%E6%80%A7/"><![CDATA[<h1 id="compose-dsl-与-kotlin-高阶函数打造优雅声明式-ui-的秘密武器">Compose DSL 与 Kotlin 高阶函数：打造优雅声明式 UI 的秘密武器</h1>

<p>最近在项目中深度使用 Compose 开发 UI，越来越感受到 Kotlin 高阶函数与 DSL 结合的强大之处。不得不说，这套组合拳真的改变了我们构建 UI 的方式。今天就来聊聊这背后的原理和实战技巧，希望能给大家带来一些启发。</p>

<h2 id="kotlin-高阶函数一切-dsl-的基石">Kotlin 高阶函数：一切 DSL 的基石</h2>

<p>说到 Compose 的 DSL，就不得不先聊聊 Kotlin 高阶函数。这可以说是整个 DSL 体系的基石，没有它，就没有今天这么优雅的 Compose API。</p>

<h3 id="函数可以像变量一样传递">函数可以像变量一样传递</h3>

<p>Kotlin 中，函数可以像普通变量一样被传递和使用，这一点太关键了：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 看这个简单例子</span>
<span class="k">fun</span> <span class="nf">calculate</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> <span class="n">operation</span><span class="p">:</span> <span class="p">(</span><span class="nc">Int</span><span class="p">,</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Int</span><span class="p">):</span> <span class="nc">Int</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nf">operation</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// 使用起来超级灵活</span>
<span class="kd">val</span> <span class="py">sum</span> <span class="p">=</span> <span class="nf">calculate</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="p">{</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="p">-&gt;</span> <span class="n">x</span> <span class="p">+</span> <span class="n">y</span> <span class="p">}</span>  <span class="c1">// 结果为 8</span>
<span class="kd">val</span> <span class="py">multiply</span> <span class="p">=</span> <span class="nf">calculate</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="p">{</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="p">-&gt;</span> <span class="n">x</span> <span class="p">*</span> <span class="n">y</span> <span class="p">}</span>  <span class="c1">// 结果为 15</span>
</code></pre></div></div>

<p>在我看来，这种设计让代码变得异常灵活，特别是在构建 UI 这种场景下，简直如鱼得水。</p>

<h3 id="带接收者的函数dsl-的核心魔法">带接收者的函数：DSL 的核心魔法</h3>

<p>如果说高阶函数是基石，那带接收者的函数就是 Compose DSL 的核心魔法了：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 定义一个简单的 UI 构建器</span>
<span class="kd">class</span> <span class="nc">UIBuilder</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">text</span><span class="p">(</span><span class="n">content</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"添加文本: $content"</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="k">fun</span> <span class="nf">button</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">onClick</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"添加按钮: $text"</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="k">fun</span> <span class="nf">image</span><span class="p">(</span><span class="n">url</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"添加图片: $url"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// 这才是真正的魔法 - 带接收者的函数类型</span>
<span class="k">fun</span> <span class="nf">buildUI</span><span class="p">(</span><span class="n">content</span><span class="p">:</span> <span class="nc">UIBuilder</span><span class="p">.()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"开始构建UI"</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">builder</span> <span class="p">=</span> <span class="nc">UIBuilder</span><span class="p">()</span>
    <span class="n">builder</span><span class="p">.</span><span class="nf">content</span><span class="p">()</span>  <span class="c1">// 在 builder 上下文中执行 content 函数</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"UI构建完成"</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// 看看使用起来多自然 - 就像是在描述UI结构</span>
<span class="nf">buildUI</span> <span class="p">{</span>
    <span class="nf">text</span><span class="p">(</span><span class="s">"欢迎使用我的应用"</span><span class="p">)</span>
    <span class="nf">image</span><span class="p">(</span><span class="s">"header.png"</span><span class="p">)</span>
    <span class="nf">button</span><span class="p">(</span><span class="s">"点击登录"</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// 处理点击事件</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"用户点击了登录按钮"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这个例子是不是更容易理解了？<code class="language-plaintext highlighter-rouge">buildUI</code> 函数接收一个带接收者的函数 <code class="language-plaintext highlighter-rouge">UIBuilder.() -&gt; Unit</code>，这使得在调用时，lambda 表达式内部的 <code class="language-plaintext highlighter-rouge">this</code> 指向 <code class="language-plaintext highlighter-rouge">UIBuilder</code> 实例，所以可以直接调用 <code class="language-plaintext highlighter-rouge">text()</code>、<code class="language-plaintext highlighter-rouge">image()</code> 和 <code class="language-plaintext highlighter-rouge">button()</code> 方法，就好像这些方法是在当前作用域中定义的一样。</p>

<p>这正是 Compose 的核心设计理念 - 通过带接收者的函数创造出一种声明式的 UI 构建方式，让代码读起来就像是在描述 UI 结构，而不是一堆函数调用。</p>

<h2 id="compose-dsl魔法背后的秘密">Compose DSL：魔法背后的秘密</h2>

<p>Compose 的 DSL 正是建立在这些 Kotlin 特性之上，但它还有自己的独特魔法。</p>

<h3 id="composable-注解远不止是个标记">@Composable 注解：远不止是个标记</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">Greeting</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span>
    <span class="nc">Text</span><span class="p">(</span><span class="s">"你好, $name!"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这个看似简单的注解，背后其实暗藏玄机。Compose 编译器会对标记了 <code class="language-plaintext highlighter-rouge">@Composable</code> 的函数进行特殊处理：</p>

<ol>
  <li>添加隐式参数（比如 <code class="language-plaintext highlighter-rouge">Composer</code> 对象）</li>
  <li>插入跟踪代码，用于检测状态变化和触发重组</li>
  <li>生成唯一标识，用于在组合树中定位</li>
</ol>

<h3 id="作用域控制上下文感知的-api">作用域控制：上下文感知的 API</h3>

<p>Compose 中的作用域控制是我最喜欢的特性之一：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Column</span> <span class="p">{</span>
    <span class="c1">// 这里可以使用 ColumnScope 的方法</span>
    <span class="nc">Text</span><span class="p">(</span><span class="s">"标题"</span><span class="p">,</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">weight</span><span class="p">(</span><span class="mf">1f</span><span class="p">))</span>
    
    <span class="nc">Row</span> <span class="p">{</span>
        <span class="c1">// 这里可以使用 RowScope 的方法</span>
        <span class="nc">Text</span><span class="p">(</span><span class="s">"左侧"</span><span class="p">,</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">weight</span><span class="p">(</span><span class="mf">0.3f</span><span class="p">))</span>
        <span class="nc">Text</span><span class="p">(</span><span class="s">"右侧"</span><span class="p">,</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">weight</span><span class="p">(</span><span class="mf">0.7f</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>注意到没有？<code class="language-plaintext highlighter-rouge">weight()</code> 修饰符只能在特定作用域中使用。这种设计太巧妙了，它确保了 API 的上下文相关性，防止了错误使用。</p>

<h2 id="配置与渲染分离我最爱的设计模式">配置与渲染分离：我最爱的设计模式</h2>

<p>在深入使用 Compose 后，我发现”配置与渲染分离”是一种非常实用的设计模式。这种模式将”做什么”和”如何做”清晰地分开：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">CustomCard</span><span class="p">(</span>
    <span class="n">modifier</span><span class="p">:</span> <span class="nc">Modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">,</span>
    <span class="n">content</span><span class="p">:</span> <span class="nc">CardScope</span><span class="p">.()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="c1">// 1. 配置阶段：收集所有信息</span>
    <span class="kd">val</span> <span class="py">cardScope</span> <span class="p">=</span> <span class="nc">CardScopeImpl</span><span class="p">().</span><span class="nf">apply</span><span class="p">(</span><span class="n">content</span><span class="p">)</span>
    
    <span class="c1">// 2. 渲染阶段：根据配置渲染UI</span>
    <span class="nc">Card</span><span class="p">(</span><span class="n">modifier</span> <span class="p">=</span> <span class="n">modifier</span><span class="p">)</span> <span class="p">{</span>
        <span class="nc">Column</span> <span class="p">{</span>
            <span class="n">cardScope</span><span class="p">.</span><span class="n">headerContent</span><span class="o">?.</span><span class="nf">invoke</span><span class="p">()</span>
            <span class="n">cardScope</span><span class="p">.</span><span class="n">mainContent</span><span class="o">?.</span><span class="nf">invoke</span><span class="p">()</span>
            <span class="n">cardScope</span><span class="p">.</span><span class="n">footerContent</span><span class="o">?.</span><span class="nf">invoke</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这种模式在我们团队的项目中被广泛应用，特别是在构建复杂的自定义组件时，效果特别好。它让代码结构更清晰，也更容易维护。</p>

<h2 id="实战案例构建一个自定义表单组件">实战案例：构建一个自定义表单组件</h2>

<p>来看一个实际的例子，这是我最近在项目中实现的一个表单组件的简化版：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 1. 定义作用域接口</span>
<span class="kd">interface</span> <span class="nc">FormScope</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">textField</span><span class="p">(</span><span class="n">key</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">label</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">validator</span><span class="p">:</span> <span class="p">((</span><span class="nc">String</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Boolean</span><span class="p">)?</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span>
    <span class="k">fun</span> <span class="nf">passwordField</span><span class="p">(</span><span class="n">key</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">label</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>
    <span class="k">fun</span> <span class="nf">submitButton</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">onClick</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// 2. 实现作用域</span>
<span class="kd">class</span> <span class="nc">FormScopeImpl</span> <span class="p">:</span> <span class="nc">FormScope</span> <span class="p">{</span>
    <span class="c1">// 存储表单项配置</span>
    <span class="kd">val</span> <span class="py">items</span> <span class="p">=</span> <span class="n">mutableListOf</span><span class="p">&lt;</span><span class="nc">FormItem</span><span class="p">&gt;()</span>
    <span class="kd">var</span> <span class="py">submitConfig</span><span class="p">:</span> <span class="nc">SubmitConfig</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span>
    
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">textField</span><span class="p">(</span><span class="n">key</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">label</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">validator</span><span class="p">:</span> <span class="p">((</span><span class="nc">String</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Boolean</span><span class="p">)?)</span> <span class="p">{</span>
        <span class="n">items</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">TextFieldItem</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">label</span><span class="p">,</span> <span class="n">validator</span><span class="p">))</span>
    <span class="p">}</span>
    
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">passwordField</span><span class="p">(</span><span class="n">key</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">label</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">items</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">PasswordFieldItem</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">label</span><span class="p">))</span>
    <span class="p">}</span>
    
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">submitButton</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">onClick</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">submitConfig</span> <span class="p">=</span> <span class="nc">SubmitConfig</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">onClick</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="c1">// 3. 渲染方法</span>
    <span class="nd">@Composable</span>
    <span class="k">fun</span> <span class="nf">Render</span><span class="p">(</span><span class="n">modifier</span><span class="p">:</span> <span class="nc">Modifier</span><span class="p">)</span> <span class="p">{</span>
        <span class="nc">Column</span><span class="p">(</span><span class="n">modifier</span> <span class="p">=</span> <span class="n">modifier</span><span class="p">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">16</span><span class="p">.</span><span class="n">dp</span><span class="p">))</span> <span class="p">{</span>
            <span class="c1">// 渲染表单项</span>
            <span class="n">items</span><span class="p">.</span><span class="nf">forEach</span> <span class="p">{</span> <span class="n">item</span> <span class="p">-&gt;</span>
                <span class="k">when</span> <span class="p">(</span><span class="n">item</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">is</span> <span class="nc">TextFieldItem</span> <span class="p">-&gt;</span> <span class="p">{</span>
                        <span class="kd">var</span> <span class="py">text</span> <span class="k">by</span> <span class="nf">remember</span> <span class="p">{</span> <span class="nf">mutableStateOf</span><span class="p">(</span><span class="s">""</span><span class="p">)</span> <span class="p">}</span>
                        <span class="nc">OutlinedTextField</span><span class="p">(</span>
                            <span class="n">value</span> <span class="p">=</span> <span class="n">text</span><span class="p">,</span>
                            <span class="n">onValueChange</span> <span class="p">=</span> <span class="p">{</span> <span class="n">text</span> <span class="p">=</span> <span class="n">it</span> <span class="p">},</span>
                            <span class="n">label</span> <span class="p">=</span> <span class="p">{</span> <span class="nc">Text</span><span class="p">(</span><span class="n">item</span><span class="p">.</span><span class="n">label</span><span class="p">)</span> <span class="p">},</span>
                            <span class="n">modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">fillMaxWidth</span><span class="p">().</span><span class="nf">padding</span><span class="p">(</span><span class="n">vertical</span> <span class="p">=</span> <span class="mi">8</span><span class="p">.</span><span class="n">dp</span><span class="p">)</span>
                        <span class="p">)</span>
                    <span class="p">}</span>
                    <span class="k">is</span> <span class="nc">PasswordFieldItem</span> <span class="p">-&gt;</span> <span class="p">{</span>
                        <span class="kd">var</span> <span class="py">text</span> <span class="k">by</span> <span class="nf">remember</span> <span class="p">{</span> <span class="nf">mutableStateOf</span><span class="p">(</span><span class="s">""</span><span class="p">)</span> <span class="p">}</span>
                        <span class="kd">var</span> <span class="py">visible</span> <span class="k">by</span> <span class="nf">remember</span> <span class="p">{</span> <span class="nf">mutableStateOf</span><span class="p">(</span><span class="k">false</span><span class="p">)</span> <span class="p">}</span>
                        <span class="nc">OutlinedTextField</span><span class="p">(</span>
                            <span class="n">value</span> <span class="p">=</span> <span class="n">text</span><span class="p">,</span>
                            <span class="n">onValueChange</span> <span class="p">=</span> <span class="p">{</span> <span class="n">text</span> <span class="p">=</span> <span class="n">it</span> <span class="p">},</span>
                            <span class="n">label</span> <span class="p">=</span> <span class="p">{</span> <span class="nc">Text</span><span class="p">(</span><span class="n">item</span><span class="p">.</span><span class="n">label</span><span class="p">)</span> <span class="p">},</span>
                            <span class="n">visualTransformation</span> <span class="p">=</span> <span class="k">if</span> <span class="p">(</span><span class="n">visible</span><span class="p">)</span> <span class="nc">VisualTransformation</span><span class="p">.</span><span class="nc">None</span> 
                                                  <span class="k">else</span> <span class="nc">PasswordVisualTransformation</span><span class="p">(),</span>
                            <span class="n">trailingIcon</span> <span class="p">=</span> <span class="p">{</span>
                                <span class="nc">IconButton</span><span class="p">(</span><span class="n">onClick</span> <span class="p">=</span> <span class="p">{</span> <span class="n">visible</span> <span class="p">=</span> <span class="p">!</span><span class="n">visible</span> <span class="p">})</span> <span class="p">{</span>
                                    <span class="nc">Icon</span><span class="p">(</span>
                                        <span class="k">if</span> <span class="p">(</span><span class="n">visible</span><span class="p">)</span> <span class="nc">Icons</span><span class="p">.</span><span class="nc">Default</span><span class="p">.</span><span class="nc">Visibility</span> 
                                        <span class="k">else</span> <span class="nc">Icons</span><span class="p">.</span><span class="nc">Default</span><span class="p">.</span><span class="nc">VisibilityOff</span><span class="p">,</span>
                                        <span class="n">contentDescription</span> <span class="p">=</span> <span class="k">null</span>
                                    <span class="p">)</span>
                                <span class="p">}</span>
                            <span class="p">},</span>
                            <span class="n">modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">fillMaxWidth</span><span class="p">().</span><span class="nf">padding</span><span class="p">(</span><span class="n">vertical</span> <span class="p">=</span> <span class="mi">8</span><span class="p">.</span><span class="n">dp</span><span class="p">)</span>
                        <span class="p">)</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">}</span>
            
            <span class="c1">// 渲染提交按钮</span>
            <span class="n">submitConfig</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span> <span class="n">config</span> <span class="p">-&gt;</span>
                <span class="nc">Button</span><span class="p">(</span>
                    <span class="n">onClick</span> <span class="p">=</span> <span class="n">config</span><span class="p">.</span><span class="n">onClick</span><span class="p">,</span>
                    <span class="n">modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">fillMaxWidth</span><span class="p">().</span><span class="nf">padding</span><span class="p">(</span><span class="n">vertical</span> <span class="p">=</span> <span class="mi">16</span><span class="p">.</span><span class="n">dp</span><span class="p">)</span>
                <span class="p">)</span> <span class="p">{</span>
                    <span class="nc">Text</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// 4. 主要的可组合函数</span>
<span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">Form</span><span class="p">(</span>
    <span class="n">modifier</span><span class="p">:</span> <span class="nc">Modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">,</span>
    <span class="n">content</span><span class="p">:</span> <span class="nc">FormScope</span><span class="p">.()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">formScope</span> <span class="p">=</span> <span class="nc">FormScopeImpl</span><span class="p">().</span><span class="nf">apply</span><span class="p">(</span><span class="n">content</span><span class="p">)</span>
    <span class="n">formScope</span><span class="p">.</span><span class="nc">Render</span><span class="p">(</span><span class="n">modifier</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>使用起来就像这样：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Form</span> <span class="p">{</span>
    <span class="nf">textField</span><span class="p">(</span><span class="s">"username"</span><span class="p">,</span> <span class="s">"用户名"</span><span class="p">)</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">isNotEmpty</span><span class="p">()</span> <span class="p">}</span>
    <span class="nf">passwordField</span><span class="p">(</span><span class="s">"password"</span><span class="p">,</span> <span class="s">"密码"</span><span class="p">)</span>
    <span class="nf">submitButton</span><span class="p">(</span><span class="s">"登录"</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// 处理表单提交</span>
        <span class="kd">val</span> <span class="py">username</span> <span class="p">=</span> <span class="n">formValues</span><span class="p">[</span><span class="s">"username"</span><span class="p">]</span> <span class="o">?:</span> <span class="s">""</span>
        <span class="kd">val</span> <span class="py">password</span> <span class="p">=</span> <span class="n">formValues</span><span class="p">[</span><span class="s">"password"</span><span class="p">]</span> <span class="o">?:</span> <span class="s">""</span>
        <span class="n">viewModel</span><span class="p">.</span><span class="nf">login</span><span class="p">(</span><span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>是不是感觉非常直观？这就是 DSL 的魅力！</p>

<h2 id="实战经验分享dsl-设计的几个关键点">实战经验分享：DSL 设计的几个关键点</h2>

<p>在实际项目中设计和使用 DSL 时，我总结了几点经验：</p>

<h3 id="1-保持作用域专注">1. 保持作用域专注</h3>

<p>每个作用域应该只关注一个特定的功能领域。比如在我们的项目中，有专门的 <code class="language-plaintext highlighter-rouge">ChartScope</code>、<code class="language-plaintext highlighter-rouge">FormScope</code>、<code class="language-plaintext highlighter-rouge">DialogScope</code> 等，各司其职：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 好的做法</span>
<span class="kd">interface</span> <span class="nc">ChartScope</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">title</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>
    <span class="k">fun</span> <span class="nf">xAxis</span><span class="p">(</span><span class="n">labels</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">&gt;)</span>
    <span class="k">fun</span> <span class="nf">series</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Float</span><span class="p">&gt;,</span> <span class="n">color</span><span class="p">:</span> <span class="nc">Color</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// 避免这样</span>
<span class="kd">interface</span> <span class="nc">MegaScope</span> <span class="p">{</span>
    <span class="c1">// 包含了太多不相关的功能</span>
    <span class="k">fun</span> <span class="nf">chartTitle</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>
    <span class="k">fun</span> <span class="nf">formField</span><span class="p">(</span><span class="n">key</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">label</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>
    <span class="k">fun</span> <span class="nf">dialogButton</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="2-使用扩展函数增强-dsl">2. 使用扩展函数增强 DSL</h3>

<p>我特别喜欢用扩展函数来增强 DSL 的表达能力：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 基本作用域</span>
<span class="kd">interface</span> <span class="nc">CardScope</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">header</span><span class="p">(</span><span class="n">content</span><span class="p">:</span> <span class="nd">@Composable</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span>
    <span class="k">fun</span> <span class="nf">content</span><span class="p">(</span><span class="n">content</span><span class="p">:</span> <span class="nd">@Composable</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// 通过扩展函数增强</span>
<span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nc">CardScope</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">header</span> <span class="p">{</span>
        <span class="nc">Text</span><span class="p">(</span>
            <span class="n">text</span> <span class="p">=</span> <span class="n">text</span><span class="p">,</span>
            <span class="n">style</span> <span class="p">=</span> <span class="nc">MaterialTheme</span><span class="p">.</span><span class="n">typography</span><span class="p">.</span><span class="n">titleLarge</span><span class="p">,</span>
            <span class="n">fontWeight</span> <span class="p">=</span> <span class="nc">FontWeight</span><span class="p">.</span><span class="nc">Bold</span>
        <span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// 使用</span>
<span class="nc">CustomCard</span> <span class="p">{</span>
    <span class="nf">title</span><span class="p">(</span><span class="s">"这比直接用header更直观"</span><span class="p">)</span>
    <span class="nf">content</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这种方式让 DSL 更加灵活，也更容易扩展。</p>

<h3 id="3-验证必要的配置">3. 验证必要的配置</h3>

<p>别忘了在渲染前验证必要的配置，这能避免很多运行时错误：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">Chart</span><span class="p">(</span><span class="n">content</span><span class="p">:</span> <span class="nc">ChartScope</span><span class="p">.()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">scope</span> <span class="p">=</span> <span class="nc">ChartScope</span><span class="p">().</span><span class="nf">apply</span><span class="p">(</span><span class="n">content</span><span class="p">)</span>
    
    <span class="c1">// 验证必要的配置</span>
    <span class="nf">require</span><span class="p">(</span><span class="n">scope</span><span class="p">.</span><span class="n">data</span><span class="p">.</span><span class="nf">isNotEmpty</span><span class="p">())</span> <span class="p">{</span> <span class="s">"Chart must have data!"</span> <span class="p">}</span>
    
    <span class="c1">// 渲染</span>
    <span class="n">scope</span><span class="p">.</span><span class="nc">Render</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这一点在我们的项目中帮助捕获了很多潜在问题，特别是当多人协作时。</p>

<h2 id="性能考虑dsl-不是没有代价的">性能考虑：DSL 不是没有代价的</h2>

<p>虽然 DSL 让代码更优雅，但也要注意它的性能影响。在我的实践中，有几点值得注意：</p>

<ol>
  <li><strong>避免过度嵌套</strong>：过深的组合嵌套会增加重组成本</li>
  <li><strong>合理使用 remember</strong>：缓存那些创建成本高的对象</li>
  <li><strong>注意闭包捕获</strong>：lambda 中捕获的变量会影响重组范围</li>
</ol>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 不好的做法</span>
<span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">BadExample</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">items</span> <span class="p">=</span> <span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">1000</span><span class="p">).</span><span class="nf">toList</span><span class="p">()</span>
    <span class="nc">LazyColumn</span> <span class="p">{</span>
        <span class="n">items</span><span class="p">.</span><span class="nf">forEach</span> <span class="p">{</span> <span class="n">item</span> <span class="p">-&gt;</span>  <span class="c1">// 这里每次重组都会创建新的闭包</span>
            <span class="nf">item</span> <span class="p">{</span>
                <span class="nc">Text</span><span class="p">(</span><span class="s">"Item $item"</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// 好的做法</span>
<span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">GoodExample</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">items</span> <span class="p">=</span> <span class="nf">remember</span> <span class="p">{</span> <span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">1000</span><span class="p">).</span><span class="nf">toList</span><span class="p">()</span> <span class="p">}</span>
    <span class="nc">LazyColumn</span> <span class="p">{</span>
        <span class="nf">items</span><span class="p">(</span><span class="n">items</span><span class="p">)</span> <span class="p">{</span> <span class="n">item</span> <span class="p">-&gt;</span>  <span class="c1">// 使用专门的API，避免不必要的闭包</span>
            <span class="nc">Text</span><span class="p">(</span><span class="s">"Item $item"</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="总结dsl--高阶函数--现代ui开发的未来">总结：DSL + 高阶函数 = 现代UI开发的未来</h2>

<p>通过这段时间的实践，我越来越确信 Kotlin 的高阶函数和 DSL 特性与 Compose 的结合，代表了现代 UI 开发的未来方向。它不仅让代码更加声明式、更易读，还提供了强大的类型安全和灵活性。</p>

<p>配置与渲染分离模式更是锦上添花，让复杂组件的开发变得更加结构化和可维护。如果你还没有深入探索这些特性，强烈建议你在下一个项目中尝试应用它们。</p>

<p>最后，分享一个我的小技巧：当设计 DSL 时，先从使用者的角度思考 API 应该是什么样子，然后再去实现它。这种”API 优先”的思维方式，往往能带来更好的用户体验。</p>

<p>希望这篇文章对你有所帮助，欢迎在评论区分享你使用 Compose DSL 的经验和技巧！</p>]]></content><author><name>CaptainZ</name><email>jelychow@gmail.com</email></author><category term="Jetpack Compose" /><category term="Kotlin" /><category term="DSL" /><category term="高阶函数" /><category term="声明式 UI" /><summary type="html"><![CDATA[深入探讨 Compose DSL 与 Kotlin 高阶函数如何协同工作，打造现代化声明式 UI。从实战角度剖析核心机制、设计模式与最佳实践，帮你掌握这套强大的开发范式。]]></summary></entry><entry><title type="html">移动端图像搜索革命</title><link href="https://jelychow.github.io/%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%9B%BE%E5%83%8F%E6%90%9C%E7%B4%A2%E9%9D%A9%E5%91%BD/" rel="alternate" type="text/html" title="移动端图像搜索革命" /><published>2025-07-04T00:00:00+00:00</published><updated>2025-07-04T00:00:00+00:00</updated><id>https://jelychow.github.io/%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%9B%BE%E5%83%8F%E6%90%9C%E7%B4%A2%E9%9D%A9%E5%91%BD</id><content type="html" xml:base="https://jelychow.github.io/%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%9B%BE%E5%83%8F%E6%90%9C%E7%B4%A2%E9%9D%A9%E5%91%BD/"><![CDATA[<h2 id="引言">引言</h2>

<p>在移动应用领域，图像搜索功能传统上受到移动设备计算能力的限制。计算图像之间的相似度——尤其是在大型图片集合中——一直是一个资源密集型过程，常导致性能瓶颈、电池消耗和用户体验下降。</p>

<p>本文探讨了PicQuery应用如何通过实现向量数据库方法来革新移动端图像搜索，替代传统的相似度计算。我们将研究这种方法的实际工程挑战、实现细节和性能优势。</p>

<h2 id="移动端图像相似度计算的挑战">移动端图像相似度计算的挑战</h2>

<p>移动设备上的传统图像相似度计算方法通常包括：</p>

<ul>
  <li><strong>成对比较</strong>：计算每对图像之间的相似度分数，导致O(n²)复杂度</li>
  <li><strong>内存处理</strong>：计算过程中在内存中保存大量嵌入向量</li>
  <li><strong>顺序处理</strong>：由于移动硬件限制导致并行化能力有限</li>
  <li><strong>批处理</strong>：在后台作业中执行计算，通常会有明显延迟</li>
</ul>

<p>随着用户照片库的增长，这些方法变得越来越低效。对于仅1,000张图像的集合，可能需要近百万次比较才能识别相似照片。</p>

<h2 id="向量数据库移动端友好的解决方案">向量数据库：移动端友好的解决方案</h2>

<p>向量数据库通过提供高维向量的优化存储和检索，提供了一种引人注目的替代方案。在PicQuery的实现中，我们利用ObjectBox的向量数据库功能显著提高了搜索性能。</p>

<h3 id="实现的关键组件">实现的关键组件</h3>

<h4 id="1-使用hnsw索引的向量表示">1. 使用HNSW索引的向量表示</h4>

<p>我们实现的核心是 <code class="language-plaintext highlighter-rouge">ObjectBoxEmbedding</code> 模型：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Entity</span>
<span class="kd">data class</span> <span class="nc">ObjectBoxEmbedding</span><span class="p">(</span>
    <span class="nd">@Id</span>
    <span class="kd">var</span> <span class="py">id</span><span class="p">:</span> <span class="nc">Long</span> <span class="p">=</span> <span class="mi">0</span><span class="p">,</span>

    <span class="nd">@Index</span>
    <span class="kd">val</span> <span class="py">photoId</span><span class="p">:</span> <span class="nc">Long</span><span class="p">,</span>

    <span class="nd">@Index</span>
    <span class="kd">val</span> <span class="py">albumId</span><span class="p">:</span> <span class="nc">Long</span><span class="p">,</span>

    <span class="nd">@HnswIndex</span><span class="p">(</span>
        <span class="n">dimensions</span> <span class="p">=</span> <span class="mi">512</span><span class="p">,</span>
        <span class="n">distanceType</span> <span class="p">=</span> <span class="nc">VectorDistanceType</span><span class="p">.</span><span class="nc">COSINE</span>
    <span class="p">)</span>
    <span class="kd">val</span> <span class="py">data</span><span class="p">:</span> <span class="nc">FloatArray</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">Serializable</span>
</code></pre></div></div>

<p>这里的关键注解是<code class="language-plaintext highlighter-rouge">@HnswIndex</code>，它为向量数据创建了层次导航小世界(HNSW)图索引。这种索引结构使得近似最近邻搜索的复杂度从线性扫描降低到对数级别。</p>

<h4 id="2-高效的向量搜索实现">2. 高效的向量搜索实现</h4>

<p>搜索功能在<code class="language-plaintext highlighter-rouge">ObjectBoxEmbeddingDao</code>中实现：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">searchNearestVectors</span><span class="p">(</span>
    <span class="n">queryVector</span><span class="p">:</span> <span class="nc">FloatArray</span><span class="p">,</span>
    <span class="n">topK</span><span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">10</span><span class="p">,</span>
    <span class="n">similarityThreshold</span><span class="p">:</span> <span class="nc">Float</span> <span class="p">=</span> <span class="mf">0.7f</span><span class="p">,</span>
    <span class="n">albumIds</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Long</span><span class="p">&gt;?</span> <span class="p">=</span> <span class="k">null</span>
<span class="p">):</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">ObjectWithScore</span><span class="p">&lt;</span><span class="nc">ObjectBoxEmbedding</span><span class="p">&gt;&gt;</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">query</span> <span class="p">=</span> <span class="n">embeddingBox</span>
        <span class="p">.</span><span class="nf">query</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">nearestNeighbors</span><span class="p">(</span><span class="nc">ObjectBoxEmbedding_</span><span class="p">.</span><span class="n">data</span><span class="p">,</span> <span class="n">queryVector</span><span class="p">,</span> <span class="n">topK</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">build</span><span class="p">()</span>

    <span class="kd">val</span> <span class="py">results</span> <span class="p">=</span> <span class="n">query</span><span class="p">.</span><span class="nf">findWithScores</span><span class="p">().</span><span class="nf">filter</span> <span class="p">{</span> <span class="n">result</span> <span class="p">-&gt;</span>
        <span class="kd">val</span> <span class="py">cosineSimilarity</span> <span class="p">=</span> <span class="mf">1.0</span> <span class="p">-</span> <span class="n">result</span><span class="p">.</span><span class="n">score</span>
        <span class="n">cosineSimilarity</span> <span class="p">&gt;</span> <span class="n">similarityThreshold</span>
    <span class="p">}</span>
    
    <span class="k">return</span> <span class="n">results</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这种实现利用了数据库内置的最近邻搜索功能，这些功能针对移动性能进行了优化。</p>

<h2 id="工程挑战与解决方案">工程挑战与解决方案</h2>

<h3 id="1-嵌入向量生成与存储">1. 嵌入向量生成与存储</h3>

<p>主要挑战之一是高效地为潜在的数千张图像生成和存储嵌入向量。我们的解决方案包括：</p>

<ul>
  <li><strong>批处理</strong>：分批处理图像以避免内存问题</li>
  <li><strong>增量更新</strong>：只为新增或修改的图像生成嵌入向量</li>
  <li><strong>持久化存储</strong>：立即持久化嵌入向量以避免数据丢失</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">suspend</span> <span class="k">fun</span> <span class="nf">saveBitmapsToEmbedding</span><span class="p">(</span>
    <span class="n">items</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">PhotoBitmap</span><span class="p">?&gt;,</span>
    <span class="n">imageEncoder</span><span class="p">:</span> <span class="nc">ImageEncoder</span><span class="p">,</span>
    <span class="n">embeddingRepository</span><span class="p">:</span> <span class="nc">EmbeddingRepository</span><span class="p">,</span>
    <span class="n">embeddingObjectRepository</span><span class="p">:</span> <span class="nc">ObjectBoxEmbeddingRepository</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="nf">coroutineScope</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">embeddings</span> <span class="p">=</span> <span class="n">imageEncoder</span><span class="p">.</span><span class="nf">encodeBatch</span><span class="p">(</span><span class="n">items</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="o">!!</span><span class="p">.</span><span class="n">bitmap</span> <span class="p">})</span>
        <span class="n">embeddings</span><span class="p">.</span><span class="nf">forEachIndexed</span> <span class="p">{</span> <span class="n">index</span><span class="p">,</span> <span class="n">feat</span> <span class="p">-&gt;</span>
            <span class="n">embeddingObjectRepository</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span>
                <span class="nc">ObjectBoxEmbedding</span><span class="p">(</span>
                    <span class="n">photoId</span> <span class="p">=</span> <span class="n">items</span><span class="p">[</span><span class="n">index</span><span class="p">]</span><span class="o">!!</span><span class="p">.</span><span class="n">photo</span><span class="p">.</span><span class="n">id</span><span class="p">,</span>
                    <span class="n">albumId</span> <span class="p">=</span> <span class="n">items</span><span class="p">[</span><span class="n">index</span><span class="p">]</span><span class="o">!!</span><span class="p">.</span><span class="n">photo</span><span class="p">.</span><span class="n">albumID</span><span class="p">,</span>
                    <span class="n">data</span> <span class="p">=</span> <span class="n">feat</span>
                <span class="p">)</span>
            <span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="2-针对不同用例的查询优化">2. 针对不同用例的查询优化</h3>

<p>不同的搜索场景需要不同的查询策略：</p>

<ul>
  <li><strong>文本到图像搜索</strong>：将文本查询转换为嵌入向量</li>
  <li><strong>图像到图像搜索</strong>：直接使用图像的嵌入向量</li>
  <li><strong>特定相册搜索</strong>：将搜索限制在特定相册中</li>
</ul>

<p>我们的实现通过灵活的查询参数适应这些场景：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">searchWithVectorV2</span><span class="p">(</span>
    <span class="n">range</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Album</span><span class="p">&gt;,</span>
    <span class="n">textFeat</span><span class="p">:</span> <span class="nc">FloatArray</span>
<span class="p">):</span> <span class="nc">MutableList</span><span class="p">&lt;</span><span class="nc">Pair</span><span class="p">&lt;</span><span class="nc">Long</span><span class="p">,</span> <span class="nc">Double</span><span class="p">&gt;&gt;</span> <span class="p">=</span> <span class="nf">withContext</span><span class="p">(</span><span class="n">dispatcher</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">albumIds</span> <span class="p">=</span> <span class="k">if</span> <span class="p">(</span><span class="n">range</span><span class="p">.</span><span class="nf">isEmpty</span><span class="p">()</span> <span class="p">||</span> <span class="n">isSearchAll</span><span class="p">.</span><span class="n">value</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">null</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">range</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">id</span> <span class="p">}</span>
    <span class="p">}</span>

    <span class="kd">val</span> <span class="py">searchResults</span> <span class="p">=</span> <span class="n">objectBoxEmbeddingRepository</span><span class="p">.</span><span class="nf">searchNearestVectors</span><span class="p">(</span>
        <span class="n">queryVector</span> <span class="p">=</span> <span class="n">textFeat</span><span class="p">,</span>
        <span class="n">topK</span> <span class="p">=</span> <span class="n">resultCount</span><span class="p">.</span><span class="n">value</span><span class="p">,</span>
        <span class="n">similarityThreshold</span> <span class="p">=</span> <span class="n">matchThreshold</span><span class="p">.</span><span class="n">value</span><span class="p">,</span>
        <span class="n">albumIds</span> <span class="p">=</span> <span class="n">albumIds</span>
    <span class="p">)</span>
    
    <span class="k">return</span><span class="nd">@withContext</span> <span class="n">searchResults</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="k">get</span><span class="p">().</span><span class="n">photoId</span> <span class="n">to</span> <span class="n">it</span><span class="p">.</span><span class="n">score</span> <span class="p">}.</span><span class="nf">toMutableList</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="3-基于相似度的照片分组">3. 基于相似度的照片分组</h3>

<p>除了简单搜索外，我们还实现了基于相似度的高效照片分组：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">fun</span> <span class="nf">unionFindSimilarityGroups</span><span class="p">(</span>
    <span class="n">photos</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">SimilarityNode</span><span class="p">&gt;,</span>
    <span class="n">similarityThreshold</span><span class="p">:</span> <span class="nc">Float</span> <span class="p">=</span> <span class="mf">0.95f</span>
<span class="p">):</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">List</span><span class="p">&lt;</span><span class="nc">SimilarityNode</span><span class="p">&gt;&gt;</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">embeddings</span> <span class="p">=</span> <span class="n">embeddingRepository</span><span class="p">.</span><span class="nf">getByPhotoIds</span><span class="p">(</span><span class="n">photos</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">photoId</span> <span class="p">}.</span><span class="nf">toLongArray</span><span class="p">())</span>
    <span class="kd">val</span> <span class="py">unionFind</span> <span class="p">=</span> <span class="nc">UnionFind</span><span class="p">(</span><span class="n">photos</span><span class="p">.</span><span class="n">size</span><span class="p">)</span>

    <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="n">photos</span><span class="p">.</span><span class="n">indices</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="n">j</span> <span class="k">in</span> <span class="n">i</span> <span class="p">+</span> <span class="mi">1</span> <span class="n">until</span> <span class="n">photos</span><span class="p">.</span><span class="n">size</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">similarity</span> <span class="p">=</span> <span class="nf">calculateSimilarity</span><span class="p">(</span>
                <span class="n">embeddings</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">data</span><span class="p">.</span><span class="nf">toFloatArray</span><span class="p">(),</span>
                <span class="n">embeddings</span><span class="p">[</span><span class="n">j</span><span class="p">].</span><span class="n">data</span><span class="p">.</span><span class="nf">toFloatArray</span><span class="p">()</span>
            <span class="p">)</span>

            <span class="k">if</span> <span class="p">(</span><span class="n">similarity</span> <span class="p">&gt;=</span> <span class="n">similarityThreshold</span><span class="p">.</span><span class="nf">toDouble</span><span class="p">())</span> <span class="p">{</span>
                <span class="n">unionFind</span><span class="p">.</span><span class="nf">union</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">unionFind</span><span class="p">.</span><span class="nf">getGroups</span><span class="p">().</span><span class="nf">map</span> <span class="p">{</span> <span class="n">group</span> <span class="p">-&gt;</span>
        <span class="n">group</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">index</span> <span class="p">-&gt;</span>
            <span class="n">photos</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这种方法使用并查集算法高效地对相似照片进行分组，无需进行穷尽比较。</p>

<h2 id="性能优势">性能优势</h2>

<p>向量数据库方法带来了几项显著的性能改进：</p>

<h3 id="1-搜索速度">1. 搜索速度</h3>

<p>传统的相似度搜索对于大型照片集合可能需要数秒甚至数分钟。使用向量数据库实现，搜索结果通常在<strong>毫秒级</strong>返回，提供近乎即时的用户体验。</p>

<h3 id="2-内存效率">2. 内存效率</h3>

<p>通过将向量存储和搜索卸载到数据库，应用程序的内存占用显著减少。这在内存限制严格的移动设备上尤为重要。</p>

<h3 id="3-电池消耗">3. 电池消耗</h3>

<p>优化的搜索算法减少了CPU使用率，降低了电池消耗——这是移动应用的关键因素。</p>

<h3 id="4-可扩展性">4. 可扩展性</h3>

<p>HNSW索引方法的扩展是对数级而非线性级，这意味着即使用户的照片集合增长到数万张图像，搜索性能仍然出色。</p>

<h2 id="移动开发者的实施考虑">移动开发者的实施考虑</h2>

<p>如果您考虑在移动应用中实施类似方法，以下是一些实用考虑因素：</p>

<h3 id="1-选择合适的向量数据库">1. 选择合适的向量数据库</h3>

<p>虽然我们使用ObjectBox进行实现，但移动平台上有几种选择：</p>

<ul>
  <li><strong>ObjectBox</strong>：轻量级，针对移动设备优化，内置向量搜索</li>
  <li><strong>带向量扩展的SQLite</strong>：更广泛可用但可能优化程度较低</li>
  <li><strong>自定义实现</strong>：可能提供更多控制但需要大量开发工作</li>
</ul>

<h3 id="2-优化嵌入向量生成">2. 优化嵌入向量生成</h3>

<p>生成嵌入向量计算成本高昂。考虑：</p>

<ul>
  <li>卸载到后台服务</li>
  <li>使用专为移动设备设计的轻量级模型</li>
  <li>实施渐进增强（先生成低质量嵌入向量，然后改进）</li>
</ul>

<h3 id="3-平衡精度和性能">3. 平衡精度和性能</h3>

<p>更高维度的嵌入向量通常提供更好的相似度匹配，但消耗更多存储和处理能力。对于我们的实现，我们发现512维向量在准确性和性能之间取得了良好平衡。</p>

<h3 id="4-实现缓存策略">4. 实现缓存策略</h3>

<p>即使使用高效的向量数据库，缓存频繁访问的结果也可以进一步提高性能：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="kd">val</span> <span class="py">_cachedSimilarityGroups</span> <span class="p">=</span> <span class="nc">Collections</span><span class="p">.</span><span class="nf">synchronizedList</span><span class="p">(</span><span class="n">mutableListOf</span><span class="p">&lt;</span><span class="nc">List</span><span class="p">&lt;</span><span class="nc">SimilarityNode</span><span class="p">&gt;&gt;())</span>
<span class="k">private</span> <span class="kd">var</span> <span class="py">isFullyLoaded</span> <span class="p">=</span> <span class="k">false</span>
<span class="k">private</span> <span class="kd">val</span> <span class="py">cacheLock</span> <span class="p">=</span> <span class="nc">Any</span><span class="p">()</span>

<span class="k">fun</span> <span class="nf">getSimilarityGroupByIndex</span><span class="p">(</span><span class="n">index</span><span class="p">:</span> <span class="nc">Int</span><span class="p">):</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">SimilarityNode</span><span class="p">&gt;?</span> <span class="p">{</span>
    <span class="nf">synchronized</span><span class="p">(</span><span class="n">cacheLock</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(!</span><span class="n">isFullyLoaded</span><span class="p">)</span> <span class="k">return</span> <span class="k">null</span>

        <span class="k">return</span> <span class="k">if</span> <span class="p">(</span><span class="n">index</span> <span class="k">in</span> <span class="n">_cachedSimilarityGroups</span><span class="p">.</span><span class="n">indices</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">_cachedSimilarityGroups</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">null</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="结论">结论</h2>

<p>从传统相似度计算到向量数据库的转变代表了移动图像搜索技术的重大进步。通过利用优化的数据结构和搜索算法，PicQuery证明了复杂的图像搜索功能可以在移动设备上高效实现。</p>

<p>这种方法不仅改善了性能指标，还实现了以前在移动平台上不切实际的全新用户体验。随着向量数据库技术的不断成熟，我们可以期待移动领域出现更多创新应用。</p>

<p>对于开发图像密集型移动应用的开发者来说，向量数据库为相似度搜索的长期挑战提供了一个引人注目的解决方案。本文分享的实际实现细节为将这些技术集成到您自己的应用中提供了起点，有可能彻底改变用户在移动设备上与视觉内容的交互方式。</p>]]></content><author><name>CaptainZ</name><email>jelychow@gmail.com</email></author><summary type="html"><![CDATA[探索PicQuery应用如何通过向量数据库优化移动端图像搜索性能]]></summary></entry><entry><title type="html">Kotlin flow 全面解析：从基础到高级</title><link href="https://jelychow.github.io/Kotlin-Flow-%E5%85%A8%E9%9D%A2%E8%A7%A3%E6%9E%90-%E4%BB%8E%E5%9F%BA%E7%A1%80%E5%88%B0%E9%AB%98%E7%BA%A7/" rel="alternate" type="text/html" title="Kotlin flow 全面解析：从基础到高级" /><published>2025-06-05T00:00:00+00:00</published><updated>2025-06-05T00:00:00+00:00</updated><id>https://jelychow.github.io/Kotlin%20Flow%20%E5%85%A8%E9%9D%A2%E8%A7%A3%E6%9E%90%EF%BC%9A%E4%BB%8E%E5%9F%BA%E7%A1%80%E5%88%B0%E9%AB%98%E7%BA%A7</id><content type="html" xml:base="https://jelychow.github.io/Kotlin-Flow-%E5%85%A8%E9%9D%A2%E8%A7%A3%E6%9E%90-%E4%BB%8E%E5%9F%BA%E7%A1%80%E5%88%B0%E9%AB%98%E7%BA%A7/"><![CDATA[<h2 id="一个典型的-flow-包含哪些部分">一个典型的 flow 包含哪些部分？</h2>

<p>这是一个非常简单的问题，但是也最为我们所忽略。我们来看一个典型的 flow：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">flowA</span> <span class="p">=</span> <span class="nf">flowOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span> <span class="p">+</span> <span class="mi">1</span> <span class="p">}</span>

<span class="n">flow</span><span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-&gt;</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"Received $value"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>由上面的代码我们可以看到一个 flow，一般包含三个部分构造符，collector，还有操作符（可选）。</p>

<p><img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/6cb7697a76134d4a85b0e48655d4bf36~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ2FwdGFpblo=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzU0NDQ4MTIxNzM4MzkxMSJ9&amp;rk3s=f64ab15b&amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;x-orig-expires=1749707563&amp;x-orig-sign=GjTBDe1usA9f2LtnQmzVsjztOvc%3D" alt="image.png" /></p>

<h2 id="一个-flow-通常来说会经历过哪些过程">一个 flow 通常来说会经历过哪些过程？</h2>

<p>这个问题可以看作是对上个问题的补充，考察的是对 kotlin flow 的基础理解。<br />
<a href="https://developer.android.com/kotlin/flow?hl=zh-cn#context"><strong>android 官方文档</strong></a>详细介绍了每个流程。具体流程为</p>

<ol>
  <li>创建数据流</li>
  <li>修改数据流</li>
  <li>收集数据流</li>
  <li>捕获数据流异常</li>
  <li>在不同 Coroutine​Context 中运行</li>
</ol>

<p>希望大家都能牢记这些流程，他对我们后续其他问题的理解会有帮助。</p>

<h2 id="一个-flow-可以有多少个-collector">一个 flow 可以有多少个 collector？</h2>

<p>kotlin 本身并没有对 collector 数量进行限制，每个 collector 都会收到同样的数据。</p>

<h2 id="flow-到底是热的还是冷的">flow 到底是热的还是冷的？</h2>

<p>默认使用 builder 构造出来的 flow 是冷的，而 StateFlow，SharedFlow 是热的。</p>

<p><img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/501f979f2a9f41cb91fdbd1351874409~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ2FwdGFpblo=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzU0NDQ4MTIxNzM4MzkxMSJ9&amp;rk3s=f64ab15b&amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;x-orig-expires=1749707563&amp;x-orig-sign=g2hXeK8dQIGz8pQkWO8L5I4vxl4%3D" alt="image.png" /></p>

<h2 id="冷流热流的区别">冷流热流的区别</h2>

<p><strong>冷流 (Cold Flow):</strong></p>

<ul>
  <li><strong>惰性 (Lazy):</strong>  按需执行。</li>
  <li><strong>单播 (Unicast) per collection:</strong>  每个收集器获得独立的执行和数据。</li>
  <li><strong>生产者为每个收集器重新执行。</strong></li>
</ul>

<p><strong>热流 (Hot Flow):</strong></p>

<ul>
  <li><strong>主动 (Active):</strong>  可能在没有收集器的情况下就发射数据。</li>
  <li><strong>多播 (Multicast) / 广播 (Broadcast):</strong>  多个收集器共享来自同一生产者的数据。</li>
  <li><strong>生产者通常只执行一次（或独立于收集器执行）。</strong></li>
  <li>新收集器可能<strong>错过</strong>早期数据，除非 Flow 配置了重播 (replay) 机制。</li>
</ul>

<h3 id="那么-cold-flow-与-hot-flow-的-collect-执行起来有什么区别吗">那么 cold flow 与 hot flow 的 collect 执行起来有什么区别吗？</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">stateFlow</span> <span class="p">=</span> <span class="nc">MutableStateFlow</span><span class="p">(</span><span class="s">"初始值"</span><span class="p">)</span>

<span class="nf">launch</span> <span class="p">{</span> <span class="n">stateFlow</span><span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="s">"收集器1: $it"</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span>
<span class="nf">launch</span> <span class="p">{</span> <span class="n">stateFlow</span><span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="s">"收集器2: $it"</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span>
<span class="nf">launch</span> <span class="p">{</span> <span class="n">stateFlow</span><span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="s">"收集器3: $it"</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
<span class="n">stateFlow</span><span class="p">.</span><span class="nf">emit</span><span class="p">(</span><span class="s">"修改后的值"</span><span class="p">)</span>

<span class="kd">val</span> <span class="py">flowNormal</span> <span class="p">=</span> <span class="nf">flowOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="p">)</span>
<span class="nf">launch</span> <span class="p">{</span>
    <span class="n">flowNormal</span><span class="p">.</span><span class="nf">collect</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"flowNormal collector1 $it"</span><span class="p">)</span>

    <span class="p">}</span>
<span class="p">}</span>

<span class="nf">launch</span> <span class="p">{</span>
    <span class="n">flowNormal</span><span class="p">.</span><span class="nf">collect</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"flowNormal collector2: $it"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>看下上面代码，猜一猜会输出什么？</p>

<p>好了，展示一下执行结果，是不是有些奇怪，<strong>cold flow 每次收集都是独立的</strong>，这也是冷流热流的主要区别之一。</p>

<details>
  <summary>查看结果</summary>

print console：

*   收集器1: 初始值
*   收集器2: 初始值
*   收集器3: 初始值
*   收集器1: 修改后的值
*   收集器2: 修改后的值
*   收集器3: 修改后的值
*   flowNormal collector1 1
*   flowNormal collector1 2
*   flowNormal collector1 3
*   flowNormal collector2: 1
*   flowNormal collector2: 2
*   flowNormal collector2: 3

</details>

<h2 id="热流与冷流-collect-是否阻塞后续代码执行">热流与冷流 collect 是否阻塞后续代码执行？</h2>

<p>相信很多好奇的宝宝已经看到上面的代码都运行在独立的 coroutineScope 之中了，你可能会问 我如果去掉 scope 让他们运行在同一个 coroutineScope 可以不可以呢？下面我们改下代码，咱们看看情况。</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">stateFlow</span><span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="s">"收集器1: $it"</span><span class="p">)</span> <span class="p">}</span> 
<span class="n">stateFlow</span><span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="s">"收集器2: $it"</span><span class="p">)</span> <span class="p">}</span>
<span class="nf">delay</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
<span class="n">stateFlow</span><span class="p">.</span><span class="nf">emit</span><span class="p">(</span><span class="s">"修改后的值"</span><span class="p">)</span>
</code></pre></div></div>

<p>猜一下输出结果会是怎样？结果可能让很多人大吃一惊。</p>

<details>
  <summary>查看结果</summary>
  收集器1: 初始值
</details>

<p>我们可以看到 <code class="language-plaintext highlighter-rouge">stateFlow.collect { println("收集器1: $it") }</code> 这行代码执行完之后，阻塞了后续的代码执行，也就是说当前 flow 挂起在这里了，当然如果感兴趣的同学可以看看源码实现。最后这个规则一定要记住，热流需要放在单独的 scope 之中运行，不然会阻塞后续的任务执行，我曾经就写过一个相关的 bug, 我在 viewmodel scope 里面执行了两个 collect 但是第二个并没有正确执行。</p>

<details>
  <summary>查看源码</summary>

```kt
  // SharedFlow.kt中的接口
public interface SharedFlow<out T=""> : Flow<T> {
    public val replayCache: List<T>
}

// SharedFlowImpl.kt中的实现(简化)
internal class SharedFlowImpl<T>(
    private val replay: Int,
    private val extraBufferCapacity: Int,
    onBufferOverflow: BufferOverflow
) : MutableSharedFlow<T> {
    // 收集实现
    override suspend fun collect(collector: FlowCollector<T>) {
        val slot = allocateSlot()
        try {
            if (replay &gt; 0) {
                // 尝试从replay缓存中发射值
                val replaySnapshot = replayCache
                for (value in replaySnapshot) {
                    collector.emit(value)
                }
            }
            // 无限循环，等待新值
            while (true) {
                // 获取新值或挂起等待
                val newValue = awaitValue() 
                collector.emit(newValue)
            }
        } finally {
            freeSlot(slot)
        }
    }
    
    // 挂起等待新值
    private suspend fun awaitValue(): T {
        // 如果没有新值，这里会挂起协程，直到有新值发射
        return suspendCancellableCoroutine { continuation -&gt;
            // 将continuation保存到等待列表中
            // 当有新值时，会恢复这个continuation
        }
    }
}
```

&lt;/details&gt;

好了上面讲述了热流 collect 方法执行之后，我们来看一下冷流的 collect，贴上代码：

```kt
val stateFlow = MutableStateFlow("初始值")

val fowNormal = flowOf(1, 2, 3)
flowNormal.collect {
   println("flowNormal collector1 $it")
}

flowNormal.collect {
   println("flowNormal collector2: $it")
 }
```

结果见这里：

<details>
  <summary>查看结果</summary>
  结果  

*   flowNormal collector1 1
*   flowNormal collector1 2
*   flowNormal collector1 3
*   flowNormal collector2: 1
*   flowNormal collector2: 2
*   flowNormal collector2: 3

</details>    

通过上面的执行过程我们可以看到 当构建器代码块不发射任何值并正常结束时，`collect`操作会立即完成并继续执行后续代码，不会阻塞。这是因为`flow`构建器中的代码执行完毕后，收集过程就自然结束了。在这里需要说明的是，虽然 collector 执行完之后不会阻塞后续任务，但是还是建议都运行在单独的 scope 里面，因为 collector 里面可能会执行耗时任务，那么此时阻塞会导致后续代码得不到及时的处理。

<details>
  <summary>查看源码</summary>

```kt
// Flow.kt中flow构建器的简化实现
public fun <T> flow(block: suspend FlowCollector<T>.() -&gt; Unit): Flow<T> = SafeFlow(block)

private class SafeFlow<T>(private val block: suspend FlowCollector<T>.() -&gt; Unit) : AbstractFlow<T>() {
    override suspend fun collectSafely(collector: FlowCollector<T>) {
        collector.block() // 执行构建器中的代码块
    }
}
```

&lt;/details&gt;

## flow 会丢失事件吗？

在上面的例子，我贴过这样的一段代码：

```kt
launch { stateFlow.collect { println("收集器1: $it") } }
launch { stateFlow.collect { println("收集器2: $it") } }
launch { stateFlow.collect { println("收集器3: $it") } }
delay(10)
stateFlow.emit("修改后的值")
```

结果可见上面，相信大家都会有疑惑，如果我删除掉 delay(10) 结果会是怎样呢？

*   收集器1: 修改后的值
*   收集器2: 修改后的值

在这里就不卖关子了，直接展示出结果，stateflow 只展示最新的一条。这是一个大家都熟知的知识点：**StateFlow 会展示最新的值**。那么隐藏在他后面的信息是什么呢？既是热流不依赖 collector 就可以发送，所以热流会丢失事件。那么热流如何能够避免丢失事件呢？

1.  使用 SharedFlow，添加 replay 大小 和 `extraBufferCapacity` 避免溢出
2.  使用 buffer() 转成冷流来处理背压

说到背压下一节我们讲一下 冷流与热流的背压策略的不同。

## 冷流与热流的被压策略

&gt; 背压（Backpressure）是指当数据生产速度快于消费速度时如何处理多余数据的策略。在 Kotlin Flow 中，冷流和热流有不同的背压处理机制。

![image.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/f75c3919c6724da793e2f2361991cad5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ2FwdGFpblo=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzU0NDQ4MTIxNzM4MzkxMSJ9&amp;rk3s=f64ab15b&amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;x-orig-expires=1749707563&amp;x-orig-sign=2qQ%2FE6o2u3eqPo%2B8Nou2gDtCG%2BU%3D)

## 冷流的背压策略

冷流（如通过 `flow {}`构建器创建的流）默认采用**挂起式背压**：

1.  **默认行为**：当下游消费者处理不及时时，上游生产者会自动挂起，等待消费者准备好
2.  **自然协调**：生产者和消费者之间自然形成速度匹配，不需要额外缓冲区
3.  **可配置性**：可以通过操作符修改默认行为

### 冷流背压操作符

冷流可以使用以下操作符修改背压行为：

```kotlin

// 添加缓冲区，可以避免因下游消费过慢阻塞上游生产
flow.buffer(capacity = 10)

// 指定缓冲区溢出策略
flow.buffer(
    capacity = 10,
    onBufferOverflow = BufferOverflow.DROP_OLDEST // 丢弃最旧的元素
)

// 其他背压相关操作符
flow.conflate()  // 只保留最新值，丢弃中间值
flow.collectLatest { ... }  // 取消处理中的值，开始处理最新值
```

对上面的背压操作符我这里举一个例子，现在假设我们有一个 事件中心用来接收服务端的事件，如果服务端一直发送事件过来，我们客户端可能没法及时处理，会导致 flow 上游挂起，消息堆积，此时我们只有两个措施，要么 drop 要么保存。所以 buffer 操作符就这样诞生了，但是 buffer 操作符也不是万能的，一直有消息过来，buffer 也是会满的，那么此时就需要使用不同策略了，可以选择丢弃最旧的信息等。

## 热流的背压策略

热流（如 `SharedFlow` 和 `StateFlow`）有更复杂的背压处理机制，因为它们可能有多个消费者：

### SharedFlow 背压策略

`SharedFlow`在创建时可以配置背压策略：

```kotlin

val sharedFlow = MutableSharedFlow<Int>(
    replay = 1,  // 重放缓存大小
    extraBufferCapacity = 10,  // 额外缓冲区容量
    onBufferOverflow = BufferOverflow.DROP_OLDEST  // 缓冲区溢出策略
)
```

背压参数说明：

*   **replay**：保留最近发射的N个值，供新订阅者立即消费
*   **extraBufferCapacity**：额外缓冲区大小，当所有消费者处理不及时时使用
*   **onBufferOverflow**：缓冲区满时的策略，可选值：
    *   `SUSPEND` ：挂起发射者（默认）
    *   `DROP_OLDEST`：丢弃最旧的值
    *   `DROP_LATEST`：丢弃最新的值

### StateFlow 背压策略

`StateFlow`是一种特殊的 `SharedFlow`，它：始终有 `replay = 1`（只保留最新值）

没有额外缓冲区（` extraBufferCapacity = 0`）
使用 DROP\_OLDEST 溢出策略（总是保留最新值）这意味着 `StateFlow`永远不会因背压而挂起发射者，它总是保留最新值并丢弃旧值。

## 冷流与热流背压策略的关键区别

1.  **默认行为**：
    *   冷流：默认挂起发射者等待消费者
    *   热流：可配置（`SharedFlow`）或固定策略（`StateFlow`）

2.  **多消费者场景**：
    *   冷流：每个消费者获得独立的数据流，背压独立处理
    *   热流：所有消费者共享同一数据流，最慢的消费者可能影响所有人

3.  **缓冲区配置**：
    *   冷流：通过操作符动态添加
    *   热流：在创建时静态配置

4.  **数据丢失风险**：
    *   冷流：默认不会丢失数据
    *   热流：可能配置为丢弃数据（` DROP_OLDEST`/`DROP_LATEST`）

## 实际应用示例

### 冷流背压示例

```kotlin

val slowConsumer = flow {
    for (i in 1..100) {
        delay(10)  // 生产速度快
        emit(i)
    }
}.buffer(10)  // 添加缓冲区
 .flowOn(Dispatchers.Default)  // 在不同协程上下文中执行

// 消费者处理慢
slowConsumer.collect { value -&gt;
    delay(100)  // 消费速度慢
    println("Processed: $value")
}
```

### 热流背压示例

```kotlin
// 配置不同背压策略的SharedFlow
val suspendingFlow = MutableSharedFlow<Int>(
    replay = 0,
    extraBufferCapacity = 0,  // 没有额外缓冲区，会挂起
    onBufferOverflow = BufferOverflow.SUSPEND
)

val droppingFlow = MutableSharedFlow<Int>(
    replay = 0,
    extraBufferCapacity = 10,  // 有缓冲区
    onBufferOverflow = BufferOverflow.DROP_OLDEST  // 缓冲区满时丢弃旧值
)

// StateFlow总是使用DROP_OLDEST策略
val stateFlow = MutableStateFlow(0)
```

一般来说选择合适的背压策略取决于您的应用场景：

*   需要处理所有数据时，使用默认挂起策略
*   只关心最新数据时，使用丢弃策略
*   需要平衡吞吐量和内存使用时，配置适当大小的缓冲区

## Kotlin Flow 与线程安全

#### 冷流的线程安全特性：

*   **独立执行**：每个收集器获得独立的流实例，不同收集器之间不会相互干扰
*   **单线程收集**：默认情况下，单个收集操作是在单一线程上顺序执行的
*   **线程安全**：由于每次收集都是独立的，冷流本身不存在线程安全问题

#### 热流（Hot Flow）

热流的线程安全特性：

**线程安全实现**：内部使用原子操作和同步机制确保线程安全

*   **多线程访问**：支持从多个线程同时发射和收集值
*   **原子性保证**：eimit 和 tryEmit 操作是原子的，确保值的完整性

### Flow 操作符的线程安全性

Flow 操作符（如 `map`、`filter`、`transform`等）的线程安全性取决于：

*   **操作符实现**：大多数操作符保证内部线程安全
*   **用户提供的转换函数**：如果您的转换函数访问共享状态，需要自行确保线程安全
*   **执行上下文**：操作符在哪个调度器上执行会影响线程安全需求

```kotlin
// 这个转换函数访问共享状态，需要确保线程安全
var counter = 0
val flow = flowOf(1, 2, 3)
    .map { 
        counter++ // 注意：这是非线程安全的操作, 当前修改与 counter 并不在同一线程内
        it * 2 
    }
    .flowOn(Dispatchers.Default)
```

#### 线程安全的最佳实践

1.  **避免可变共享状态**：尽量避免在 Flow 操作中访问可变共享状态
2.  **使用不可变数据**：优先使用不可变数据结构，避免并发修改问题
3.  **正确使用调度器**：了解  `flowOn `、 `launchIn`和 `collect`的上下文影响
4.  **注意 StateFlow 更新**：对于 ` StateFlow`，使用`update`函数进行原子更新
5.  **使用线程安全的集合**：当需要在 Flow 中使用集合时，考虑使用线程安全的集合类

#### 常见线程安全陷阱

1.  **收集者上下文**：默认情况下，Flow 在收集者的上下文中执行，可能导致 UI 线程阻塞

```kt
// 错误：可能在主线程上执行耗时操作
lifecycleScope.launch {
    flow.collect { /* 在主线程执行 */ }
}

// 正确：使用适当的调度器
lifecycleScope.launch {
    flow.flowOn(Dispatchers.IO).collect { /* 处理数据 */ }
}
```

2.  **共享可变状态**：在 Flow 操作符中修改共享状态可能导致竞态条件

```kt
// 错误：非线程安全的状态修改
val list = mutableListOf<Int>()
flow.onEach { list.add(it) } // 可能导致竞态条件
// 正确：使用线程安全的收集方式
val result = flow.toList() // 收集到线程安全的集合
```

## The end

受限于文章长度吗，本文只讲述了一些日常开发容易被大家忽略的 kotlin flow 知识点，希望这篇文章能够帮助到你。
</Int></Int></Int></Int></T></T></T></T></T></T></T></details></T></T></T></T></T></out></details>]]></content><author><name>CaptainZ</name><email>jelychow@gmail.com</email></author><summary type="html"><![CDATA[Kotlin Flow 全面解析：从基础到高级！]]></summary></entry><entry><title type="html">Google io 追踪，在移动端部署 gemma 模型</title><link href="https://jelychow.github.io/Google-IO-%E8%BF%BD%E8%B8%AA-%E5%9C%A8%E7%A7%BB%E5%8A%A8%E7%AB%AF%E9%83%A8%E7%BD%B2-Gemma-%E6%A8%A1%E5%9E%8B/" rel="alternate" type="text/html" title="Google io 追踪，在移动端部署 gemma 模型" /><published>2025-05-25T00:00:00+00:00</published><updated>2025-05-25T00:00:00+00:00</updated><id>https://jelychow.github.io/Google%20IO%20%E8%BF%BD%E8%B8%AA%EF%BC%8C%E5%9C%A8%E7%A7%BB%E5%8A%A8%E7%AB%AF%E9%83%A8%E7%BD%B2%20Gemma%20%E6%A8%A1%E5%9E%8B</id><content type="html" xml:base="https://jelychow.github.io/Google-IO-%E8%BF%BD%E8%B8%AA-%E5%9C%A8%E7%A7%BB%E5%8A%A8%E7%AB%AF%E9%83%A8%E7%BD%B2-Gemma-%E6%A8%A1%E5%9E%8B/"><![CDATA[<p>前两天 Google IO 给我们演示 Gemma 模型在移动端的效果，说实话看着挺让人激动的，于是乎就体验了一番，说实话效果很棒，在这里就写一篇文章来给大家分享一下，如何在移动端 借助 LiteRT 来部署 Gemma 模型</p>

<h2 id="1-前置条件">1. 前置条件</h2>

<ol>
  <li>
    <p>注册 huggingface 账号<br />
<a href="https://huggingface.co/">hugginguface</a> 是世界上最大的开源模型，数据集，机器学习的社区，上面会托管各式各样的模型，是每个 AI 爱好者的必备网站之一。</p>
  </li>
  <li>
    <p>科学上网<br />
这个重要性不言而喻</p>
  </li>
</ol>

<h2 id="2-下载-app-或编译源码">2. 下载 app 或编译源码</h2>

<h3 id="下载地址此处可见"><a href="https://huggingface.co/litert-community/Gemma3-1B-IT">下载地址此处可见</a></h3>

<h3 id="源码地址此处可见"><a href="https://github.com/google-ai-edge/gallery/tree/main/Android">源码地址此处可见</a></h3>

<h2 id="3-安装-app">3. 安装 App</h2>

<h2 id="4-运行">4. 运行</h2>

<p>打开之后会出现如下页面</p>

<p><img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/41ced6ea374347db8435aec5e7c6268f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ2FwdGFpblo=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzU0NDQ4MTIxNzM4MzkxMSJ9&amp;rk3s=f64ab15b&amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;x-orig-expires=1748762610&amp;x-orig-sign=NN32BuTa8nvccZJHapcIpKT29PE%3D" alt="image.png" /></p>

<h2 id="5-选择模块">5. 选择模块</h2>

<p>这里以 AI chat model 为例</p>

<p><img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/e8903efa6227435d99fe8d016f8e3e0a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ2FwdGFpblo=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzU0NDQ4MTIxNzM4MzkxMSJ9&amp;rk3s=f64ab15b&amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;x-orig-expires=1748762610&amp;x-orig-sign=46qvuA9La3RH7pueEUuCq9tIsQQ%3D" alt="image.png" /></p>

<p>在这里我想选择第一个模型来体验，此处需要提醒使用者，一般来说模型越大占内存也会越大，使用起来手机发热，耗电量会增加，建议开发者在移动端谨慎选择，当然模型参数较小，量化都会降低精度。</p>

<p>选取了模型之后页面会跳转到浏览器，我这里使用的是 Chrome 浏览器。在之前我会在电脑上使用 chrome 注册 huggingface 账号，保存在 chrome 里面方便手机端同步账号。
跳转到 huggingface 获取认证之后会跳转回来开始下载流程，在下载完成之后页面上会出现这个页面</p>

<blockquote>
  <p>在这里有个好用的知识点推荐给大家，使用 Chrome Custom Tabs被用于提供一种轻量级的网页浏览体验，特别是在获取用户登录信息时。在有 chrome 浏览器的设备上他的体验会非常好，过度很自然，建议大家优先使用。</p>
</blockquote>

<p><img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/914f53d7e8ed47769557cad0af3d4cdd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ2FwdGFpblo=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzU0NDQ4MTIxNzM4MzkxMSJ9&amp;rk3s=f64ab15b&amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;x-orig-expires=1748762610&amp;x-orig-sign=5OEwhSDJWw4NZ7aJeCn1kF9qPXM%3D" alt="image.png" /></p>

<h2 id="6-提问">6. 提问</h2>

<p>然后开始 chat，举个例子：帮我简单叙述一下 kotlin 的 flow 和 stateflow 区别？</p>

<p><img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/e61c05e2771445f1995ba5d9434bcd13~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ2FwdGFpblo=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzU0NDQ4MTIxNzM4MzkxMSJ9&amp;rk3s=f64ab15b&amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;x-orig-expires=1748762610&amp;x-orig-sign=G4V%2FkOW45HjCYbpuGZdyB%2FnCCAg%3D" alt="image.png" /></p>

<p>然后这个模型会给你疯狂作答，下面会展示本次会话的状态。下面我们会注意到这个 model 是运行的 CPU 上的，我们可以指定 model 运行在 gpu 上看看效果。点击右上角的配置按钮，我们会得到如下的对话框</p>

<p><img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/37c48c9ac4e6493285692940dc5c7355~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ2FwdGFpblo=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzU0NDQ4MTIxNzM4MzkxMSJ9&amp;rk3s=f64ab15b&amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;x-orig-expires=1748762610&amp;x-orig-sign=JcEEUpHEMBJiO49kRGKE7NfXt6g%3D" alt="image.png" />
简单的设置一下加速器为 gpu 等待模型加载然后看看效果：</p>

<p><img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/8733f8ae5cf644f580ac87c1b6b2934c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ2FwdGFpblo=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzU0NDQ4MTIxNzM4MzkxMSJ9&amp;rk3s=f64ab15b&amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;x-orig-expires=1748762610&amp;x-orig-sign=wToKQf%2BPlsEwngHi610PUFqVESw%3D" alt="image.png" /></p>

<p>可以看到 GPU 的 token 生成速度是要远快于 CPU的，建议我们在体验的时候优先使用 GPU 来部署。</p>

<h2 id="如果想看视频效果的请见此链接">如果想看视频效果的<a href="https://www.bilibili.com/video/BV1iWjuz1Evs/?vd_source=5700933c78eb2b70e4a486b309d6e808">请见此链接</a></h2>

<h2 id="总结">总结</h2>

<p>祝周末愉快，码力十足🙂</p>]]></content><author><name>CaptainZ</name><email>jelychow@gmail.com</email></author><summary type="html"><![CDATA[Google IO 2025 展示了 Gemma 模型在移动端的强大能力，本文详细介绍如何使用 LiteRT 在 Android 设备上部署和运行 Gemma 模型，包括模型下载、CPU/GPU 加速对比及实际效果展示。]]></summary></entry><entry><title type="html">Navigation 3 介绍</title><link href="https://jelychow.github.io/navigation-3-%E4%BB%8B%E7%BB%8D/" rel="alternate" type="text/html" title="Navigation 3 介绍" /><published>2025-05-23T00:00:00+00:00</published><updated>2025-05-23T00:00:00+00:00</updated><id>https://jelychow.github.io/navigation%203%20%E4%BB%8B%E7%BB%8D</id><content type="html" xml:base="https://jelychow.github.io/navigation-3-%E4%BB%8B%E7%BB%8D/"><![CDATA[<h1 id="navigation-3-使用介绍">Navigation 3 使用介绍</h1>

<h2 id="前言">前言</h2>
<p>关于 navigation 3 的诞生背景可以看<a href="https://android-developers.googleblog.com/2025/05/announcing-jetpack-navigation-3-for-compose.html?m=1">这篇文章</a>。如果想看源码学习如何使用 Navigation 3 可以看这个 <a href="https://github.com/android/nav3-recipes">repository</a>。</p>

<h2 id="为什么要开发新的-navigation-3">为什么要开发新的 navigation 3？</h2>
<p>对于很多使用 Compose UI 的开发者来说，或许既有的 navigation 框架也许能满足他们的基本使用。但是当页面涉及到比较复杂的页面的时候就会有一些问题。举个例子假设你在开发一个<strong>单页面应用/模块</strong>，这时候如果有个需求要跳转到之前的某个页面，这时候路由的回退栈对你来说或许没那么友好，需要设置一堆属性，而且调试不是那么容易。而且这种方式很难满足越来越复杂的业务开发，所以这时候 navigation 3 就应运而生。</p>

<h2 id="navigation-3-的主要变化">navigation 3 的主要变化？</h2>
<p>对于 navigation 3 最主要的变化是开发者获得了 <code class="language-plaintext highlighter-rouge">NavBackStack</code>的完整控制权，可以对路由操作进行高度的定制化操作。以前的回退栈只能通过被动观察，可能会导致路由跟当前存储的页面不匹配的情况。当然 navigation 3 的强大功能是一把双刃剑，使用不当开发起来肯定也是非常抓狂。所以对于大部分场景，我们可以使用默认的API来进行开发。</p>

<h2 id="navigation-3-的使用">navigation 3 的使用</h2>

<h3 id="简单使用方式">简单使用方式</h3>

<pre><code class="language-kt">val backStack = remember { mutableStateListOf&lt;Any&gt;(RouteA) }

NavDisplay(
    backStack = backStack,
    onBack = { backStack.removeLastOrNull() },
    entryProvider = { key -&gt;
        when (key) {
            is RouteA -&gt; NavEntry(key) {
                ContentGreen("Welcome to Nav3") {
                    Button(onClick = {
                        backStack.add(RouteB("123"))
                    }) {
                        Text("Click to navigate")
                    }
                }
            }

            is RouteB -&gt; NavEntry(key) {
                ContentBlue("Route id: ${key.id} ")
            }

            else -&gt; {
                error("Unknown route: $key")
            }
        }
    }
)
</code></pre>
<p>只需要定义一个 mutableStateListOf 作为回退栈即可，或者更简单</p>

<pre><code class="language-kt">    val backStack = rememberNavBackStack(RouteA)

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        entryProvider = entryProvider {
            entry&lt;RouteA&gt; {
                ContentGreen("Welcome to Nav3") {
                    Button(onClick = {
                        backStack.add(RouteB("123"))
                    }) {
                        Text("Click to navigate")
                    }
                }
            }
            entry&lt;RouteB&gt; { key -&gt;
                ContentBlue("Route id: ${key.id} ")
            }
        }
    )
}
</code></pre>

<h3 id="自定义的使用方式">自定义的使用方式</h3>

<pre><code class="language-kt">class TopLevelBackStack&lt;T: Any&gt;(startKey: T) {

    // Maintain a stack for each top level route
    private var topLevelStacks : LinkedHashMap&lt;T, SnapshotStateList&lt;T&gt;&gt; = linkedMapOf(
        startKey to mutableStateListOf(startKey)
    )

    // Expose the current top level route for consumers
    var topLevelKey by mutableStateOf(startKey)
        private set

    // Expose the back stack so it can be rendered by the NavDisplay
    val backStack = mutableStateListOf(startKey)

    private fun updateBackStack() =
        backStack.apply {
            clear()
            addAll(topLevelStacks.flatMap { it.value })
        }

    fun addTopLevel(key: T){

        // If the top level doesn't exist, add it
        if (topLevelStacks[key] == null){
            topLevelStacks.put(key, mutableStateListOf(key))
        } else {
            // Otherwise just move it to the end of the stacks
            topLevelStacks.apply {
                remove(key)?.let {
                    put(key, it)
                }
            }
        }
        topLevelKey = key
        updateBackStack()
    }

    fun add(key: T){
        topLevelStacks[topLevelKey]?.add(key)
        updateBackStack()
    }

    fun removeLast(){
        val removedKey = topLevelStacks[topLevelKey]?.removeLastOrNull()
        // If the removed key was a top level key, remove the associated top level stack
        topLevelStacks.remove(removedKey)
        topLevelKey = topLevelStacks.keys.last()
        updateBackStack()
    }
}
</code></pre>
<p>我们可以看到这个路由回退栈是高度自定义的，而且可以按照他的每个层级都可以自定义，这让我们开发复杂需求的时候可以更得心应手。</p>

<h2 id="navdisplay-源码介绍">NavDisplay 源码介绍</h2>
<p>源码地址可见<a href="https://android.googlesource.com/platform/frameworks/support/+/44c07c80b0a8b6c357f95cf508264d0b4313fcb5/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt">这里</a></p>

<h3 id="主要功能">主要功能</h3>

<ol>
  <li><strong>单窗格内容显示</strong>：每次只显示一个导航目的地的内容</li>
  <li><strong>自定义过渡动画</strong>：支持自定义进入和退出过渡动画</li>
  <li><strong>对话框支持</strong>：能够将导航目的地显示为对话框</li>
  <li><strong>后退处理</strong>：集成了系统后退按钮的处理</li>
</ol>

<h3 id="源码介绍">源码介绍</h3>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Composable</span>
<span class="k">public</span> <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span> <span class="p">:</span> <span class="nc">Any</span><span class="p">&gt;</span> <span class="nf">NavDisplay</span><span class="p">(</span>
    <span class="n">backstack</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;,</span>
    <span class="n">modifier</span><span class="p">:</span> <span class="nc">Modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">,</span>
    <span class="n">wrapperManager</span><span class="p">:</span> <span class="nc">NavWrapperManager</span> <span class="p">=</span> <span class="nf">rememberNavWrapperManager</span><span class="p">(</span><span class="nf">emptyList</span><span class="p">()),</span>
    <span class="n">contentAlignment</span><span class="p">:</span> <span class="nc">Alignment</span> <span class="p">=</span> <span class="nc">Alignment</span><span class="p">.</span><span class="nc">TopStart</span><span class="p">,</span>
    <span class="n">sizeTransform</span><span class="p">:</span> <span class="nc">SizeTransform</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
    <span class="n">enterTransition</span><span class="p">:</span> <span class="nc">EnterTransition</span> <span class="p">=</span> <span class="nf">fadeIn</span><span class="p">(</span><span class="o">..</span><span class="p">.),</span>
    <span class="n">exitTransition</span><span class="p">:</span> <span class="nc">ExitTransition</span> <span class="p">=</span> <span class="nf">fadeOut</span><span class="p">(</span><span class="o">..</span><span class="p">.),</span>
    <span class="n">onBack</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">Unit</span> <span class="p">=</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">backstack</span> <span class="k">is</span> <span class="nc">MutableList</span><span class="p">)</span> <span class="n">backstack</span><span class="p">.</span><span class="nf">removeAt</span><span class="p">(</span><span class="n">backstack</span><span class="p">.</span><span class="n">size</span> <span class="p">-</span> <span class="mi">1</span><span class="p">)</span> <span class="p">},</span>
    <span class="n">recordProvider</span><span class="p">:</span> <span class="p">(</span><span class="n">key</span><span class="p">:</span> <span class="nc">T</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">NavRecord</span><span class="p">&lt;</span><span class="k">out</span> <span class="nc">T</span><span class="p">&gt;</span>
<span class="p">)</span>
</code></pre></div></div>

<ol>
  <li><strong>backstack</strong>: 表示导航状态的键集合，不能为空</li>
  <li><strong>wrapperManager</strong>: 组合所有 NavContentWrapper 的管理器</li>
  <li><strong>sizeTransform</strong>: 用于控制大小变化的转换</li>
  <li><strong>enterTransition</strong>: 默认的进入过渡动画，默认为淡入效果</li>
  <li><strong>exitTransition</strong>: 默认的退出过渡动画，默认为淡出效果</li>
  <li><strong>onBack</strong>: 处理系统返回按钮的回调</li>
  <li><strong>recordProvider</strong>: 用于构造每个可能的 NavRecord 的 lambda 函数</li>
</ol>

<h2 id="navigation-3-的-自适应布局介绍">navigation 3 的 自适应布局介绍</h2>
<h3 id="核心组件">核心组件:</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ListDetailSceneStrategy</code> Material 3 提供的场景策略，用于创建自适应的列表-详情布局</li>
</ul>

<h3 id="工作原理">工作原理:</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">backStack</span> <span class="p">=</span> <span class="nf">rememberNavBackStack</span><span class="p">(</span><span class="nc">ConversationList</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">listDetailStrategy</span> <span class="p">=</span> <span class="n">rememberListDetailSceneStrategy</span><span class="p">&lt;</span><span class="nc">NavKey</span><span class="p">&gt;()</span>

<span class="nc">NavDisplay</span><span class="p">(</span>
    <span class="n">backStack</span> <span class="p">=</span> <span class="n">backStack</span><span class="p">,</span>
    <span class="n">onBack</span> <span class="p">=</span> <span class="p">{</span> <span class="n">keysToRemove</span> <span class="p">-&gt;</span> <span class="nf">repeat</span><span class="p">(</span><span class="n">keysToRemove</span><span class="p">)</span> <span class="p">{</span> <span class="n">backStack</span><span class="p">.</span><span class="nf">removeLastOrNull</span><span class="p">()</span> <span class="p">}</span> <span class="p">},</span>
    <span class="n">sceneStrategy</span> <span class="p">=</span> <span class="n">listDetailStrategy</span><span class="p">,</span>
    <span class="n">entryProvider</span> <span class="p">=</span> <span class="nf">entryProvider</span> <span class="p">{</span>
        <span class="c1">// 配置导航目的地</span>
    <span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="关键特性">关键特性:</h3>

<ol>
  <li>
    <p><strong>三种窗格类型</strong>:</p>

    <ul>
      <li>
        <p>` ListDetailSceneStrategy.listPane()` : 列表窗格，可以设置详情占位符</p>
      </li>
      <li>
        <p>` ListDetailSceneStrategy.detailPane() `: 详情窗格</p>
      </li>
      <li>
        <p><code class="language-plaintext highlighter-rouge">ListDetailSceneStrategy.extraPane() </code>: 额外窗格</p>
      </li>
    </ul>
  </li>
  <li>
    <p><strong>自适应行为</strong>:</p>

    <ul>
      <li>在窄屏设备上: 只显示一个窗格，用户需要导航来查看其他内容</li>
      <li>在宽屏设备上: 同时显示列表和详情窗格，提供分屏体验</li>
    </ul>
  </li>
  <li>
    <p><strong>实现示例</strong>:</p>
  </li>
</ol>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">entry</span><span class="p">&lt;</span><span class="nc">ConversationList</span><span class="p">&gt;(</span>
    <span class="n">metadata</span> <span class="p">=</span> <span class="nc">ListDetailSceneStrategy</span><span class="p">.</span><span class="nf">listPane</span><span class="p">(</span>
        <span class="n">detailPlaceholder</span> <span class="p">=</span> <span class="p">{</span>
            <span class="nc">ContentYellow</span><span class="p">(</span><span class="s">"Choose a conversation from the list"</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">)</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="nc">ContentRed</span><span class="p">(</span><span class="s">"Welcome to Nav3"</span><span class="p">)</span> <span class="p">{</span>
        <span class="nc">Button</span><span class="p">(</span><span class="n">onClick</span> <span class="p">=</span> <span class="p">{</span> <span class="n">backStack</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="nc">ConversationDetail</span><span class="p">(</span><span class="s">"ABC"</span><span class="p">))</span> <span class="p">})</span> <span class="p">{</span>
            <span class="nc">Text</span><span class="p">(</span><span class="s">"View conversation"</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="2-自定义双窗格布局-twopaneactivity">2. 自定义双窗格布局 (TwoPaneActivity)</h2>

<p>这是一个展示如何创建自定义自适应布局的实现，使用 Scenes API 和自定义的<code class="language-plaintext highlighter-rouge">TwoPaneScene</code>和<code class="language-plaintext highlighter-rouge">TwoPaneSceneStrategy</code>。</p>

<h3 id="核心组件-1">核心组件:</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">TwoPaneScene</code>: 自定义场景类，以 50/50 分割方式显示两个导航条目</li>
  <li><code class="language-plaintext highlighter-rouge">TwoPaneSceneStrategy</code>: 自定义场景策略，决定何时激活双窗格布局</li>
</ul>

<h3 id="工作原理-1">工作原理:</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">backStack</span> <span class="p">=</span> <span class="nf">rememberNavBackStack</span><span class="p">(</span><span class="nc">Home</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">twoPaneStrategy</span> <span class="p">=</span> <span class="nf">remember</span> <span class="p">{</span> <span class="nc">TwoPaneSceneStrategy</span><span class="p">&lt;</span><span class="nc">Any</span><span class="p">&gt;()</span> <span class="p">}</span>

<span class="nc">NavDisplay</span><span class="p">(</span>
    <span class="n">backStack</span> <span class="p">=</span> <span class="n">backStack</span><span class="p">,</span>
    <span class="n">onBack</span> <span class="p">=</span> <span class="p">{</span> <span class="n">keysToRemove</span> <span class="p">-&gt;</span> <span class="nf">repeat</span><span class="p">(</span><span class="n">keysToRemove</span><span class="p">)</span> <span class="p">{</span> <span class="n">backStack</span><span class="p">.</span><span class="nf">removeLastOrNull</span><span class="p">()</span> <span class="p">}</span> <span class="p">},</span>
    <span class="n">sceneStrategy</span> <span class="p">=</span> <span class="n">twoPaneStrategy</span><span class="p">,</span>
    <span class="n">entryProvider</span> <span class="p">=</span> <span class="nf">entryProvider</span> <span class="p">{</span>
        <span class="c1">// 配置导航目的地</span>
    <span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="关键特性-1">关键特性:</h3>

<ol>
  <li>
    <p><strong>自定义布局逻辑</strong>:</p>

    <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Row</span><span class="p">(</span><span class="n">modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">fillMaxSize</span><span class="p">())</span> <span class="p">{</span>
    <span class="nc">Column</span><span class="p">(</span><span class="n">modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">weight</span><span class="p">(</span><span class="mf">0.5f</span><span class="p">))</span> <span class="p">{</span>
        <span class="n">firstEntry</span><span class="p">.</span><span class="n">content</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="n">firstEntry</span><span class="p">.</span><span class="n">key</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nc">Column</span><span class="p">(</span><span class="n">modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">weight</span><span class="p">(</span><span class="mf">0.5f</span><span class="p">))</span> <span class="p">{</span>
        <span class="n">secondEntry</span><span class="p">.</span><span class="n">content</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="n">secondEntry</span><span class="p">.</span><span class="n">key</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>激活条件</strong>:</p>

    <ul>
      <li>窗口宽度至少为 600dp (中等宽度断点)</li>
      <li>回退栈中最后两个条目都声明支持双窗格显示</li>
    </ul>
  </li>
  <li>
    <p><strong>窗口大小检测</strong>:</p>

    <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">windowSizeClass</span> <span class="p">=</span> <span class="nf">currentWindowAdaptiveInfo</span><span class="p">().</span><span class="n">windowSizeClass</span>
<span class="k">if</span> <span class="p">(!</span><span class="n">windowSizeClass</span><span class="p">.</span><span class="nf">isWidthAtLeastBreakpoint</span><span class="p">(</span><span class="nc">WIDTH_DP_MEDIUM_LOWER_BOUND</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">null</span>
<span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>元数据标记</strong>:</p>

    <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">entry</span><span class="p">&lt;</span><span class="nc">Home</span><span class="p">&gt;(</span><span class="n">metadata</span> <span class="p">=</span> <span class="nc">TwoPaneScene</span><span class="p">.</span><span class="nf">twoPane</span><span class="p">())</span> <span class="p">{</span> <span class="o">..</span><span class="p">.</span> <span class="p">}</span>
</code></pre></div>    </div>
  </li>
</ol>

<h2 id="自适应布局的共同特点">自适应布局的共同特点</h2>

<ol>
  <li>
    <p><strong>响应式设计</strong>:</p>

    <ul>
      <li>根据屏幕尺寸自动调整布局</li>
      <li>在不同设备和方向上提供最佳用户体验</li>
    </ul>
  </li>
  <li>
    <p><strong>场景策略模式</strong>:</p>

    <ul>
      <li>使用<code class="language-plaintext highlighter-rouge">SceneStrategy</code>接口来决定何时和如何显示特定布局</li>
      <li>通过<code class="language-plaintext highlighter-rouge">calculateScene</code>方法根据条件返回适当的场景</li>
    </ul>
  </li>
  <li>
    <p><strong>元数据驱动</strong>:</p>

    <ul>
      <li>使用元数据来标记导航目的地的布局偏好</li>
      <li>允许每个目的地指定自己的显示方式</li>
    </ul>
  </li>
</ol>

<h2 id="总结">总结</h2>
<p>本文中介绍 navigation 3 的一些用法，当然 navigation 3 目前还不是很稳定，希望感兴趣的可以尝试一下</p>]]></content><author><name>CaptainZ</name><email>jelychow@gmail.com</email></author><summary type="html"><![CDATA[Navigation 3 介绍]]></summary></entry><entry><title type="html">从 google io 看移动端发展</title><link href="https://jelychow.github.io/%E4%BB%8E-Google-IO-%E7%9C%8B%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%8F%91%E5%B1%95/" rel="alternate" type="text/html" title="从 google io 看移动端发展" /><published>2025-05-22T00:00:00+00:00</published><updated>2025-05-22T00:00:00+00:00</updated><id>https://jelychow.github.io/%E4%BB%8E%20Google%20IO%20%E7%9C%8B%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%8F%91%E5%B1%95</id><content type="html" xml:base="https://jelychow.github.io/%E4%BB%8E-Google-IO-%E7%9C%8B%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%8F%91%E5%B1%95/"><![CDATA[<p>最近开始的 Google IO，google 一口气推出了不少 fancy 的好玩意，不过在这里我仅对移动端的新闻做一些介绍。</p>

<h2 id="亮点一-compose-生态持续发展">亮点一 Compose 生态持续发展</h2>

<p>Compose 推出已有一段时日了，只不过目前国内的各大厂都没有跟进。实际上 compose ui 现在的功能要远比传统的 xml要更加强大。Android 目前推出一些新特性基本上都是基于 compose ui 进行开发，建议大家尽早的跟进。</p>

<h3 id="navigation-3-导航库">Navigation 3 导航库</h3>

<p>这是一个专门为 compose 开发的导航库，相较于之前，他的功能更强大，支持多个目的地导航，在这里只能说喜欢 compose 的有福了，不过鉴于目前这个库还处理 alpha 阶段，可能后期 api 可能会变动，可以尝尝鲜，给官方提 pr。</p>

<h3 id="compose-material-新特性">compose material 新特性</h3>

<ul>
  <li>自动填充支持</li>
  <li>文本自动调整</li>
  <li>动画边界修饰符</li>
  <li>测试中的可访问性检查</li>
</ul>

<h3 id="material-expressive">Material expressive</h3>

<p>一组看起来很炫酷的组件，感兴趣的可以看<a href="https://m3.material.io/blog/building-with-m3-expressive">这里</a></p>

<h3 id="adaptive-layouts--自适应布局">Adaptive layouts / 自适应布局</h3>

<p>随着 Android 生态的持续发展，目前各式各样的设备都已经部署了 Android 系统。从穿戴设备到手机，到折叠设备，到汽车，Android 基本上无处不在，但是与之存在便是不同设备之间的 UI 不连贯，往往需要二次或多次开发才能完成需求，这时候推出 Adaptive layouts 便是为了解决这个问题。</p>

<h3 id="performance--性能">Performance / 性能</h3>

<p>compose 自诞生以来性能问题一直深受诟病，当然由于 compose 今天的重要性，Android 是不可能让问题持续存在的，自发布起就一直在优化性能。</p>

<p><img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/88d3789d1421481eba8a55402aa39fca~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ2FwdGFpblo=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzU0NDQ4MTIxNzM4MzkxMSJ9&amp;rk3s=f64ab15b&amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;x-orig-expires=1748763078&amp;x-orig-sign=YpcX4nId%2Fk2sTbt3KHN4R%2BWZjxI%3D" alt="image.png" /></p>

<h3 id="stability--稳定性">Stability / 稳定性</h3>

<p>谷歌内部做了大量的测试，并且减少/转正了很多<strong>实验性的</strong> API，让大家对 compose 更有信心。</p>

<h3 id="增加了库支持">增加了库支持</h3>

<p>多媒体的发展在当今移动互联网的时代非常重要，其对性能也有着更高的要求，所以 Compose 也增加了 CameraX 和 Media3的支持。</p>

<h3 id="tools">Tools</h3>

<p>Android studio 增加了如下功能，方便 compose 的开发与 调试</p>

<ul>
  <li>可调整大小的预览可立即向您展示 Compose UI 如何适应不同的窗口大小</li>
  <li>使用可点击的名称和组件 预览导航改进</li>
  <li>Studio Labs 🧪：使用 Gemini 快速生成 Compose预览</li>
  <li>Studio Labs 🧪：使用 Gemini 转换 UI，直接从预览中使用自然语言更改您的 UI。</li>
  <li>Studio Labs 🧪：Gemini 中的图像附件从图像生成 Compose 代码。</li>
</ul>

<h2 id="亮点二-端侧-ai-持续发力">亮点二 端侧 AI 持续发力</h2>

<p>随着 LLM 大模型的火热发展，很多模式也会自然的移植到移动端。谷歌推出了 Gemma 模型，后续可以在移动端有很多应用场景。
当然还配套的 AI 套件例如 MLkit 和 Firebase AI，端侧的生态越来越强大，对于一些实时性要求高的业务采用端侧 SLM/小模型 是一个非常不错的选择。</p>

<h2 id="总结">总结</h2>

<p>通过这次的 Goole IO 可以看到 compose 在 Android 的地位进一步提升，目前放出的更新内容来看，传统的 xml、view 体系应该是要慢慢退出历史舞台了。其次就是<strong>端侧 AI</strong>持续演进，是未来移动端的重要发展方向，然后就是 AI 编程配套措施的逐步完善，保证谷歌自己的科技护城河。</p>]]></content><author><name>CaptainZ</name><email>jelychow@gmail.com</email></author><summary type="html"><![CDATA[Google IO 2025 展示了移动端发展新趋势：Compose生态持续完善，包括Navigation 3导航库、自适应布局和性能优化；同时端侧AI技术如Gemma模型成为未来发展重点，传统XML视图体系或将逐步退出舞台。]]></summary></entry><entry><title type="html">Dag navigation in android</title><link href="https://jelychow.github.io/Dag-navigation-in-android/" rel="alternate" type="text/html" title="Dag navigation in android" /><published>2025-04-12T00:00:00+00:00</published><updated>2025-04-12T00:00:00+00:00</updated><id>https://jelychow.github.io/Dag%20navigation%20in%20android</id><content type="html" xml:base="https://jelychow.github.io/Dag-navigation-in-android/"><![CDATA[<h3 id="使用-dag-有向无环图管理复杂依赖">使用 DAG （有向无环图）管理复杂依赖</h3>

<h4 id="前言">前言</h4>
<p>在 Android 项目中，DAG（有向无环图）被用于管理应用的导航流程，管理项目依赖。我们常见的很多工具例如 gradle 里面也会应用 DAG 来检测循环依赖。DAG 的核心思想是通过节点和边的有向连接来表示任务或步骤之间的依赖关系，确保任务按照正确的顺序执行，同时避免循环依赖。本文将以一个简单的例子来介绍使用 DAG 来实现项目里面的复杂导航。</p>

<h4 id="需求介绍">需求介绍</h4>
<p>打开 app 的时候，如果没有授权过都会弹出一个<strong>隐私协议</strong>，如果没有登录需要进入<strong>登录页面</strong>，获取登录态才可以进入<strong>首页</strong>，这是一个非常初级的需求，相信大部分人用 <strong>if else</strong> 都可以完成，但是如果一个 app 有很多检查项，很多流程那么使用  <strong>if else</strong> 怕是很难满足业务需求。</p>

<h4 id="需求设计">需求设计</h4>

<h4 id="1-dag的核心组件">1. <strong>DAG的核心组件</strong></h4>
<ul>
  <li><strong>DagNode</strong>: 代表DAG中的一个节点，包含节点的唯一标识符（<code class="language-plaintext highlighter-rouge">id</code>）、数据（<code class="language-plaintext highlighter-rouge">data</code>）、依赖节点（<code class="language-plaintext highlighter-rouge">dependencies</code>）以及条件检查函数（<code class="language-plaintext highlighter-rouge">conditionCheck</code>）。节点可以依赖于其他节点，并且可以设置条件来决定节点是否可以被执行。</li>
  <li><strong>DagManager</strong>: 负责管理所有的DagNode，提供节点的添加、删除、查询、拓扑排序等功能。它还负责检测循环依赖，并确保DAG的正确性。</li>
  <li><strong>FlowManager</strong>: 使用DagManager来管理应用的导航流程。它根据用户偏好（如是否同意隐私政策、是否登录等）来决定当前应该显示的界面。
    <blockquote>
      <p>上面的架构设计好了，然后动动手指头让 AI 生成一下。</p>
    </blockquote>
  </li>
</ul>

<h4 id="2-代码片段">2. 代码片段</h4>

<p>Node 节点片段</p>

<pre><code class="language-kt">/**
 * DAG节点，代表流程中的一个步骤或任务
 */
class DagNode&lt;T&gt;(
    val id: String,
    val data: T,
    val dependencies: MutableSet&lt;DagNode&lt;T&gt;&gt; = mutableSetOf(),
    private var conditionCheck: (() -&gt; Boolean)? = null,
) {
    // 添加依赖节点
    fun dependsOn(node: DagNode&lt;T&gt;) {
        dependencies.add(node)
    }
    
    // 添加多个依赖节点
    fun dependsOn(vararg nodes: DagNode&lt;T&gt;) {
        nodes.forEach { dependencies.add(it) }
    }
    
    // 检查是否依赖于指定节点
    fun isDependentOn(node: DagNode&lt;T&gt;): Boolean {
        if (dependencies.contains(node)) return true
        
        // 递归检查间接依赖
        for (dependency in dependencies) {
            if (dependency.isDependentOn(node)) return true
        }
        
        return false
    }
    
    ...
}
</code></pre>

<p>DagManager</p>

<pre><code class="language-kt">/**
 * DAG管理器，负责管理节点和执行流程
 */
class DagManager&lt;T&gt; {
    private val nodes = mutableMapOf&lt;String, DagNode&lt;T&gt;&gt;()

    // 添加节点
    fun addNode(node: DagNode&lt;T&gt;) {
        // 检查是否会形成循环依赖
        for (existingNode in nodes.values) {
            require(!(node.isDependentOn(existingNode) &amp;&amp; existingNode.isDependentOn(node))) {
                "添加节点 ${node.id} 会导致循环依赖"
            }
        }
        nodes[node.id] = node
    }
}
</code></pre>

<p>FlowManager</p>

<pre><code class="language-kt">/**
 * 应用流程管理器，使用DAG管理应用的导航流程
 */
class FlowManager(private val userPreferences: UserPreferences) {
    

    
    // 创建DAG管理器
    private val dagManager = DagManager&lt;FlowStep&gt;()
    
    init {
        // 创建流程节点
        val privacyNode = DagNode("privacy", FlowStep.PRIVACY_POLICY)
        val loginNode = DagNode("login", FlowStep.LOGIN)
        val mainNode = DagNode("main", FlowStep.MAIN)
        
        // 建立依赖关系
        loginNode.dependsOn(privacyNode)  // 登录依赖于隐私政策同意
        mainNode.dependsOn(loginNode)     // 主界面依赖于登录
        
        // 添加节点到管理器
        dagManager.addNode(privacyNode)
        dagManager.addNode(loginNode)
        dagManager.addNode(mainNode)
    }
    ...
}
</code></pre>

<h4 id="3-创建dag">3. <strong>创建DAG</strong></h4>

<h5 id="31-创建dagnode">3.1 创建DagNode</h5>
<p>首先，你需要创建DagNode来表示流程中的每个步骤。每个节点可以设置条件检查函数，来决定该节点是否可以被执行。</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 创建流程节点</span>
 <span class="kd">val</span> <span class="py">privacyNode</span> <span class="p">=</span> <span class="nc">DagNode</span><span class="p">(</span><span class="s">"privacy"</span><span class="p">,</span> <span class="nc">Screen</span><span class="p">.</span><span class="nc">PrivacyPolicy</span><span class="p">.</span><span class="n">route</span><span class="p">)</span>
 <span class="kd">val</span> <span class="py">loginNode</span> <span class="p">=</span> <span class="nc">DagNode</span><span class="p">(</span><span class="s">"login"</span><span class="p">,</span> <span class="nc">Screen</span><span class="p">.</span><span class="nc">Login</span><span class="p">.</span><span class="n">route</span><span class="p">)</span>
 <span class="kd">val</span> <span class="py">mainNode</span> <span class="p">=</span> <span class="nc">DagNode</span><span class="p">(</span><span class="s">"main"</span><span class="p">,</span> <span class="nc">Screen</span><span class="p">.</span><span class="nc">Main</span><span class="p">.</span><span class="n">route</span><span class="p">)</span>
 <span class="c1">// 建立依赖关系</span>
 <span class="n">loginNode</span><span class="p">.</span><span class="nf">dependsOn</span><span class="p">(</span><span class="n">privacyNode</span><span class="p">)</span>  <span class="c1">// 登录依赖于隐私政策同意</span>
 <span class="n">mainNode</span><span class="p">.</span><span class="nf">dependsOn</span><span class="p">(</span><span class="n">loginNode</span><span class="p">)</span>     <span class="c1">// 主界面依赖于登录       </span>
</code></pre></div></div>
<p>上面的代码看起来有些偏向于比较传统的命令式编程方式，我们能不能使用 DSL 声明式语法来实现，让语义更简单明了</p>

<pre><code class="language-kt">val privacyNode = DagNode("privacy", Screen.PrivacyPolicy.route) {
            condition { userPreferences.hasAgreedToPrivacyPolicy() }
        }

val loginNode = DagNode("login", Screen.Login.route) {
            +privacyNode  // 登录依赖于隐私政策同意
            condition { userPreferences.isLoggedIn() }
        }
</code></pre>
<ol>
  <li>使用高阶函数作为构造器参数：
    <ul>
      <li>添加了 initBlock: (DagNode<T>.() -&gt; Unit)? 作为构造函数的可选参数</T></li>
      <li>这是一个接收者函数类型，允许在其作用域内直接访问 DagNode，最重要的是他作为最后一个参数可以进行 lamada 优化</li>
    </ul>
  </li>
  <li>实现运算符重载：
    <ul>
      <li>添加了 operator fun DagNode<T>.unaryPlus() 以支持 +node 语法</T></li>
      <li>这使依赖关系的表达更加简洁明了<br />
<strong>note</strong>：unaryPlus 操作符很好记，一元操作符，意思只有一个 + 对象，而 plus 是二元的</li>
    </ul>
  </li>
  <li>添加 DSL 专用方法：
    <ul>
      <li>创建 condition { … } 方法，将条件检查逻辑与节点创建紧密集成</li>
    </ul>
  </li>
</ol>

<h5 id="32-创建dagmanager">3.2 创建DagManager</h5>
<p>接下来，你需要创建DagManager来管理这些节点。使用我们上一节学到的方法来简单的改造一下方法，实现如下调用</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">dagManager</span> <span class="p">=</span> <span class="nc">DagManager</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">&gt;()</span>
<span class="n">dagManager</span><span class="p">.</span><span class="nf">dag</span> <span class="p">{</span>
    <span class="p">+</span><span class="n">privacyNode</span>
    <span class="p">+</span><span class="n">loginNode</span>
    <span class="p">+</span><span class="n">mainNode</span>
<span class="p">}</span>
</code></pre></div></div>

<h5 id="33-获取当前步骤">3.3 获取当前步骤</h5>
<p>通过DagManager的<code class="language-plaintext highlighter-rouge">getStartNode</code>方法，获取当前应该执行的步骤。本算法是基于<strong>入度</strong> 来实现，从依赖为 0 的节点开始遍历，然后解除依赖，如果没有解除过的同学可以了练练<a href="https://leetcode.cn/problems/course-schedule/description/">课程表</a>这个题目，里面有更详细的解释。</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">currentStep</span> <span class="p">=</span> <span class="n">dagManager</span><span class="p">.</span><span class="nf">getStartNode</span><span class="p">()</span><span class="o">?.</span><span class="n">data</span> <span class="o">?:</span> <span class="nc">Main</span><span class="p">.</span><span class="n">route</span>
</code></pre></div></div>

<h4 id="4-dag的优势">4. <strong>DAG的优势</strong></h4>
<ul>
  <li><strong>清晰的依赖关系</strong>: DAG通过节点和边的连接清晰地表示了任务之间的依赖关系，使得复杂的流程变得易于管理。</li>
  <li><strong>避免循环依赖</strong>: DagManager会自动检测循环依赖，并抛出异常，确保流程的正确性。</li>
  <li><strong>条件检查</strong>: 每个节点可以设置条件检查函数，确保只有在满足特定条件时才会执行该节点。</li>
</ul>

<h4 id="5-总结">5. <strong>总结</strong></h4>
<p>DAG在项目中主要用于管理应用的导航流程，确保用户按照正确的顺序完成某些任务。通过DagNode和DagManager，开发者可以轻松地定义和管理复杂的任务依赖关系，并确保流程的正确性。DAG的使用不仅提高了代码的可读性和可维护性，还增强了应用的健壮性。</p>

<p>通过本文的介绍，你应该能够理解如何在项目中使用DAG来管理任务流程，并能够根据实际需求进行扩展和优化。
最后附上<a href="https://github.com/jelychow/DAG">项目传送门</a></p>]]></content><author><name>CaptainZ</name><email>jelychow@gmail.com</email></author><summary type="html"><![CDATA[excerpt: Dag navigation in android]]></summary></entry><entry><title type="html">Now in android 精讲 7 你的代码谁来守护？</title><link href="https://jelychow.github.io/Now-In-Android-%E7%B2%BE%E8%AE%B2-7-%E4%BD%A0%E7%9A%84%E4%BB%A3%E7%A0%81%E8%B0%81%E6%9D%A5%E5%AE%88%E6%8A%A4/" rel="alternate" type="text/html" title="Now in android 精讲 7 你的代码谁来守护？" /><published>2025-03-18T00:00:00+00:00</published><updated>2025-03-18T00:00:00+00:00</updated><id>https://jelychow.github.io/Now%20In%20Android%20%E7%B2%BE%E8%AE%B2%207%20-%20%E4%BD%A0%E7%9A%84%E4%BB%A3%E7%A0%81%E8%B0%81%E6%9D%A5%E5%AE%88%E6%8A%A4%EF%BC%9F</id><content type="html" xml:base="https://jelychow.github.io/Now-In-Android-%E7%B2%BE%E8%AE%B2-7-%E4%BD%A0%E7%9A%84%E4%BB%A3%E7%A0%81%E8%B0%81%E6%9D%A5%E5%AE%88%E6%8A%A4/"><![CDATA[<h2 id="前言">前言</h2>

<p>相信我们经常写代码的同学都会有这样的感觉，团队协作、代码提交、检查变得越来越重要。在某些团队里面，code review 甚至会花出相当大比例的时间与精力来保证代码质量。本文来学习借鉴一下 <a href="https://github.com/android/nowinandroid">now in android</a> 项目是如何保证稳定性。</p>

<h2 id="切入点">切入点</h2>

<p>在大型项目中，质量保障机制往往深度集成于CI流程。通过分析<code class="language-plaintext highlighter-rouge">.github/workflows/Build.yaml</code> 配置文件，我们可以系统性拆解nowinandroid项目的质量保障策略。该CI流程包含多个关键任务节点，形成覆盖代码规范、依赖管理、自动化测试等多维度的质量防护网。</p>

<h3 id="github-ci-文件介绍">GitHub CI 文件介绍</h3>

<p>GitHub 的 CI 文件一般会配置在 <code class="language-plaintext highlighter-rouge">.github</code> 文件夹里面。今天我们要学习的在 <a href="https://github.com/android/nowinandroid/blob/main/.github/workflows/Build.yaml">.github/workflows/Build.yaml</a> 这个文件里面。下面我们就来逐步分析里面关于保证代码质量的相关模块吧。</p>

<h2 id="项目插件目录检查">项目插件目录检查</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check build-logic</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">./gradlew :build-logic:convention:check</span>
</code></pre></div></div>

<p>这个任务比较简单，是使用 Android Lint 来校验 <code class="language-plaintext highlighter-rouge">:build-logic</code> 模块代码质量。由于 <code class="language-plaintext highlighter-rouge">build-logic</code> 里面会配置一些 convention plugin，包含variant定义、依赖版本控制等关键配置，需优先确保其规范性</p>

<h2 id="格式化风格检查">格式化风格检查</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check spotless</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache</span>
</code></pre></div></div>

<p><a href="https://github.com/android/nowinandroid/blob/main/gradle/init.gradle.kts">init.gradle</a> 文件详见这里，这个脚本值得我们学习借鉴，建议大家都试一下。首先找个 <code class="language-plaintext highlighter-rouge">src</code> 目录下面建一个 <code class="language-plaintext highlighter-rouge">test.kt</code> 文件，然后执行上面的脚本，看看是不是报错了，然后按照它报错的地方进行修改。<code class="language-plaintext highlighter-rouge">./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache</code> 这个脚本的用法确实是我第一次见到这么实用的，这脚本可以看成是两步：</p>

<ol>
  <li>配置 Spotless 初始化脚本</li>
  <li>执行 <code class="language-plaintext highlighter-rouge">spotlessCheck</code></li>
</ol>

<p>有关 Spotless 的介绍可以看<a href="https://github.com/diffplug/spotless">这里</a>。首先呢，Spotless check 类似于 Lint 的 check，它会根据配置文件里面的参数来进行 check，如果遇到了规则之外的代码，它会 report 一个错误出来，并且终止当前 task。Spotless 还有一个 apply 任务，一般用于在代码提交之前，用于按照设定的规则对文件进行修改。目前 Spotless 里面配置的 Kotlin 的规则是 ktlint，如果是以纯 Kotlin 开发的项目，建议直接使用 ktlint 插件，使用起来会更简单。对于 now in android 里面这个 <code class="language-plaintext highlighter-rouge">spotlessCheck</code> 主要作用是校验 Kotlin 格式，以及 xml，kt，kts 的 copyright 的校验。</p>

<h2 id="依赖检查">依赖检查</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check Dependency Guard</span>
  <span class="na">id</span><span class="pi">:</span> <span class="s">dependencyguard_verify</span>
  <span class="na">continue-on-error</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">./gradlew dependencyGuard</span>
</code></pre></div></div>

<p>在我们开发过程中依赖库版本变化经常会让我们头疼不已，一不小心一个下午就偷偷溜走。<strong>dependencyGuard</strong> 任务会检查当前依赖库有没有发生改变，如果发生改变，它会终止当前任务。好奇的宝宝可以尝试在项目里面修改一下某个 library 的版本号，然后执行一下 <code class="language-plaintext highlighter-rouge">./gradlew dependencyGuard</code> 看看会发生什么。当然细心的同学肯定会问，如果我想更新依赖怎么办呢？我们可以通过 <code class="language-plaintext highlighter-rouge">./gradlew dependencyGuardBaseline</code> 来更新依赖文件。如果想了解更多关于 dependencyguard 的知识请移步至<a href="https://github.com/dropbox/dependency-guard">这里</a>。</p>

<h2 id="重新生成依赖文件如果上个任务失败并且这个任务是-pr-触发的">重新生成依赖文件，如果上个任务失败，并且这个任务是 PR 触发的</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Generate new Dependency Guard baselines if verification failed and it's a PR</span>
  <span class="na">id</span><span class="pi">:</span> <span class="s">dependencyguard_baseline</span>
  <span class="na">if</span><span class="pi">:</span> <span class="s">steps.dependencyguard_verify.outcome == 'failure' &amp;&amp; github.event_name == 'pull_request'</span>
  <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">./gradlew dependencyGuardBaseline</span>
    <span class="no">  </span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Push new Dependency Guard baselines if available</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">stefanzweifel/git-auto-commit-action@v5</span>
  <span class="na">if</span><span class="pi">:</span> <span class="s">steps.dependencyguard_baseline.outcome == 'success'</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">file_pattern</span><span class="pi">:</span> <span class="s1">'</span><span class="s">**/dependencies/*.txt'</span>
    <span class="na">disable_globbing</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">commit_message</span><span class="pi">:</span> <span class="s2">"</span><span class="s">🤖</span><span class="nv"> </span><span class="s">Updates</span><span class="nv"> </span><span class="s">baselines</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">Dependency</span><span class="nv"> </span><span class="s">Guard"</span>
</code></pre></div></div>

<p>有了上一节的知识，这个任务不必多说了。</p>

<h2 id="验证截图">验证截图</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run all local screenshot tests (Roborazzi)</span>
  <span class="na">id</span><span class="pi">:</span> <span class="s">screenshotsverify</span>
  <span class="na">continue-on-error</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">./gradlew verifyRoborazziDemoDebug</span>
</code></pre></div></div>

<p>对于这个任务，我个人认为其偏向于自动化测试方面的。当然，Android 开发如果想了解更多我觉得这是一个不错的知识点。它的介绍可以看<a href="https://github.com/takahirom/roborazzi">这里</a>。对于某些不经常变更又非常重要的页面，做 screenshot test 是一件极其有意义的事情，它直观地反映出修改，以免在发布之前出现行为不一致。</p>

<h2 id="运行本地测试样例">运行本地测试样例</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run local tests</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">./gradlew testDemoDebug :lint:test</span>
</code></pre></div></div>

<p>这步主要是为了执行一些自定义的 lint 任务，在项目这个 test 主要是调用 <code class="language-plaintext highlighter-rouge">DesignSystemDetector</code> 这个文件来检查项目里面有没有使用 design system 里面的组件，如果使用了系统自带的有关组件，它会 report 一个 issue 来提醒使用方使用正确的组件。当然，Android Lint 的功能非常强大，远远不止校验组件这一个功能，在实际开发过程中我们可以按照自己需求灵活使用。</p>

<h2 id="build-所有变种组合然后上传到指定文件夹">Build 所有变种组合然后上传到指定文件夹</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build all build type and flavor permutations</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">./gradlew :app:assemble</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload build outputs (APKs)</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/upload-artifact@v4</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">APKs</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">**/build/outputs/apk/**/*.apk'</span>
</code></pre></div></div>

<p>这个任务堪称整个 CI 的核心，俗称 <code class="language-plaintext highlighter-rouge">丐中丐</code>，即便我们编写一个最简单的 CI 任务，我们也需要运行这个任务，保证我们上传的代码能够正常地通过编译，同时还要生成产物方便后续 QA 下载测试。</p>

<h2 id="上传产物">上传产物</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload JVM local results (XML)</span>
  <span class="na">if</span><span class="pi">:</span> <span class="s">$</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/upload-artifact@v4</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">local-test-results</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">**/build/test-results/test*UnitTest/**.xml'</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload screenshot results (PNG)</span>
  <span class="na">if</span><span class="pi">:</span> <span class="s">$</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/upload-artifact@v4</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">screenshot-test-results</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">**/build/outputs/roborazzi/*_compare.png'</span>
</code></pre></div></div>

<p>上传本地测试任务和截图到 outputs</p>

<h2 id="执行-lint-任务">执行 Lint 任务</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check lint</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">./gradlew :app:lintProdRelease :app-nia-catalog:lintRelease :lint:lint</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload lint reports (HTML)</span>
  <span class="na">if</span><span class="pi">:</span> <span class="s">$</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/upload-artifact@v4</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">lint-reports</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">**/build/reports/lint-results-*.html'</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload lint reports (SARIF)</span>
  <span class="na">if</span><span class="pi">:</span> <span class="s">$</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">github/codeql-action/upload-sarif@v3</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">sarif_file</span><span class="pi">:</span> <span class="s1">'</span><span class="s">./'</span>
</code></pre></div></div>

<p>执行 <code class="language-plaintext highlighter-rouge">lint</code> 任务并且上传到产物，和 codeql，其中 codeql 是一个静态分析的工具，主要用于检测代码中的安全漏洞和潜在缺陷。</p>

<h2 id="检查测试覆盖率">检查测试覆盖率</h2>

<p><code class="language-plaintext highlighter-rouge">androidTest</code> 这个 job 主要作用是使用 <code class="language-plaintext highlighter-rouge">jacoco</code> 来检查各种 test 的覆盖率，以此来保证尽可能多的场景被测试到。</p>

<h2 id="其他措施">其他措施</h2>

<h3 id="code-style-配置">Code Style 配置</h3>

<p>Android Studio 项目可以配置 code style，默认目录是在 <code class="language-plaintext highlighter-rouge">.idea</code> 目录下面，在这里我们可以看到 now in android 项目，也配置自己的 code style。</p>

<h3 id="commit-hook">Commit Hook</h3>

<p>在 <code class="language-plaintext highlighter-rouge">tools</code> 目录下面有两个 sh 文件，<code class="language-plaintext highlighter-rouge">setup.sh</code>，<code class="language-plaintext highlighter-rouge">pre-push</code>，它们的主要作用就是用来 hook commit，在本项目里面这个 hook 的主要目的是：</p>

<ul>
  <li>校验 branch 名称</li>
  <li>检查 WIP or stash</li>
  <li>执行 <code class="language-plaintext highlighter-rouge">spotless check</code> 任务，确保 push 的文件格式正确</li>
</ul>

<p>在团队协作的时候，每个团队可能都会有一些要求，比如提交的 branch、commit 命名需要满足特定的规则，这时候 git hook 就能通过设置规则，使得代码的提交更为规范，尽可能地让代码风格保持一致。</p>

<h3 id="巧用-android-studio-配置">巧用 Android Studio 配置</h3>

<p><img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9dcee73466264a179befa9a54467e17d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQ2FwdGFpblo=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzU0NDQ4MTIxNzM4MzkxMSJ9&amp;rk3s=f64ab15b&amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;x-orig-expires=1742861605&amp;x-orig-sign=%2FpqkeZfQozGrxgjO9oLxqTekgWU%3D" alt="image.png" />
AS 本身也带有 commit checks，建议需要的小伙伴可以按需选择。</p>

<h2 id="总结">总结</h2>

<h2 id="结语">结语</h2>

<p>nowinandroid项目展示了Google Android团队在代码质量管理方面的最佳实践。其CI体系通过分层校验、自动化防护、智能基线管理等手段，构建了完整质量保障生态。建议开发团队结合自身项目特点，选择性引入相关实践，逐步建立适应自身需求的质量保障体系。值得注意的是，任何自动化检查都应服务于开发效率与代码质量的平衡，避免过度校验导致的开发流程僵化。</p>]]></content><author><name>CaptainZ</name><email>jelychow@gmail.com</email></author><summary type="html"><![CDATA[Learn how the Now In Android project ensures code quality through CI workflows, dependency management, and automated testing.]]></summary></entry></feed>