<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ViO Tech</title>
    <description>The latest articles on DEV Community by ViO Tech (@vio_di_code).</description>
    <link>https://dev.to/vio_di_code</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3372636%2Fcf571c85-5763-47f5-a061-6f6b108aa671.jpg</url>
      <title>DEV Community: ViO Tech</title>
      <link>https://dev.to/vio_di_code</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vio_di_code"/>
    <language>en</language>
    <item>
      <title>📘 Paywall SDK – Tài liệu sử dụng TỪ A Z (kèm JSON mẫu)</title>
      <dc:creator>ViO Tech</dc:creator>
      <pubDate>Fri, 09 Jan 2026 04:24:59 +0000</pubDate>
      <link>https://dev.to/vio_di_code/paywall-sdk-tai-lieu-su-dung-tu-a-z-kem-json-mau-586f</link>
      <guid>https://dev.to/vio_di_code/paywall-sdk-tai-lieu-su-dung-tu-a-z-kem-json-mau-586f</guid>
      <description>&lt;p&gt;Tài liệu này hướng dẫn &lt;strong&gt;toàn bộ quy trình Paywall SDK&lt;/strong&gt;, từ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cấu trúc JSON config&lt;/li&gt;
&lt;li&gt;Parse &amp;amp; cung cấp dữ liệu&lt;/li&gt;
&lt;li&gt;Hiển thị UI (Fragment / Dialog)&lt;/li&gt;
&lt;li&gt;Purchase &amp;amp; Sale / Retention flow&lt;/li&gt;
&lt;li&gt;Best practices tối ưu conversion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Phù hợp cho &lt;strong&gt;Android Kotlin – app nội bộ &amp;amp; production&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1️⃣ Tổng quan kiến trúc Paywall
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Remote Config / API / Local JSON
        ↓
PaywallDataProvider
        ↓
PaywallRepository
        ↓
BasePaywallFragment / BasePaywallDialog
        ↓
Paywall UI (Full / One / Dialog)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2️⃣ JSON Paywall Config – Chuẩn sử dụng
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 JSON mẫu (FULL SCREEN)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uiType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"paywall_full_01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ctaId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timesOpen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"delayButtonX"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timesFree"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1,2,9,10,14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"showAdsClose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"packageId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test.com.year.discount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"packageIdOrigin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test.com.yearly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"packageType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yearly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ctaId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"isPopular"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"isSelected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"packageId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test.com.month.discount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"packageIdOrigin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test.com.monthly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"packageType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"weekly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ctaId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"isPopular"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"isSelected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"packageId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test.com.week.offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"packageType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"weekly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ctaId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"isPopular"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"isSelected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2.2 Giải thích từng field
&lt;/h3&gt;

&lt;h4&gt;
  
  
  🔹 Root level
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Ý nghĩa&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uiType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mapping UI (&lt;code&gt;paywall_full_01&lt;/code&gt;, &lt;code&gt;paywall_dialog_01&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ctaId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CTA mặc định nếu chưa chọn package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bật / tắt paywall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timesOpen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Số lần app mở mới show (0 = luôn check)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delayButtonX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delay hiển thị nút close (giây)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timesFree&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Danh sách lần sử dụng được free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;showAdsClose&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Đóng paywall bằng ads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  🔹 Package item
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Ý nghĩa&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;packageId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SKU sale / active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;packageIdOrigin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SKU gốc (để tính discount)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;packageType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;weekly / monthly / yearly / lifetime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ctaId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CTA riêng cho package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;isPopular&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hiển thị badge “Popular”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;isSelected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auto select khi mở paywall&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  3️⃣ Parse JSON → PaywallData
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parsePaywall&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;PaywallData&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isBlank&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Json&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ignoreUnknownKeys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
        &lt;span class="n"&gt;explicitNulls&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;decodeFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 JSON có thể lấy từ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Firebase Remote Config&lt;/li&gt;
&lt;li&gt;API backend&lt;/li&gt;
&lt;li&gt;Local config&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4️⃣ PaywallDataProvider – Cấp dữ liệu cho SDK
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Cung cấp Paywall config
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getPaywallConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;placementType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;PaywallData&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;placementType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;PAYWALL_HOME_ICON&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;homeIconJson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parsePaywall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nc"&gt;PAYWALL_SETTING&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;settingJson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parsePaywall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  4.2 Retention mapping
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getRetentionPlacement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;PAYWALL_HOME_ICON&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;PAYWALL_HOME_ICON_SALE&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5️⃣ PaywallRepository – Luồng sử dụng
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 Init
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;paywallRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDebugMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5.2 Check show paywall
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paywallRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shouldShow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PAYWALL_HOME_ICON&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showPaywall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5.3 Load data &amp;amp; product
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paywallRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPaywallData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PAYWALL_HOME_ICON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;packages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paywallRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPackages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6️⃣ Hiển thị UI Paywall
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6.1 Full Screen – Ví dụ PaywallFull01Fragment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaywallFull01Fragment&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nc"&gt;BasePaywallFragment&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FragmentPaywallFull01Binding&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
    &lt;span class="nc"&gt;PackageSubCallback&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;bindingInflater&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nc"&gt;FragmentPaywallFull01Binding&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;inflate&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;adapter&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;PackageSubAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onViewCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onViewCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rcvPackage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;layoutManager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GridLayoutManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="nd"&gt;@PaywallFull01Fragment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;btnContinue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOnClickListener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPaywallProduct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;imgClose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOnClickListener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txtPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOnClickListener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;openPolicy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txtTerm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOnClickListener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;openTerm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;bindData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaywallData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PaywallProduct&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submitData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onSelectPackage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paywallProduct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaywallProduct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;btnContinue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;PaywallCtaUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCtaStringResId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paywallProduct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;descRes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PaywallDescUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDescResIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;paywallProduct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;PaywallDescUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayoutType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FULL_SCREEN&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;descRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;descRes&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txtDescription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PaywallDescUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nf"&gt;requireContext&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;descRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;descRes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;paywallProduct&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 &lt;strong&gt;Khi dùng:&lt;/strong&gt; onboarding, hard paywall, feature lock&lt;/p&gt;




&lt;h3&gt;
  
  
  6.2 Dialog Sale – Ví dụ PaywallDialog01
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaywallDialog01&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nc"&gt;BasePaywallDialog&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DialogPaywall01Binding&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;bindingInflater&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nc"&gt;DialogPaywall01Binding&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;inflate&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;currentItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaywallProduct&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;bindData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaywallData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PaywallProduct&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstOrNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="n"&gt;currentItem&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;

        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txtPrice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priceText&lt;/span&gt;

        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;btnContinue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;PaywallCtaUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCtaStringResId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;descRes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PaywallDescUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDescResIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;PaywallDescUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayoutType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DIALOG_1_ITEM&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;descRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;descRes&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txtDescription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PaywallDescUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nf"&gt;requireContext&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;descRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;descRes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;item&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;btnContinue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOnClickListener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;currentItem&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;imgClose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOnClickListener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 &lt;strong&gt;Khi dùng:&lt;/strong&gt; retention, sale off, exit intent&lt;/p&gt;




&lt;h3&gt;
  
  
  6.2 Dialog (Sale / Retention)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;PaywallDialog01&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dùng cho:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sale&lt;/li&gt;
&lt;li&gt;Exit intent&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7️⃣ Package selection &amp;amp; CTA
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;PaywallCtaUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCtaStringResId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paywallProduct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CTA thay đổi theo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trial&lt;/li&gt;
&lt;li&gt;Sale&lt;/li&gt;
&lt;li&gt;Lifetime&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  8️⃣ Discount &amp;amp; Sale logic
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;packageIdOrigin&lt;/code&gt; ≠ null → enable sale&lt;/li&gt;
&lt;li&gt;Tính %OFF từ giá gốc&lt;/li&gt;
&lt;li&gt;Gạch ngang giá cũ&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  9️⃣ Purchase flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paywallProduct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SDK xử lý:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Billing&lt;/li&gt;
&lt;li&gt;Verify&lt;/li&gt;
&lt;li&gt;Emit purchaseStatus&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔟 Retention Flow (chuẩn)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Paywall A
  ↓ close
Sale Paywall Dialog
  ↓ close
No more paywall
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  1️⃣1️⃣ Best Practices (Rất quan trọng)
&lt;/h2&gt;

&lt;p&gt;✅ Full screen: 2–3 packages&lt;/p&gt;

&lt;p&gt;✅ Dialog sale: 1 package&lt;/p&gt;

&lt;p&gt;✅ Weekly = entry price thấp&lt;/p&gt;

&lt;p&gt;✅ Yearly = best value&lt;/p&gt;

&lt;p&gt;✅ Lifetime = anchor price&lt;/p&gt;




&lt;h2&gt;
  
  
  1️⃣2️⃣ Checklist tích hợp nhanh
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] JSON config hợp lệ&lt;/li&gt;
&lt;li&gt;[ ] Map placement → config&lt;/li&gt;
&lt;li&gt;[ ] Init repository&lt;/li&gt;
&lt;li&gt;[ ] Test sandbox purchase&lt;/li&gt;
&lt;li&gt;[ ] Test sale / retention&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1️⃣3️⃣ Ví dụ thực tế: Tích hợp Paywall trong SettingFragment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mục tiêu
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Soft paywall (user chủ động mua)&lt;/li&gt;
&lt;li&gt;Upsell nhẹ, không làm gián đoạn trải nghiệm&lt;/li&gt;
&lt;li&gt;Cho phép quản lý subscription sau khi mua&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Inject dependency
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;paywallRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaywallRepository&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;launcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaywallLauncher&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Hiển thị Subscribe CTA theo config &amp;amp; trạng thái mua
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;ctlSub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isVisible&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;paywallRepository&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPaywallData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Constants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PaywallConfigKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PAYWALL_SETTING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;txtSubDes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paywallRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isPurchased&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unlocked_all_features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upgrade_to_unlock_all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Manage Subscription (chỉ khi là subscription)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;ctlManageSubscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isVisible&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;paywallRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isPurchased&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="n"&gt;paywallRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentPackagePurchased&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="nc"&gt;PandaPackageType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LIFETIME&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 Lifetime không cần manage → UX chuẩn Play Store&lt;/p&gt;




&lt;h3&gt;
  
  
  Click Subscribe → Mở Paywall bằng Launcher
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;ctlSub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOnClickListener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;lifecycleScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;activity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="nd"&gt;@launch&lt;/span&gt;

        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nc"&gt;Constants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PaywallConfigKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PAYWALL_SETTING&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;PaywallResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Success&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;ctlManageSubscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isVisible&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                    &lt;span class="n"&gt;paywallRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isPurchased&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
                    &lt;span class="n"&gt;paywallRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentPackagePurchased&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="nc"&gt;PandaPackageType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LIFETIME&lt;/span&gt;

                &lt;span class="n"&gt;txtSubDes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unlocked_all_features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="nc"&gt;NativeAdManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disableAllAds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nc"&gt;InterManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disableAllAds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nc"&gt;RewardAdsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disableAllAds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nc"&gt;BannerAdManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disableAllAds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nc"&gt;PandaResumeAd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;INSTANCE&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;disableAllAds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nc"&gt;PaywallResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dismissed&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// User đóng paywall&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nc"&gt;PaywallResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NotShown&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Đã mua hoặc chưa đủ điều kiện show&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Flow tổng quát
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Setting Screen
   ↓
Subscribe CTA
   ↓ click
PaywallLauncher
   ↓
┌───────────────┬───────────────┬───────────────┐
│ Success       │ Dismissed     │ NotShown      │
│ Disable ads   │ Log event     │ Continue app  │
│ Update UI     │               │               │
└───────────────┴───────────────┴───────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Best Practices cho Setting Paywall
&lt;/h3&gt;

&lt;p&gt;✅ Soft paywall, không ép&lt;/p&gt;

&lt;p&gt;✅ Không show paywall nếu &lt;code&gt;enable = false&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;✅ Lifetime ≠ manage subscription&lt;/p&gt;

&lt;p&gt;✅ Disable ads &lt;strong&gt;chỉ khi Success&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;✅ Update UI ngay, không reload màn hình&lt;/p&gt;

</description>
      <category>android</category>
      <category>architecture</category>
      <category>kotlin</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>⚡ Jetpack Compose Performance: Macrobenchmark &amp; Baseline Profile</title>
      <dc:creator>ViO Tech</dc:creator>
      <pubDate>Fri, 02 Jan 2026 14:58:31 +0000</pubDate>
      <link>https://dev.to/vio_di_code/jetpack-compose-performance-macrobenchmark-baseline-profile-373b</link>
      <guid>https://dev.to/vio_di_code/jetpack-compose-performance-macrobenchmark-baseline-profile-373b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A &lt;strong&gt;production-grade guide&lt;/strong&gt; to measuring, optimizing, and &lt;em&gt;locking in&lt;/em&gt; Jetpack Compose performance using &lt;strong&gt;Macrobenchmark&lt;/strong&gt; and &lt;strong&gt;Baseline Profiles&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🎯 Why Macrobenchmark &amp;amp; Baseline Profile matter (especially for Compose)
&lt;/h2&gt;

&lt;p&gt;Jetpack Compose performance problems often &lt;strong&gt;do not show up in debug builds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In production, users experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Janky scroll&lt;/li&gt;
&lt;li&gt;Slow cold start&lt;/li&gt;
&lt;li&gt;Frame drops after navigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two tools solve this &lt;em&gt;systematically&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Macrobenchmark&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Measure real performance (startup, scroll, animation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Baseline Profile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Make release builds fast from first launch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you use Compose &lt;strong&gt;without these&lt;/strong&gt;, you are flying blind.&lt;/p&gt;




&lt;h2&gt;
  
  
  1️⃣ Macrobenchmark – Measuring Real User Performance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🧠 What Macrobenchmark actually measures
&lt;/h3&gt;

&lt;p&gt;Macrobenchmark runs your &lt;strong&gt;real app APK&lt;/strong&gt; in a &lt;strong&gt;realistic environment&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Release build&lt;/li&gt;
&lt;li&gt;No debugger&lt;/li&gt;
&lt;li&gt;No Compose tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You measure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App startup (cold / warm / hot)&lt;/li&gt;
&lt;li&gt;Frame timing (jank)&lt;/li&gt;
&lt;li&gt;Scroll performance&lt;/li&gt;
&lt;li&gt;Navigation cost&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🧱 Setup Macrobenchmark module
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew :macrobenchmark:connectedCheck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;build.gradle (macrobenchmark module)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;android&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;compileSdk&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;34&lt;/span&gt;

    &lt;span class="nf"&gt;defaultConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;minSdk&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;
        &lt;span class="n"&gt;targetSdk&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;34&lt;/span&gt;
        &lt;span class="n"&gt;testInstrumentationRunner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="s"&gt;"androidx.benchmark.junit4.AndroidBenchmarkRunner"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx.benchmark:benchmark-macro-junit4:1.2.4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🚀 Startup benchmark example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RunWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AndroidJUnit4&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StartupBenchmark&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;Rule&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;benchmarkRule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MacrobenchmarkRule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;coldStartup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;benchmarkRule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;measureRepeated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;packageName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.vio.app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StartupTimingMetric&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="n"&gt;iterations&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;startupMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StartupMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;COLD&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;pressHome&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;startActivityAndWait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 &lt;strong&gt;Compose-specific insight&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large Composables during initial composition = slower cold start&lt;/li&gt;
&lt;li&gt;Heavy &lt;code&gt;remember&lt;/code&gt; logic also counts&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  📉 Frame timing benchmark (scroll)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;scrollRecordingList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;benchmarkRule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;measureRepeated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;packageName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.vio.app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FrameTimingMetric&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="n"&gt;iterations&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;startActivityAndWait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;res&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"recording_list"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Direction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DOWN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ Detects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jank &amp;gt; 16ms&lt;/li&gt;
&lt;li&gt;Layout thrashing&lt;/li&gt;
&lt;li&gt;Excessive recomposition during scroll&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2️⃣ Reading Macrobenchmark Results (What Actually Matters)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔍 Key metrics
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What to watch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;startupMs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cold start regression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;frameDurationCpuMs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CPU bottlenecks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;frameOverrunMs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;UI jank&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;📌 Rule of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&amp;gt; 5% janky frames → user-visible&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3️⃣ Baseline Profile – Making Compose Fast on First Launch
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🧠 What is Baseline Profile?
&lt;/h3&gt;

&lt;p&gt;Baseline Profile tells ART:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Compile &lt;em&gt;these code paths ahead of time&lt;/em&gt;."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compose code interpreted&lt;/li&gt;
&lt;li&gt;JIT kicks in late&lt;/li&gt;
&lt;li&gt;First launch = slow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AOT compilation&lt;/li&gt;
&lt;li&gt;Fast startup from launch #1&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🧱 Setup Baseline Profile module
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx.profileinstaller:profileinstaller:1.3.1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;strong&gt;baselineprofile&lt;/strong&gt; module.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧪 Generate Baseline Profile
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RunWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AndroidJUnit4&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaselineProfileGenerator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;Rule&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BaselineProfileRule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;packageName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.vio.app"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;pressHome&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;startActivityAndWait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;// Navigate critical paths&lt;/span&gt;
        &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;res&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"recording_list"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;res&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"recording_item_0"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ This captures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial composition&lt;/li&gt;
&lt;li&gt;Navigation&lt;/li&gt;
&lt;li&gt;Scroll&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  📦 Applying Baseline Profile
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew :baselineprofile:assembleRelease
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Profile is packaged into APK → applied automatically on install.&lt;/p&gt;




&lt;h2&gt;
  
  
  4️⃣ Compose-specific Best Practices for Baseline Profiles
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ What to include
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;App start&lt;/li&gt;
&lt;li&gt;First screen composition&lt;/li&gt;
&lt;li&gt;Navigation graph&lt;/li&gt;
&lt;li&gt;Lazy list scroll&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ❌ What NOT to include
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Rare flows&lt;/li&gt;
&lt;li&gt;Debug-only screens&lt;/li&gt;
&lt;li&gt;Feature flags off by default&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5️⃣ Macrobenchmark + Baseline Profile Workflow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write benchmark → measure baseline
↓
Optimize Compose
↓
Regenerate Baseline Profile
↓
Lock performance in CI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 &lt;strong&gt;Golden rule&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every performance fix should come with a benchmark.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6️⃣ Common Compose Performance Traps (Seen in Benchmarks)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trap&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reading Flow at root&lt;/td&gt;
&lt;td&gt;Startup slow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing Lazy keys&lt;/td&gt;
&lt;td&gt;Scroll jank&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animation in composition&lt;/td&gt;
&lt;td&gt;Frame drops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unstable lambdas&lt;/td&gt;
&lt;td&gt;Layout thrash&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Macrobenchmark exposes all of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  7️⃣ Before / After Numbers – Real Impact
&lt;/h2&gt;

&lt;h3&gt;
  
  
  📊 Case study (realistic production scenario)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After Macrobenchmark + Baseline Profile&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold start (P50)&lt;/td&gt;
&lt;td&gt;820 ms&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;420 ms&lt;/strong&gt; (-48%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start (P90)&lt;/td&gt;
&lt;td&gt;1,200 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;620 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Janky frames (scroll)&lt;/td&gt;
&lt;td&gt;12–15%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.1%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First navigation&lt;/td&gt;
&lt;td&gt;180 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;70 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;📌 Key takeaway:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Most gains come from &lt;strong&gt;Baseline Profile on first launch&lt;/strong&gt;, not micro-optimizations.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  8️⃣ CI: Fail the Build on Performance Regression
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🎯 Goal
&lt;/h3&gt;

&lt;p&gt;Make performance &lt;strong&gt;non-negotiable&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧪 Step 1: Export benchmark results
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;benchmarkRule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;measureRepeated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// metrics automatically exported as JSON&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results live under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/benchmark-results/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🧪 Step 2: Define thresholds
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"coldStartMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jankPercent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🧪 Step 3: CI assertion (example)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COLD_START&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; &lt;span class="s2"&gt;"500"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"❌ Cold start regression detected"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ Result: PR fails if performance regresses.&lt;/p&gt;




&lt;h2&gt;
  
  
  9️⃣ Follow-up: Why My Compose App Was Slow Without Baseline Profile
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🚨 Symptoms
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;First launch slow&lt;/li&gt;
&lt;li&gt;Second launch magically fast&lt;/li&gt;
&lt;li&gt;No obvious bottlenecks in profiler&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🧠 Root cause
&lt;/h3&gt;

&lt;p&gt;Without Baseline Profile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compose runtime interpreted&lt;/li&gt;
&lt;li&gt;No AOT compilation&lt;/li&gt;
&lt;li&gt;JIT warms up too late&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 This affects &lt;strong&gt;every user&lt;/strong&gt;, not just low-end devices.&lt;/p&gt;




&lt;h3&gt;
  
  
  ✅ Fix
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Add Baseline Profile&lt;/li&gt;
&lt;li&gt;Capture startup + navigation paths&lt;/li&gt;
&lt;li&gt;Ship profile with release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Performance fixed &lt;strong&gt;without touching UI code&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🔟 Open-source: Compose Benchmark Template Repo
&lt;/h2&gt;

&lt;h3&gt;
  
  
  📦 What the template includes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Macrobenchmark module&lt;/li&gt;
&lt;li&gt;Baseline Profile module&lt;/li&gt;
&lt;li&gt;Startup &amp;amp; scroll benchmarks&lt;/li&gt;
&lt;li&gt;CI-ready scripts&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  📁 Repo structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
macrobenchmark/
baselineprofile/
ci/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🚀 How to use
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Clone repo&lt;/li&gt;
&lt;li&gt;Rename package&lt;/li&gt;
&lt;li&gt;Adjust benchmarks&lt;/li&gt;
&lt;li&gt;Run CI&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;📌 Perfect starting point for any Compose app.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Final Mental Model
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Macrobenchmark shows the truth. Baseline Profile locks the win. CI keeps it that way.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Compose performance is not about hope — it is about measurement.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Ultimate Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔲 Benchmarked cold &amp;amp; warm startup&lt;/li&gt;
&lt;li&gt;🔲 Measured scroll jank&lt;/li&gt;
&lt;li&gt;🔲 Baseline Profile generated&lt;/li&gt;
&lt;li&gt;🔲 CI guards regressions&lt;/li&gt;
&lt;li&gt;🔲 Numbers documented&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📦 I can generate the &lt;strong&gt;actual GitHub template repo&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;📊 Add charts for dev.to&lt;/li&gt;
&lt;li&gt;🧪 Write a CI-ready GitHub Action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Just say the word 🚀&lt;/p&gt;

</description>
      <category>android</category>
      <category>jetpackcompose</category>
      <category>kotlin</category>
      <category>performance</category>
    </item>
    <item>
      <title>🚀 Jetpack Compose Performance Audit</title>
      <dc:creator>ViO Tech</dc:creator>
      <pubDate>Fri, 02 Jan 2026 14:51:07 +0000</pubDate>
      <link>https://dev.to/vio_di_code/jetpack-compose-performance-audit-4okp</link>
      <guid>https://dev.to/vio_di_code/jetpack-compose-performance-audit-4okp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A &lt;strong&gt;practical, production-ready guide&lt;/strong&gt; to auditing Jetpack Compose performance — from checklists, automated tests for excessive recompositions, real screen analysis, to a clear comparison with View-based performance traps.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🎯 Why this article?
&lt;/h2&gt;

&lt;p&gt;Jetpack Compose is &lt;em&gt;declarative&lt;/em&gt;, but &lt;strong&gt;performance problems don’t disappear — they just change shape&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In real production apps, most Compose performance issues come from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Excessive recompositions&lt;/li&gt;
&lt;li&gt;Wrong state boundaries&lt;/li&gt;
&lt;li&gt;Unstable objects &amp;amp; lambdas&lt;/li&gt;
&lt;li&gt;Animations running in the wrong phase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ A &lt;strong&gt;Compose performance audit checklist&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ A &lt;strong&gt;real automated test&lt;/strong&gt; to catch excessive recompositions&lt;/li&gt;
&lt;li&gt;✅ A &lt;strong&gt;step-by-step analysis of a Compose screen&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ A &lt;strong&gt;Compose vs View performance comparison&lt;/strong&gt; with concrete examples &amp;amp; tests&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1️⃣ Performance Audit Checklist for Jetpack Compose
&lt;/h2&gt;

&lt;p&gt;Use this checklist whenever you review a Compose screen or PR.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧠 A. Composition &amp;amp; Recomposition
&lt;/h3&gt;

&lt;p&gt;✔ Are states read at the &lt;strong&gt;lowest possible composable&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;❌ Anti-pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;Screen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;state&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collectAsState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ Better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ Checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No frequently-changing state read at screen root&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;derivedStateOf&lt;/code&gt; for computed values&lt;/li&gt;
&lt;li&gt;Composables recompose only when their &lt;em&gt;inputs&lt;/em&gt; change&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🧠 B. Object Allocation
&lt;/h3&gt;

&lt;p&gt;✔ Are objects created inside composition?&lt;/p&gt;

&lt;p&gt;❌ Anti-pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;Brush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linearGradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ Fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;brush&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Brush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linearGradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ Checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Brush&lt;/code&gt;, &lt;code&gt;Shape&lt;/code&gt;, &lt;code&gt;TextStyle&lt;/code&gt;, &lt;code&gt;Path&lt;/code&gt;, &lt;code&gt;Outline&lt;/code&gt; are remembered&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;drawBehind&lt;/code&gt; for purely visual effects&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🧠 C. Stability
&lt;/h3&gt;

&lt;p&gt;✔ Are your models stable?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Immutable&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;UiItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ Checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@Immutable&lt;/code&gt; / &lt;code&gt;@Stable&lt;/code&gt; used where applicable&lt;/li&gt;
&lt;li&gt;Avoid &lt;code&gt;MutableList&lt;/code&gt;, &lt;code&gt;MutableMap&lt;/code&gt; in UI models&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🧠 D. Lazy Layouts
&lt;/h3&gt;

&lt;p&gt;✔ Are keys provided?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ Checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No index-based keys&lt;/li&gt;
&lt;li&gt;Item-level state isolated&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🧠 E. Animations
&lt;/h3&gt;

&lt;p&gt;✔ Where does the animation run?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Animation type&lt;/th&gt;
&lt;th&gt;Correct phase&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Layout change&lt;/td&gt;
&lt;td&gt;Composition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual only&lt;/td&gt;
&lt;td&gt;Draw / graphicsLayer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;✔ Checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;animate*AsState&lt;/code&gt; driving layout unintentionally&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;graphicsLayer&lt;/code&gt; for translations, alpha, scale&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2️⃣ Demo: Automated Test to Catch Excessive Recompositions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🎯 Goal
&lt;/h3&gt;

&lt;p&gt;Turn recomposition behavior into something &lt;strong&gt;testable &amp;amp; enforceable&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1: Create a recomposition counter modifier
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;RecomposeCountKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SemanticsPropertyKey&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"RecomposeCount"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;SemanticsPropertyReceiver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recomposeCount&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nc"&gt;RecomposeCountKey&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trackRecompositions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;composed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;count&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;SideEffect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;++&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;semantics&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;recomposeCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 2: Use it in your composable
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ProfileCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trackRecompositions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 3: Write a Compose UI test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;profileCard_shouldNotRecomposeExcessively&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;composeRule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ProfileCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Vio"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;composeRule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForIdle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;composeRule&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hasAnyAncestor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isRoot&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchSemanticsNode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;RecomposeCountKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isLessThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ This test &lt;strong&gt;fails automatically&lt;/strong&gt; if someone introduces excessive recompositions later.&lt;/p&gt;




&lt;h2&gt;
  
  
  3️⃣ Real-world Analysis: A Compose Screen
&lt;/h2&gt;

&lt;h3&gt;
  
  
  📱 Example: Recording List Screen
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;RecordingScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RecordingVM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;state&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collectAsState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nc"&gt;LazyColumn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;RecordingItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ❌ Problems
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Entire list recomposes on any state change&lt;/li&gt;
&lt;li&gt;No item keys&lt;/li&gt;
&lt;li&gt;RecordingItem recreated unnecessarily&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ Refactored Version
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;RecordingScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RecordingVM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;records&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collectAsState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nc"&gt;LazyColumn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;RecordingItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🎯 Result
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Only changed items recompose&lt;/li&gt;
&lt;li&gt;Scroll remains smooth&lt;/li&gt;
&lt;li&gt;Predictable performance&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4️⃣ Compose vs View Performance Traps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔴 Trap #1: "Compose is slower than Views"
&lt;/h3&gt;

&lt;p&gt;❌ Wrong comparison:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unoptimized Compose vs optimized RecyclerView&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✔ Fair comparison:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stable models&lt;/li&gt;
&lt;li&gt;Keys&lt;/li&gt;
&lt;li&gt;Proper state scoping&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🔴 Trap #2: Frequent invalidation
&lt;/h3&gt;

&lt;h4&gt;
  
  
  View system
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// redraw only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Compose equivalent
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawBehind&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;❌ Bad Compose equivalent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; &lt;span class="c1"&gt;// triggers recomposition&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🔴 Trap #3: Measuring everything again
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;View&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;requestLayout()&lt;/code&gt; expensive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compose&lt;/td&gt;
&lt;td&gt;Layout skipped if inputs stable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;✔ Compose wins &lt;strong&gt;if stability is respected&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  5️⃣ How to Test Performance (Compose &amp;amp; View)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🧪 Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Layout Inspector (Recomposition Count)&lt;/li&gt;
&lt;li&gt;Recomposition Highlighter&lt;/li&gt;
&lt;li&gt;Macrobenchmark&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📊 Macrobenchmark example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;measureRepeated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;packageName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.vio.app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FrameTimingMetric&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="n"&gt;iterations&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;startActivityAndWait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🧠 Final Mental Model
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Compose performance = state boundaries + stability + correct phase (composition vs draw)&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you control those three, Compose is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Faster than Views&lt;/li&gt;
&lt;li&gt;More predictable&lt;/li&gt;
&lt;li&gt;Easier to test&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ✅ TL;DR Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔲 No unnecessary recompositions&lt;/li&gt;
&lt;li&gt;🔲 Stable models &amp;amp; lambdas&lt;/li&gt;
&lt;li&gt;🔲 Keys everywhere in Lazy layouts&lt;/li&gt;
&lt;li&gt;🔲 Animations in draw phase when possible&lt;/li&gt;
&lt;li&gt;🔲 Performance covered by tests&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔍 A &lt;strong&gt;performance audit template&lt;/strong&gt; for your app&lt;/li&gt;
&lt;li&gt;🧪 A &lt;strong&gt;ready-to-use testing module&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🧠 A &lt;strong&gt;deep dive into Compose internals (slot table, skipping, groups)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Let me know — happy to go deeper 🚀&lt;/p&gt;

</description>
      <category>android</category>
      <category>jetpackcompose</category>
      <category>kotlin</category>
      <category>performance</category>
    </item>
    <item>
      <title>Android Session Tracking — A Senior Engineer’s Perspective: When the OS Promises You Nothing</title>
      <dc:creator>ViO Tech</dc:creator>
      <pubDate>Tue, 30 Dec 2025 15:09:40 +0000</pubDate>
      <link>https://dev.to/vio_di_code/android-session-tracking-a-senior-engineers-perspective-when-the-os-promises-you-nothing-43g8</link>
      <guid>https://dev.to/vio_di_code/android-session-tracking-a-senior-engineers-perspective-when-the-os-promises-you-nothing-43g8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Android makes &lt;strong&gt;no promise&lt;/strong&gt; about telling you when a user leaves your app.&lt;br&gt;
Yet analytics, ads, and business logic are &lt;strong&gt;forced to assume&lt;/strong&gt; such a thing exists.&lt;/p&gt;

&lt;p&gt;This article is not about tricks. It is about &lt;strong&gt;thinking correctly&lt;/strong&gt; about session tracking on Android — at production and SDK level.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. The real problem is not APIs — it’s illusion
&lt;/h2&gt;

&lt;p&gt;Most Android developers have believed at least one of these at some point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;onStop()&lt;/code&gt; means the user left&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onDestroy()&lt;/code&gt; means the app ended&lt;/li&gt;
&lt;li&gt;Swiping from Recents ends a session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these are &lt;strong&gt;fundamentally wrong&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Android does not run apps for you.&lt;br&gt;
Android runs &lt;strong&gt;itself&lt;/strong&gt;. Your app is a guest.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. OS truths senior engineers must accept
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Processes can be killed &lt;strong&gt;without callbacks&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;SIGKILL / LMK offer &lt;strong&gt;zero cleanup guarantees&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Lifecycle events only exist &lt;strong&gt;while the process is alive&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you are waiting for a callback to detect session end,&lt;br&gt;
you already lost — conceptually.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  3. How real SDKs actually work
&lt;/h2&gt;

&lt;p&gt;Firebase, AdMob, Adjust, AppsFlyer do not “detect kills”.&lt;/p&gt;

&lt;p&gt;They do three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Track &lt;strong&gt;process foreground / background&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Apply &lt;strong&gt;timeout heuristics&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infer session end on the next launch&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No events. No guarantees. Only &lt;strong&gt;probabilistic modeling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is senior thinking: &lt;em&gt;accept uncertainty and design around it&lt;/em&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. A session is not an event — it’s a state machine
&lt;/h2&gt;

&lt;p&gt;A real session has states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Created&lt;/li&gt;
&lt;li&gt;Active&lt;/li&gt;
&lt;li&gt;Background&lt;/li&gt;
&lt;li&gt;Expired&lt;/li&gt;
&lt;li&gt;Restored (inferred)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A session does not end when the app dies.&lt;br&gt;
It ends when &lt;strong&gt;we decide it is no longer valid&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That decision is based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Background duration&lt;/li&gt;
&lt;li&gt;Next launch timing&lt;/li&gt;
&lt;li&gt;Persisted metadata&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  5. ProcessLifecycleOwner: the right tool, not a magic one
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ProcessLifecycleOwner&lt;/code&gt; gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ON_START&lt;/code&gt;: process enters foreground&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ON_STOP&lt;/code&gt;: process goes to background&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does &lt;em&gt;not&lt;/em&gt; give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App kill&lt;/li&gt;
&lt;li&gt;Swipe from Recents&lt;/li&gt;
&lt;li&gt;Native crashes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it &lt;strong&gt;shouldn’t&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Senior engineers do not demand APIs do the impossible.&lt;/p&gt;


&lt;h2&gt;
  
  
  6. Timeout is not a workaround — it’s a definition
&lt;/h2&gt;

&lt;p&gt;Timeout is not a hack.&lt;/p&gt;

&lt;p&gt;Timeout is a &lt;strong&gt;business rule&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“If the user stays away longer than X ms, the session is considered ended.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;30 seconds? 1 minute? 5 minutes?&lt;/p&gt;

&lt;p&gt;There is no correct value — only &lt;strong&gt;product-appropriate&lt;/strong&gt; ones.&lt;/p&gt;


&lt;h2&gt;
  
  
  7. App kill: no callback, only traces
&lt;/h2&gt;

&lt;p&gt;When the app is killed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;onSessionEnd&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;No cleanup&lt;/li&gt;
&lt;li&gt;No flush&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What remains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Last background timestamp&lt;/li&gt;
&lt;li&gt;Session ID&lt;/li&gt;
&lt;li&gt;Inferred reason&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the next launch, the SDK must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load persisted state&lt;/li&gt;
&lt;li&gt;Infer the previous session ended&lt;/li&gt;
&lt;li&gt;Emit a &lt;em&gt;logical&lt;/em&gt; session end&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a &lt;strong&gt;two-phase design&lt;/strong&gt; — familiar to senior engineers.&lt;/p&gt;


&lt;h2&gt;
  
  
  8. Exit reasons: be honest, not confident
&lt;/h2&gt;

&lt;p&gt;Exit reasons should be modeled as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;USER_BACKGROUND_TIMEOUT&lt;/li&gt;
&lt;li&gt;PROCESS_KILLED_INFERRED&lt;/li&gt;
&lt;li&gt;APP_UPGRADE&lt;/li&gt;
&lt;li&gt;CRASH_DETECTED&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key word is &lt;strong&gt;inferred&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Good SDKs do not say “the user did X”.&lt;br&gt;
They say “based on evidence, we infer X”.&lt;/p&gt;


&lt;h2&gt;
  
  
  9. Why this module is testable — and why that matters
&lt;/h2&gt;

&lt;p&gt;If session tracking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;depends on Activities&lt;/li&gt;
&lt;li&gt;relies on system callbacks&lt;/li&gt;
&lt;li&gt;depends on real time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;→ it is &lt;strong&gt;not testable&lt;/strong&gt;, and therefore &lt;strong&gt;not trustworthy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A proper SessionTracker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;is pure logic&lt;/li&gt;
&lt;li&gt;injects its clock&lt;/li&gt;
&lt;li&gt;fakes lifecycle signals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testable means understandable.&lt;br&gt;
Understandable means reliable.&lt;/p&gt;


&lt;h2&gt;
  
  
  10. How to use the Session Tracking SDK
&lt;/h2&gt;

&lt;p&gt;The theory matters. Now the practice.&lt;/p&gt;

&lt;p&gt;This SDK is designed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;avoid Activity dependencies&lt;/li&gt;
&lt;li&gt;never crash on process kill&lt;/li&gt;
&lt;li&gt;never block the main thread&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  10.1 Initialize in Application
&lt;/h3&gt;

&lt;p&gt;The SDK &lt;strong&gt;must&lt;/strong&gt; be initialized in &lt;code&gt;Application.onCreate()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;lateinit&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;sessionObserver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AndroidSessionObserver&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;sessionObserver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AndroidSessionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeoutMs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30_000L&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;callback&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="err"&gt;: &lt;/span&gt;&lt;span class="nc"&gt;SessionTracker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Callback&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onSessionStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// analytics / ads init&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onSessionEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ExitReason&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// flush analytics / revenue sync&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nc"&gt;ProcessLifecycleOwner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lifecycle&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionObserver&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No Activity lifecycle usage&lt;/li&gt;
&lt;li&gt;No UI references&lt;/li&gt;
&lt;li&gt;Callbacks &lt;strong&gt;may never fire&lt;/strong&gt; if the app is killed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SDK is designed with this assumption.&lt;/p&gt;




&lt;h3&gt;
  
  
  10.2 What happens when the app is killed?
&lt;/h3&gt;

&lt;p&gt;No callback is invoked.&lt;/p&gt;

&lt;p&gt;Instead, the SDK:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;persists session state on background&lt;/li&gt;
&lt;li&gt;stores timestamp and session ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the &lt;strong&gt;next launch&lt;/strong&gt;, it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;restores previous state&lt;/li&gt;
&lt;li&gt;infers the session ended&lt;/li&gt;
&lt;li&gt;emits &lt;code&gt;ExitReason.PROCESS_KILLED_INFERRED&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This behavior is intentional.&lt;/p&gt;




&lt;h3&gt;
  
  
  10.3 Interpreting ExitReason correctly
&lt;/h3&gt;

&lt;p&gt;Exit reasons are &lt;strong&gt;not absolute truth&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;USER_BACKGROUND_TIMEOUT&lt;/code&gt; → time-based evidence&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PROCESS_KILLED_INFERRED&lt;/code&gt; → missing resume inference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Analytics and backend systems &lt;strong&gt;must treat these as inferred data&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The SDK does not hide this.&lt;/p&gt;




&lt;h3&gt;
  
  
  10.4 Unit testing: why you should trust this SDK
&lt;/h3&gt;

&lt;p&gt;All session logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;is framework-independent&lt;/li&gt;
&lt;li&gt;injects time&lt;/li&gt;
&lt;li&gt;is fully testable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`session&lt;/span&gt; &lt;span class="n"&gt;ends&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="nf"&gt;timeout`&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;fakeClock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeClock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;tracker&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SessionTracker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fakeClock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeoutMs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;tracker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onForeground&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;fakeClock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;advance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;31_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tracker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onForeground&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nf"&gt;assertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tracker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastExitReason&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;ExitReason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testable logic produces deterministic behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Full source code
&lt;/h2&gt;

&lt;p&gt;The complete SDK, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pure SessionTracker logic&lt;/li&gt;
&lt;li&gt;AndroidSessionObserver&lt;/li&gt;
&lt;li&gt;ExitReason model&lt;/li&gt;
&lt;li&gt;Unit tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;is available on GitHub:&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://github.com/vinhvox/ViO---Android-Session-Tracker" rel="noopener noreferrer"&gt;https://github.com/vinhvox/ViO---Android-Session-Tracker&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This repository is designed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read like documentation&lt;/li&gt;
&lt;li&gt;be copy-paste friendly&lt;/li&gt;
&lt;li&gt;and, most importantly, &lt;em&gt;not pretend Android is controllable&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  12. Final words for senior engineers
&lt;/h2&gt;

&lt;p&gt;Session tracking on Android is never perfect.&lt;/p&gt;

&lt;p&gt;Good SDKs do not hide that.&lt;br&gt;
They:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;state their limits clearly&lt;/li&gt;
&lt;li&gt;model uncertainty explicitly&lt;/li&gt;
&lt;li&gt;help the business make &lt;strong&gt;correct&lt;/strong&gt;, not just pretty, decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Android does not give you absolute truth.&lt;br&gt;
But it gives enough signals to infer — if you design correctly.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>android</category>
      <category>architecture</category>
      <category>systems</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Building a Modern Android UI Stack with Jetpack Compose (Senior Guide</title>
      <dc:creator>ViO Tech</dc:creator>
      <pubDate>Tue, 30 Dec 2025 14:17:43 +0000</pubDate>
      <link>https://dev.to/vio_di_code/building-a-modern-android-ui-stack-with-jetpack-compose-senior-guide-cfc</link>
      <guid>https://dev.to/vio_di_code/building-a-modern-android-ui-stack-with-jetpack-compose-senior-guide-cfc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A practical, scalable, and testable UI architecture using&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Jetpack Compose + Navigation 3 + MVVM + MVI + Koin + Ktor + Room&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🎯 Goal
&lt;/h2&gt;

&lt;p&gt;This article presents a &lt;strong&gt;production-ready UI stack&lt;/strong&gt; for modern Android apps, focusing on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Predictable state management&lt;/li&gt;
&lt;li&gt;Navigation as state (Navigation 3)&lt;/li&gt;
&lt;li&gt;Performance &amp;amp; recomposition safety&lt;/li&gt;
&lt;li&gt;Scalability for large teams&lt;/li&gt;
&lt;li&gt;High testability&lt;/li&gt;
&lt;li&gt;Fast feature development&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is &lt;strong&gt;not a beginner tutorial&lt;/strong&gt; — it’s a &lt;strong&gt;Senior-level playbook&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧱 Tech Stack (Final Choice)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;Jetpack Compose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Navigation&lt;/td&gt;
&lt;td&gt;Navigation 3 (State-driven)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;MVVM + MVI hybrid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DI&lt;/td&gt;
&lt;td&gt;Koin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Networking&lt;/td&gt;
&lt;td&gt;Ktor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local Storage&lt;/td&gt;
&lt;td&gt;Room&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Async&lt;/td&gt;
&lt;td&gt;Coroutines + Flow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🧠 Core Philosophy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1️⃣ UI is a pure function of State
&lt;/h3&gt;

&lt;h3&gt;
  
  
  2️⃣ Navigation is State
&lt;/h3&gt;

&lt;h3&gt;
  
  
  3️⃣ ViewModel never knows UI or Navigation
&lt;/h3&gt;

&lt;h3&gt;
  
  
  4️⃣ Side-effects are explicit
&lt;/h3&gt;

&lt;h3&gt;
  
  
  5️⃣ Everything must be testable without Android runtime
&lt;/h3&gt;




&lt;h2&gt;
  
  
  📦 Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feature-home/
├── HomeContract.kt
├── HomeViewModel.kt
├── HomeRoute.kt
├── HomeScreen.kt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🧭 Navigation 3 – Navigation as State
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Define Screens
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Immutable&lt;/span&gt;
&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Screen&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Home&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Screen&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Detail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Screen&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BackStack as State&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;backStack&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberSaveable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mutableStateListOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Screen&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nc"&gt;Screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Navigate → &lt;code&gt;add(Screen)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Back → &lt;code&gt;removeLast()&lt;/code&gt;&lt;br&gt;
No &lt;code&gt;NavController&lt;/code&gt;.&lt;br&gt;
No &lt;code&gt;NavGraph&lt;/code&gt;.&lt;br&gt;
Just state.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;🧭 AppNavigationHost&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;AppNavigationHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;backStack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Screen&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onNavigate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Screen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onBack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;screen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;backStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Home&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;HomeRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;onNavigateDetail&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;onNavigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Detail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;Screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Detail&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;DetailRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;onBack&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;onBack&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ Explicit&lt;br&gt;
✔ Testable&lt;br&gt;
✔ No magic&lt;/p&gt;



&lt;p&gt;🧩 MVVM + MVI Hybrid&lt;br&gt;
&lt;strong&gt;Contract&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Load&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;ItemClicked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;NavigateDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Effect&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;🧠 ViewModel (Pure Logic)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ItemRepository&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;_state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_state&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;_effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receiveAsFlow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Load&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;loadItems&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ItemClicked&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="nf"&gt;sendEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NavigateDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;loadItems&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;viewModelScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;sendEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;viewModelScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;❌ No Compose&lt;br&gt;
❌ No Navigation&lt;br&gt;
✅ 100% testable&lt;/p&gt;



&lt;p&gt;🧩 Route – Glue Layer&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;HomeRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;onNavigateDetail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HomeViewModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;koinViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;state&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collectAsStateWithLifecycle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nc"&gt;LaunchedEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NavigateDetail&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="nf"&gt;onNavigateDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;HomeScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;onEvent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;onEvent&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 Only place where:&lt;/p&gt;

&lt;p&gt;Flow is collected&lt;/p&gt;

&lt;p&gt;Navigation happens&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🎨 Screen – Pure UI&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;HomeScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;LazyColumn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clickable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ItemClicked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ Stateless&lt;br&gt;
✔ Previewable&lt;br&gt;
✔ Reusable&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;🧬 Dependency Injection – Koin&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Why Koin?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No code generation&lt;/li&gt;
&lt;li&gt;No annotation processing&lt;/li&gt;
&lt;li&gt;Fast build time&lt;/li&gt;
&lt;li&gt;Compose-friendly&lt;/li&gt;
&lt;li&gt;Easy to reason about in large teams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DI should not be the hardest part of your architecture.&lt;/p&gt;



&lt;p&gt;*&lt;em&gt;App Module&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;appModule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;module&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Network&lt;/span&gt;
    &lt;span class="nf"&gt;single&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;provideHttpClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;single&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;ApiService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Database&lt;/span&gt;
    &lt;span class="nf"&gt;single&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;AppDatabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;androidContext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;single&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AppDatabase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;itemDao&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Repository&lt;/span&gt;
    &lt;span class="n"&gt;single&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ItemRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ItemRepositoryImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;dao&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Feature Module&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;homeModule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;module&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;viewModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;HomeViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 ViewModels depend on interfaces only&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🌐 Networking – Ktor&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HttpClient Setup&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;provideHttpClient&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nc"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Android&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nf"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ContentNegotiation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Json&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;ignoreUnknownKeys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
                &lt;span class="n"&gt;explicitNulls&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nf"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BODY&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nf"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpTimeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;requestTimeoutMillis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15_000&lt;/span&gt;
            &lt;span class="n"&gt;connectTimeoutMillis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15_000&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nf"&gt;defaultRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 Configure everything in one place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ApiService&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApiService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/items"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;🗄 Local Storage – Room&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Entity&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;ItemEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nd"&gt;@PrimaryKey&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;DAO&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Dao&lt;/span&gt;
&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ItemDao&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT * FROM items"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getAll&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ItemEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nd"&gt;@Insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onConflict&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OnConflictStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;insertAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ItemEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Database&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;entities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;ItemEntity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppDatabase&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RoomDatabase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;itemDao&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;ItemDao&lt;/span&gt;

    &lt;span class="k"&gt;companion&lt;/span&gt; &lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;AppDatabase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="nc"&gt;Room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;databaseBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nc"&gt;AppDatabase&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"app.db"&lt;/span&gt;
            &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;🔁 Repository (Single Source of Truth)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ItemRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemRepositoryImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ApiService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;dao&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ItemDao&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ItemRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;remote&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;dao&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;ItemEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 ViewModel never knows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remote or local source&lt;/li&gt;
&lt;li&gt;Caching logic&lt;/li&gt;
&lt;li&gt;Mapping rules&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;⚡ Performance Guidelines&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DO&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use @Immutable for state &amp;amp; screen&lt;/li&gt;
&lt;li&gt;Use derivedStateOf for UI-only logic&lt;/li&gt;
&lt;li&gt;Key every LazyColumn item &lt;/li&gt;
&lt;li&gt;Collect Flow only in Route&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;DON'T&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create objects in Composable body&lt;/li&gt;
&lt;li&gt;Collect Flow inside list items&lt;/li&gt;
&lt;li&gt;Put ViewModel in Screen&lt;/li&gt;
&lt;li&gt;Navigate from ViewModel&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;🧪 Testing Example&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;click_item_emit_navigation_effect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runTest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;vm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HomeViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fakeRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ItemClicked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NavigateDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;🏁 Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This stack provides:&lt;/p&gt;

&lt;p&gt;✅ Predictable state&lt;br&gt;
✅ Navigation without magic&lt;br&gt;
✅ Excellent testability&lt;br&gt;
✅ High performance&lt;br&gt;
✅ Scales well for large team&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Jetpack Compose + Navigation 3 + MVI is the future-ready Android UI architecture.&lt;/code&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>android</category>
      <category>ui</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>Jetpack Compose Performance – System Trace, Recomposer, and the Truth About Frames</title>
      <dc:creator>ViO Tech</dc:creator>
      <pubDate>Wed, 17 Dec 2025 19:26:57 +0000</pubDate>
      <link>https://dev.to/vio_di_code/jetpack-compose-performance-system-trace-recomposer-and-the-truth-about-frames-4b5f</link>
      <guid>https://dev.to/vio_di_code/jetpack-compose-performance-system-trace-recomposer-and-the-truth-about-frames-4b5f</guid>
      <description>&lt;h1&gt;
  
  
  Jetpack Compose Performance – System Trace, Recomposer, and the Truth About Frames
&lt;/h1&gt;

&lt;p&gt;Jetpack Compose did not eliminate performance problems.&lt;/p&gt;

&lt;p&gt;It &lt;strong&gt;changed their shape&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Apps no longer suffer from deep view hierarchies,&lt;br&gt;&lt;br&gt;
but they now suffer from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;uncontrolled recomposition,&lt;/li&gt;
&lt;li&gt;expensive state propagation,&lt;/li&gt;
&lt;li&gt;missed frame deadlines hidden behind “low CPU usage”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article explains how &lt;strong&gt;Compose performance truly behaves at runtime&lt;/strong&gt;, using System Trace as the source of truth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frames Are the Only Currency Users Care About
&lt;/h2&gt;

&lt;p&gt;Users do not experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recompositions,&lt;/li&gt;
&lt;li&gt;allocations,&lt;/li&gt;
&lt;li&gt;CPU percentages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They experience &lt;strong&gt;frames&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every frame has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a deadline (~16.6 ms at 60 Hz),&lt;/li&gt;
&lt;li&gt;a strict schedule,&lt;/li&gt;
&lt;li&gt;zero tolerance for excuses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Miss the deadline, and users feel it immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Rendering Pipeline (Compose Edition)
&lt;/h2&gt;

&lt;p&gt;Before profiling Compose, you must understand the pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;VSYNC arrives
&lt;/li&gt;
&lt;li&gt;Choreographer schedules a frame
&lt;/li&gt;
&lt;li&gt;Compose recomposes (if needed)
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;applyChanges&lt;/code&gt; updates the UI tree
&lt;/li&gt;
&lt;li&gt;RenderThread submits GPU commands
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Profiler data only makes sense &lt;strong&gt;inside this sequence&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  System Trace: The Only Place Where Frames Exist
&lt;/h2&gt;

&lt;p&gt;CPU Profiler tells you &lt;em&gt;what ran&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;System Trace tells you &lt;em&gt;when frames were missed&lt;/em&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  📸 Figure 8 – System Trace: Compose Frame Lifecycle
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;(Insert image here)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" alt="PLACEHOLDER – Compose Frame Lifecycle" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Android System Trace showing the full Compose frame lifecycle from VSYNC through recomposition and rendering.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annotations (number these on the image):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;VSYNC – frame start signal
&lt;/li&gt;
&lt;li&gt;Choreographer#doFrame – scheduling phase
&lt;/li&gt;
&lt;li&gt;Recomposer.runRecomposeAndApplyChanges
&lt;/li&gt;
&lt;li&gt;applyChanges duration
&lt;/li&gt;
&lt;li&gt;RenderThread execution
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Critical insight:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If &lt;code&gt;applyChanges&lt;/code&gt; misses the deadline, no optimization elsewhere matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recomposer: Friend, Not Foe
&lt;/h2&gt;

&lt;p&gt;Recomposition is cheap by design.&lt;/p&gt;

&lt;p&gt;What hurts performance is &lt;strong&gt;what you do during recomposition&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  📸 Figure 9 – Recomposer Activity in System Trace
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;(Insert image here)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" alt="PLACEHOLDER – Recomposer Activity" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;System Trace highlighting Recomposer activity and applyChanges duration impacting frame rendering.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annotations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Recomposer scheduling
&lt;/li&gt;
&lt;li&gt;Long applyChanges blocks
&lt;/li&gt;
&lt;li&gt;Back-to-back recompositions
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Senior insight:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Frequent recomposition is fine.&lt;br&gt;&lt;br&gt;
Expensive recomposition is fatal.&lt;/p&gt;




&lt;h2&gt;
  
  
  SnapshotStateObserver: The Messenger You Blame
&lt;/h2&gt;

&lt;p&gt;When Compose state is read, SnapshotStateObserver tracks it.&lt;/p&gt;

&lt;p&gt;When performance is bad, it shows up everywhere.&lt;/p&gt;

&lt;p&gt;That does not make it the villain.&lt;/p&gt;




&lt;h3&gt;
  
  
  📸 Figure 10 – SnapshotStateObserver Hotspots
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;(Insert image here)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" alt="PLACEHOLDER – SnapshotStateObserver Flame Chart" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;CPU flame chart showing SnapshotStateObserver overhead caused by inefficient state reads in Jetpack Compose.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annotations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SnapshotStateObserver dominating CPU time
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;recordReadOf&lt;/code&gt; in hot paths
&lt;/li&gt;
&lt;li&gt;State read at the wrong composable scope
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
One state change invalidates too much UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Skipped Frames: When Users Feel Jank Before Logs Do
&lt;/h2&gt;

&lt;p&gt;Profiler only flags &lt;strong&gt;severe jank&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Users feel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;uneven pacing,&lt;/li&gt;
&lt;li&gt;subtle stutters,&lt;/li&gt;
&lt;li&gt;animation inconsistency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These appear as &lt;strong&gt;VSYNC gaps&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  📸 Figure 11 – Skipped Frames via VSYNC Gaps
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;(Insert image here)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" alt="PLACEHOLDER – VSYNC Gaps" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;System Trace revealing skipped frames through large gaps between VSYNC signals during animations.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annotations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Large VSYNC gaps
&lt;/li&gt;
&lt;li&gt;Frame duration exceeding deadline
&lt;/li&gt;
&lt;li&gt;Idle RenderThread waiting for Main thread
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Reality check:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Smoothness is statistical, not binary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why “Low CPU Usage” Means Nothing in Compose
&lt;/h2&gt;

&lt;p&gt;Compose work often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;runs in short bursts,&lt;/li&gt;
&lt;li&gt;blocks at terrible moments,&lt;/li&gt;
&lt;li&gt;hides behind averages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 4 ms stall at the wrong time breaks a frame.&lt;/p&gt;

&lt;p&gt;CPU charts will not scream.&lt;/p&gt;

&lt;p&gt;System Trace will.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pre-Release Compose Performance Checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping a Compose-heavy screen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recomposition is scoped and intentional
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;applyChanges&lt;/code&gt; consistently finishes within frame budget
&lt;/li&gt;
&lt;li&gt;No large VSYNC gaps during scroll or animation
&lt;/li&gt;
&lt;li&gt;SnapshotStateObserver does not dominate hot paths
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these fail, users will notice.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Part Ties Together
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Part 1 explained &lt;strong&gt;threads and deadlines&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Part 2 explained &lt;strong&gt;memory pressure&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;This part shows &lt;strong&gt;how everything collapses into frames&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Frames are where all performance debts are paid.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Junior developers ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Why is Compose recomposing so much?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Senior developers ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Which frame missed its deadline, and why?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;System Trace already knows.&lt;/p&gt;

&lt;p&gt;You just need to listen.&lt;/p&gt;




&lt;h3&gt;
  
  
  Series Navigation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Part 1 – CPU &amp;amp; Thread Reality
&lt;/li&gt;
&lt;li&gt;Part 2 – Memory, GC, and Leaks
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3 – Compose, System Trace, and Frames&lt;/strong&gt; ← you are here
&lt;/li&gt;
&lt;li&gt;Part 4 – Common Profiler Misreads&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>performance</category>
      <category>profiler</category>
      <category>jetpackcompose</category>
    </item>
    <item>
      <title>Android Profiler – Memory, GC, and Leaks That Slowly Kill Performance</title>
      <dc:creator>ViO Tech</dc:creator>
      <pubDate>Wed, 17 Dec 2025 19:20:53 +0000</pubDate>
      <link>https://dev.to/vio_di_code/android-profiler-memory-gc-and-leaks-that-slowly-kill-performance-5966</link>
      <guid>https://dev.to/vio_di_code/android-profiler-memory-gc-and-leaks-that-slowly-kill-performance-5966</guid>
      <description>&lt;h1&gt;
  
  
  Android Profiler – Memory, GC, and Leaks That Slowly Kill Performance
&lt;/h1&gt;

&lt;p&gt;Memory problems rarely crash your app.&lt;/p&gt;

&lt;p&gt;They &lt;strong&gt;slow it down quietly&lt;/strong&gt;, then surface as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;jank after minutes of usage,&lt;/li&gt;
&lt;li&gt;ANRs that only happen on older devices,&lt;/li&gt;
&lt;li&gt;“works fine after restart” reports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article explains &lt;strong&gt;how memory behavior becomes a performance bug&lt;/strong&gt;—long before a crash ever happens.&lt;/p&gt;




&lt;h2&gt;
  
  
  GC Is Not Cleanup. It Is Damage Control.
&lt;/h2&gt;

&lt;p&gt;Garbage Collection exists to &lt;strong&gt;prevent failure&lt;/strong&gt;, not to guarantee performance.&lt;/p&gt;

&lt;p&gt;If GC runs frequently, it means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your app allocates too much,&lt;/li&gt;
&lt;li&gt;the runtime is constantly recovering,&lt;/li&gt;
&lt;li&gt;CPU time is stolen from frame rendering.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GC is the symptom. Allocation is the disease.&lt;/p&gt;




&lt;h2&gt;
  
  
  Memory Profiler Timeline: Trends Over Time Matter Most
&lt;/h2&gt;

&lt;p&gt;One memory spike means nothing.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;memory trend&lt;/strong&gt; tells the truth.&lt;/p&gt;




&lt;h3&gt;
  
  
  📸 Figure 5 – Memory Profiler Timeline
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;(Insert image here)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" alt="PLACEHOLDER – Memory Profiler Timeline" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Android Memory Profiler timeline showing heap usage and garbage collection events to identify long-term memory growth.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annotations (number these on the image):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Heap size growth over time
&lt;/li&gt;
&lt;li&gt;Garbage collection events
&lt;/li&gt;
&lt;li&gt;Memory failing to return to baseline
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Senior insight:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If memory never stabilizes after GC, you do not have a spike.&lt;br&gt;&lt;br&gt;
You have a leak—or sustained allocation pressure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Allocation Tracking: Where Performance Quietly Dies
&lt;/h2&gt;

&lt;p&gt;Allocations feel harmless.&lt;/p&gt;

&lt;p&gt;Until they happen inside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scrolling,&lt;/li&gt;
&lt;li&gt;animations,&lt;/li&gt;
&lt;li&gt;repeated UI updates.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  📸 Figure 6 – Allocation Tracking by Class
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;(Insert image here)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" alt="PLACEHOLDER – Allocation Tracking" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Memory allocation tracking grouped by class to reveal excessive object creation during UI interactions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annotations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Frequently allocated classes
&lt;/li&gt;
&lt;li&gt;Allocations triggered by scroll or animation
&lt;/li&gt;
&lt;li&gt;Short-lived objects creating GC pressure
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Reality check:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Small objects allocated frequently are worse than large objects allocated rarely.&lt;/p&gt;




&lt;h2&gt;
  
  
  GC and UI Jank: The Hidden Relationship
&lt;/h2&gt;

&lt;p&gt;GC pauses are usually short.&lt;/p&gt;

&lt;p&gt;But they:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;interrupt Main thread work,&lt;/li&gt;
&lt;li&gt;shift frame timing,&lt;/li&gt;
&lt;li&gt;accumulate into visible jank.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single GC pause might not hurt.&lt;br&gt;&lt;br&gt;
Repeated pauses always do.&lt;/p&gt;




&lt;h2&gt;
  
  
  Heap Dumps: When You Suspect a Leak, Prove It
&lt;/h2&gt;

&lt;p&gt;Memory Profiler timelines suggest problems.&lt;/p&gt;

&lt;p&gt;Heap dumps &lt;strong&gt;confirm them&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  📸 Figure 7 – Heap Dump Dominator Tree
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;(Insert image here)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" alt="PLACEHOLDER – Heap Dump Dominator Tree" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Heap dump dominator tree highlighting retained objects and reference chains responsible for memory leaks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annotations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Activity instances still retained after destruction
&lt;/li&gt;
&lt;li&gt;Retained size dominating heap
&lt;/li&gt;
&lt;li&gt;Reference chain through singleton or static field
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Senior rule:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Shallow size explains object weight.&lt;br&gt;&lt;br&gt;
Retained size explains damage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Classic Leak Patterns That Survive Code Reviews
&lt;/h2&gt;

&lt;p&gt;Leaks often look reasonable in isolation.&lt;/p&gt;

&lt;p&gt;Common culprits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Activities held by singletons,&lt;/li&gt;
&lt;li&gt;listeners never unregistered,&lt;/li&gt;
&lt;li&gt;callbacks capturing Context,&lt;/li&gt;
&lt;li&gt;caches with no eviction strategy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The leak is rarely where memory grows.&lt;br&gt;&lt;br&gt;
It is where references never die.&lt;/p&gt;




&lt;h2&gt;
  
  
  Memory Pressure Is a CPU Problem in Disguise
&lt;/h2&gt;

&lt;p&gt;As memory pressure increases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GC frequency increases,&lt;/li&gt;
&lt;li&gt;CPU time is stolen,&lt;/li&gt;
&lt;li&gt;frame deadlines are missed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why memory bugs often show up as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The app feels slow after a while.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is not imagination.&lt;br&gt;&lt;br&gt;
It is physics.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why “It Gets Fixed After Restart” Is a Red Flag
&lt;/h2&gt;

&lt;p&gt;Restarting the app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clears the heap,&lt;/li&gt;
&lt;li&gt;resets allocation history,&lt;/li&gt;
&lt;li&gt;hides the real bug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If performance only recovers after restart,&lt;br&gt;&lt;br&gt;
you have not fixed the problem.&lt;/p&gt;

&lt;p&gt;You have reset it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Part Intentionally Did Not Cover
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Thread scheduling
&lt;/li&gt;
&lt;li&gt;System Trace
&lt;/li&gt;
&lt;li&gt;Jetpack Compose recomposition
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those connect memory behavior to frame rendering.&lt;br&gt;&lt;br&gt;
They come next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Junior developers ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Why is memory high?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Senior developers ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Why does memory never come back down?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Android Profiler already knows the answer.&lt;/p&gt;

&lt;p&gt;You just need to read it over time.&lt;/p&gt;




&lt;h3&gt;
  
  
  Series Navigation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Part 1 – CPU &amp;amp; Thread Reality
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 2 – Memory, GC, and Leaks&lt;/strong&gt; ← you are here
&lt;/li&gt;
&lt;li&gt;Part 3 – Compose, System Trace, and Frames
&lt;/li&gt;
&lt;li&gt;Part 4 – Common Profiler Misreads&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>performance</category>
      <category>profiler</category>
      <category>memory</category>
    </item>
    <item>
      <title>Android Profiler – CPU &amp; Thread Reality Every Senior Developer Learns the Hard Way</title>
      <dc:creator>ViO Tech</dc:creator>
      <pubDate>Wed, 17 Dec 2025 19:17:54 +0000</pubDate>
      <link>https://dev.to/vio_di_code/android-profiler-cpu-thread-reality-every-senior-developer-learns-the-hard-way-17fp</link>
      <guid>https://dev.to/vio_di_code/android-profiler-cpu-thread-reality-every-senior-developer-learns-the-hard-way-17fp</guid>
      <description>&lt;h1&gt;
  
  
  Android Profiler – CPU &amp;amp; Thread Reality Every Senior Developer Learns the Hard Way
&lt;/h1&gt;

&lt;p&gt;Most Android performance bugs are not memory problems.&lt;/p&gt;

&lt;p&gt;They are &lt;strong&gt;CPU and thread problems wearing a memory costume&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article focuses on one thing only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Why the UI feels slow, even when “nothing heavy” seems to be running.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No leaks.&lt;br&gt;&lt;br&gt;
No Compose deep dive yet.&lt;br&gt;&lt;br&gt;
Just &lt;strong&gt;threads, CPU time, and frame deadlines&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why CPU Problems Are Subtle (and Dangerous)
&lt;/h2&gt;

&lt;p&gt;CPU performance issues rarely look dramatic.&lt;/p&gt;

&lt;p&gt;They appear as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;occasional stutter,&lt;/li&gt;
&lt;li&gt;“only on some devices” lag,&lt;/li&gt;
&lt;li&gt;ANRs that QA never reproduces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason is simple:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;UI performance lives at the scale of frames, not averages.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Android Profiler Overview: Read the Timeline First
&lt;/h2&gt;

&lt;p&gt;Before diving into any chart, you must learn to read the timeline as a story.&lt;/p&gt;

&lt;h3&gt;
  
  
  📸 Figure 1 – Android Studio Profiler Overview
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;(Insert image here)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" alt="PLACEHOLDER – Android Studio Profiler Overview" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Android Studio Profiler overview showing real-time CPU, Memory, Network, and Energy timelines used to correlate performance with user actions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annotations (number these on the image):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Global timeline – select the suspicious time window
&lt;/li&gt;
&lt;li&gt;CPU usage track – spikes often align with UI jank
&lt;/li&gt;
&lt;li&gt;Memory track – useful for correlation, not conclusions
&lt;/li&gt;
&lt;li&gt;Event markers – anchor charts to user interactions
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Profiler data without a user action context is meaningless.&lt;/p&gt;




&lt;h2&gt;
  
  
  CPU Profiler: Sampled vs Instrumented Is a Strategy Choice
&lt;/h2&gt;

&lt;p&gt;Many developers treat CPU profiler modes as “accuracy levels”.&lt;/p&gt;

&lt;p&gt;They are not.&lt;/p&gt;

&lt;p&gt;They are &lt;strong&gt;different tools for different questions&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  📸 Figure 2 – Sampled CPU Flame Chart
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;(Insert image here)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" alt="PLACEHOLDER – Sampled CPU Flame Chart" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sampled CPU flame chart visualizing method execution time and call stack hierarchy to identify performance bottlenecks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annotations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Horizontal axis – wall-clock execution time
&lt;/li&gt;
&lt;li&gt;Vertical axis – call stack depth
&lt;/li&gt;
&lt;li&gt;Wide blocks on Main thread – UI lag suspects
&lt;/li&gt;
&lt;li&gt;Background threads – usually harmless unless synchronized
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Senior rule:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Optimize &lt;strong&gt;wide blocks&lt;/strong&gt;, not tall stacks.&lt;/p&gt;




&lt;h3&gt;
  
  
  📸 Figure 3 – Instrumented CPU Trace
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;(Insert image here)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" alt="PLACEHOLDER – Instrumented CPU Trace" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instrumented CPU trace showing precise method entry and exit timing for short, focused investigations.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annotations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Precise method boundaries
&lt;/li&gt;
&lt;li&gt;High tracing overhead
&lt;/li&gt;
&lt;li&gt;Best used on short interactions only
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Reality check:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Instrumented tracing answers &lt;em&gt;why&lt;/em&gt;, not &lt;em&gt;how often&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Main Thread Myth: “Nothing Heavy Is Running”
&lt;/h2&gt;

&lt;p&gt;This sentence has killed more frame rates than any single bug.&lt;/p&gt;

&lt;p&gt;The Main thread rarely dies from one big task.&lt;/p&gt;

&lt;p&gt;It dies from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;many small allocations,&lt;/li&gt;
&lt;li&gt;repeated view invalidations,&lt;/li&gt;
&lt;li&gt;logging in hot paths,&lt;/li&gt;
&lt;li&gt;synchronous callbacks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one is cheap.&lt;br&gt;&lt;br&gt;
Together, they miss frame deadlines.&lt;/p&gt;




&lt;h2&gt;
  
  
  System Trace: Where the Truth Actually Lives
&lt;/h2&gt;

&lt;p&gt;CPU charts tell you &lt;em&gt;who&lt;/em&gt; is busy.&lt;/p&gt;

&lt;p&gt;System Trace tells you &lt;em&gt;when it matters&lt;/em&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  📸 Figure 4 – System Trace: Main Thread, RenderThread, VSYNC
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;(Insert image here)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/IMAGE_URL_HERE" alt="PLACEHOLDER – System Trace Threads" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Android System Trace visualizing Main thread, RenderThread, and VSYNC signals to diagnose frame drops and UI jank.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annotations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;VSYNC – frame heartbeat (16.6 ms)
&lt;/li&gt;
&lt;li&gt;Choreographer#doFrame – frame scheduling starts
&lt;/li&gt;
&lt;li&gt;Main thread – layout &amp;amp; draw preparation
&lt;/li&gt;
&lt;li&gt;RenderThread – GPU command submission
&lt;/li&gt;
&lt;li&gt;Large VSYNC gaps – skipped frames
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Critical insight:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
GPUs are rarely the first problem.&lt;br&gt;&lt;br&gt;
The CPU usually fails to feed frames on time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frame Deadlines: The Only Metric Users Feel
&lt;/h2&gt;

&lt;p&gt;Users do not perceive CPU percentage.&lt;/p&gt;

&lt;p&gt;They perceive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missed frames,&lt;/li&gt;
&lt;li&gt;uneven pacing,&lt;/li&gt;
&lt;li&gt;delayed input response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single 20 ms stall is invisible on a CPU chart&lt;br&gt;&lt;br&gt;
and painfully obvious to a human hand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Early Warning Signs of CPU Trouble
&lt;/h2&gt;

&lt;p&gt;Before jank becomes obvious, you will often see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Main thread slices creeping closer to VSYNC boundaries,&lt;/li&gt;
&lt;li&gt;RenderThread idle while Main is late,&lt;/li&gt;
&lt;li&gt;CPU usage that looks “fine” but poorly timed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the moments seniors fix problems—&lt;br&gt;&lt;br&gt;
before users start complaining.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Part Intentionally Did Not Cover
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Memory leaks
&lt;/li&gt;
&lt;li&gt;Garbage collection
&lt;/li&gt;
&lt;li&gt;Jetpack Compose internals
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those deserve their own mental models.&lt;br&gt;&lt;br&gt;
They come next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Junior developers ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Why is CPU usage high?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Senior developers ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Which thread missed the frame deadline?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Android Profiler already knows the answer.&lt;/p&gt;

&lt;p&gt;You just need to ask the right question.&lt;/p&gt;




&lt;h3&gt;
  
  
  Series Navigation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 1 – CPU &amp;amp; Thread Reality&lt;/strong&gt; ← you are here
&lt;/li&gt;
&lt;li&gt;Part 2 – Memory, GC, and Leaks
&lt;/li&gt;
&lt;li&gt;Part 3 – Compose, System Trace, and Frames
&lt;/li&gt;
&lt;li&gt;Part 4 – Common Profiler Misreads&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>performance</category>
      <category>profiler</category>
      <category>cpu</category>
    </item>
    <item>
      <title>Android Profiler – Common Misreads That Lead to False Optimizations</title>
      <dc:creator>ViO Tech</dc:creator>
      <pubDate>Wed, 17 Dec 2025 19:15:33 +0000</pubDate>
      <link>https://dev.to/vio_di_code/android-profiler-common-misreads-that-lead-to-false-optimizations-ob1</link>
      <guid>https://dev.to/vio_di_code/android-profiler-common-misreads-that-lead-to-false-optimizations-ob1</guid>
      <description>&lt;h1&gt;
  
  
  Android Profiler – Common Misreads That Lead to False Optimizations
&lt;/h1&gt;

&lt;p&gt;Android Profiler does not lie.&lt;/p&gt;

&lt;p&gt;But developers misread it constantly.&lt;/p&gt;

&lt;p&gt;This article focuses on the most common profiler misinterpretations that lead to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wasted optimization effort,&lt;/li&gt;
&lt;li&gt;placebo performance fixes,&lt;/li&gt;
&lt;li&gt;bugs that disappear in debug builds and return in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Parts 1–3 explained &lt;em&gt;how to read profiler data&lt;/em&gt;, this part explains &lt;strong&gt;how developers fool themselves while doing it&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Misread #1 – “CPU usage is low, so performance must be fine”
&lt;/h2&gt;

&lt;p&gt;Low CPU usage is often a warning sign, not reassurance.&lt;/p&gt;

&lt;p&gt;If the UI is janky while CPU usage looks calm, it usually means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;work is split into small bursts,&lt;/li&gt;
&lt;li&gt;those bursts still block frame deadlines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CPU charts average over time. Frames do not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correct mental model&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI performance lives at the 16.6 ms frame boundary.&lt;/li&gt;
&lt;li&gt;A short stall at the wrong moment is enough to cause jank.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Misread #2 – “GC happened, so memory is under control”
&lt;/h2&gt;

&lt;p&gt;Garbage Collection is not cleanup.&lt;/p&gt;

&lt;p&gt;It is damage control.&lt;/p&gt;

&lt;p&gt;Frequent GC means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;excessive allocation,&lt;/li&gt;
&lt;li&gt;increased CPU pressure,&lt;/li&gt;
&lt;li&gt;higher chance of dropped frames.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Red flag&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory oscillates but never returns to baseline.&lt;/li&gt;
&lt;li&gt;GC frequency increases during scroll or animation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix allocation sources, not GC behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  Misread #3 – “This method is called a lot, so it must be slow”
&lt;/h2&gt;

&lt;p&gt;Call count is seductive. Time is truth.&lt;/p&gt;

&lt;p&gt;A method called 5,000 times at 0.01 ms is irrelevant.&lt;br&gt;
A method called once at 12 ms kills a frame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Profiler rule&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Optimize by wall-clock time, not invocation count.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Wide blocks matter more than tall stacks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Misread #4 – “Nothing heavy runs on the Main thread”
&lt;/h2&gt;

&lt;p&gt;Nothing &lt;em&gt;big&lt;/em&gt; runs.&lt;/p&gt;

&lt;p&gt;That does not mean nothing &lt;em&gt;harmful&lt;/em&gt; runs.&lt;/p&gt;

&lt;p&gt;Main thread death usually comes from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;many small allocations,&lt;/li&gt;
&lt;li&gt;repeated state reads,&lt;/li&gt;
&lt;li&gt;layout thrashing,&lt;/li&gt;
&lt;li&gt;logging inside hot paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each operation is cheap.&lt;br&gt;
Together, they miss deadlines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Misread #5 – “Compose recomposes too much, so recomposition is the problem”
&lt;/h2&gt;

&lt;p&gt;Recomposition is not the enemy.&lt;/p&gt;

&lt;p&gt;Expensive recomposition is.&lt;/p&gt;

&lt;p&gt;Common mistake:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;count recompositions,&lt;/li&gt;
&lt;li&gt;panic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Correct questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How long does &lt;code&gt;applyChanges&lt;/code&gt; take?&lt;/li&gt;
&lt;li&gt;What work happens during recomposition?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cheap recomposition is healthy.&lt;br&gt;
Heavy recomposition is poison.&lt;/p&gt;




&lt;h2&gt;
  
  
  Misread #6 – “SnapshotStateObserver dominates CPU, so Compose is slow”
&lt;/h2&gt;

&lt;p&gt;SnapshotStateObserver is a messenger.&lt;/p&gt;

&lt;p&gt;It appears when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;state is read,&lt;/li&gt;
&lt;li&gt;dependencies are tracked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real bug is usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;state read at the wrong composable scope,&lt;/li&gt;
&lt;li&gt;state shared too broadly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One state change invalidates half the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Misread #7 – “No skipped frames reported, so animations are smooth”
&lt;/h2&gt;

&lt;p&gt;Profiler reports severe jank by default.&lt;/p&gt;

&lt;p&gt;Users feel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;uneven pacing,&lt;/li&gt;
&lt;li&gt;micro-stutters,&lt;/li&gt;
&lt;li&gt;subtle animation inconsistency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Profiler can stay silent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior check&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VSYNC spacing consistency&lt;/li&gt;
&lt;li&gt;Frame durations flirting with deadlines&lt;/li&gt;
&lt;li&gt;RenderThread idle gaps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Smoothness is statistical, not binary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Misread #8 – “Benchmarks improved, so the issue is fixed”
&lt;/h2&gt;

&lt;p&gt;Benchmarks lie politely.&lt;/p&gt;

&lt;p&gt;Production lies creatively.&lt;/p&gt;

&lt;p&gt;Performance depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;thermal state,&lt;/li&gt;
&lt;li&gt;background services,&lt;/li&gt;
&lt;li&gt;memory pressure,&lt;/li&gt;
&lt;li&gt;real user behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If it wasn’t verified with Profiler after the fix, it isn’t fixed.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Misread #9 – “Energy Profiler doesn’t matter for my app”
&lt;/h2&gt;

&lt;p&gt;Energy issues manifest as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;thermal throttling,&lt;/li&gt;
&lt;li&gt;CPU frequency drops,&lt;/li&gt;
&lt;li&gt;delayed jank after minutes of use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ignoring energy means chasing ghosts later.&lt;/p&gt;

&lt;p&gt;Energy problems are performance bugs with a timer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Misread #10 – “Profiler will tell me what to optimize”
&lt;/h2&gt;

&lt;p&gt;Profiler shows where reality happens.&lt;/p&gt;

&lt;p&gt;It does not decide what matters.&lt;/p&gt;

&lt;p&gt;That requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;understanding frame deadlines,&lt;/li&gt;
&lt;li&gt;knowing rendering pipelines,&lt;/li&gt;
&lt;li&gt;recognizing allocation patterns,&lt;/li&gt;
&lt;li&gt;correlating data with user perception.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools amplify understanding.&lt;br&gt;
They do not replace it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Junior developers ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What should I optimize?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Senior developers ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What problem is the runtime actually experiencing?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Android Profiler answers honestly.&lt;/p&gt;

&lt;p&gt;Whether we read it honestly is the real skill.&lt;/p&gt;




&lt;h3&gt;
  
  
  Series Index
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Part 1 – CPU &amp;amp; Thread Reality
&lt;/li&gt;
&lt;li&gt;Part 2 – Memory, GC, and Leaks
&lt;/li&gt;
&lt;li&gt;Part 3 – Compose, System Trace, and Frames
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 4 – Common Profiler Misreads&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>performance</category>
      <category>profiler</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Android Performance: Những thứ thật sự làm app chậm (và cách xử lý thực tế)</title>
      <dc:creator>ViO Tech</dc:creator>
      <pubDate>Tue, 16 Dec 2025 15:45:27 +0000</pubDate>
      <link>https://dev.to/vio_di_code/android-performance-nhung-thu-that-su-lam-app-cham-va-cach-xu-ly-thuc-te-219k</link>
      <guid>https://dev.to/vio_di_code/android-performance-nhung-thu-that-su-lam-app-cham-va-cach-xu-ly-thuc-te-219k</guid>
      <description>&lt;h2&gt;
  
  
  1️⃣ Hiểu đúng về “Android Performance”
&lt;/h2&gt;

&lt;p&gt;Rất nhiều bài viết nói về performance theo kiểu:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tránh object allocation&lt;/li&gt;
&lt;li&gt;optimize code nhỏ lẻ&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;strong&gt;Nhưng trong app thật&lt;/strong&gt;, 80% vấn đề performance đến từ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;**Main Thread bị block&lt;/li&gt;
&lt;li&gt;Memory leak&lt;/li&gt;
&lt;li&gt;Startup quá nặng&lt;/li&gt;
&lt;li&gt;UI rendering kém hiệu quả**&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nếu không đo được → tối ưu chỉ là đoán.&lt;/p&gt;

&lt;h2&gt;
  
  
  2️⃣ Main Thread – nguyên nhân số 1 gây lag &amp;amp; ANR
&lt;/h2&gt;

&lt;p&gt;❌ &lt;strong&gt;Sai lầm phổ biến&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gọi API trên main thread&lt;/li&gt;
&lt;li&gt;Parse JSON nặng trong ViewModel&lt;/li&gt;
&lt;li&gt;Inflate layout phức tạp liên tục&lt;/li&gt;
&lt;li&gt;Xử lý Ads / SDK trong &lt;em&gt;onCreate&lt;/em&gt;
✅ &lt;strong&gt;Cách làm đúng (Kotlin)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;viewModelScope.launch(Dispatchers.IO) {
    val data = repository.loadData()
    withContext(Dispatchers.Main) {
        _uiState.value = data
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 &lt;strong&gt;Nguyên tắc:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Main thread = render UI&lt;/li&gt;
&lt;li&gt;Tất cả IO / computation → background&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3️⃣ Startup Time – lý do user thoát app sớm
&lt;/h2&gt;

&lt;p&gt;❌ &lt;strong&gt;Những thứ làm startup chậm&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Init SDK quá sớm (Ads, Analytics)&lt;/li&gt;
&lt;li&gt;Dùng ContentProvider không cần thiết&lt;/li&gt;
&lt;li&gt;Inflate layout phức tạp ngay màn đầu&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ &lt;strong&gt;Chiến lược tối ưu&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Defer initialization&lt;/li&gt;
&lt;li&gt;Lazy load SDK&lt;/li&gt;
&lt;li&gt;Dùng SplashScreen API đúng cách
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lifecycleScope.launch {
    delay(300)
    initAds()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 User thấy app mở nhanh hơn, dù logic vẫn load sau.&lt;/p&gt;

&lt;h2&gt;
  
  
  4️⃣ Memory Leak – app chạy lâu là sập
&lt;/h2&gt;

&lt;p&gt;❌ &lt;strong&gt;Leak hay gặp&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Context bị giữ trong singleton&lt;/li&gt;
&lt;li&gt;Callback không unregister&lt;/li&gt;
&lt;li&gt;Fragment reference trong Adapter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ &lt;strong&gt;Best practice&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Không giữ Activity context&lt;/li&gt;
&lt;li&gt;Dùng &lt;em&gt;WeakReference&lt;/em&gt; khi cần&lt;/li&gt;
&lt;li&gt;Clear reference trong &lt;em&gt;onDestroyView&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;override fun onDestroyView() {
    super.onDestroyView()
    binding = null
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 &lt;strong&gt;LeakCanary&lt;/strong&gt; là tool bắt buộc ở production build nội bộ.&lt;/p&gt;

&lt;h2&gt;
  
  
  5️⃣ UI Performance: XML vs Compose
&lt;/h2&gt;

&lt;p&gt;XML&lt;/p&gt;

&lt;p&gt;❌ Layout lồng quá sâu&lt;br&gt;
❌ RecyclerView không dùng DiffUtil&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Giải pháp:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ConstraintLayout&lt;/li&gt;
&lt;li&gt;Flatten hierarchy&lt;/li&gt;
&lt;li&gt;DiffUtil + ViewHolder chuẩn
Compose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;❌ Recomposition quá nhiều&lt;br&gt;
❌ State đặt sai chỗ&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Giải pháp:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State hoisting&lt;/li&gt;
&lt;li&gt;&lt;em&gt;rememberSaveable&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Tránh tạo object trong Composable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6️⃣ Ads &amp;amp; Performance – phần nhiều dev bỏ qua
&lt;/h2&gt;

&lt;p&gt;Ads là nguồn gây lag rất phổ biến, nhất là:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load Ads trên main thread&lt;/li&gt;
&lt;li&gt;Show Ads khi UI chưa ổn định&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ &lt;strong&gt;Best practice Ads&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preload Ads&lt;/li&gt;
&lt;li&gt;Không init Ads trong Application.onCreate&lt;/li&gt;
&lt;li&gt;Chỉ show Ads khi UI idle
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (ad.isLoaded &amp;amp;&amp;amp; lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
    ad.show(activity)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7️⃣ Đo lường – không đo thì đừng tối ưu
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tool nên dùng&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Android Profiler&lt;/li&gt;
&lt;li&gt;Layout Inspector&lt;/li&gt;
&lt;li&gt;Systrace / Perfetto&lt;/li&gt;
&lt;li&gt;Firebase Performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 Chỉ optimize khi thấy số liệu xấu&lt;/p&gt;

&lt;h2&gt;
  
  
  8️⃣ Checklist nhanh (dùng cho team)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Main thread không bị block&lt;/li&gt;
&lt;li&gt; Startup &amp;lt; 2s&lt;/li&gt;
&lt;li&gt; Không memory leak&lt;/li&gt;
&lt;li&gt; UI scroll mượt&lt;/li&gt;
&lt;li&gt; Ads không gây jank&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9️⃣ Kết luận
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Performance không phải là viết code “thông minh hơn”,&lt;br&gt;
mà là viết code có ý thức về runtime.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This article is inspired by various Android performance discussions and my own production experience.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>viodicode</category>
      <category>programming</category>
      <category>kotlin</category>
    </item>
  </channel>
</rss>
