<?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: orelzion</title>
    <description>The latest articles on DEV Community by orelzion (@orelzion).</description>
    <link>https://dev.to/orelzion</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F89973%2Fdc5b4f39-c9b5-4d04-9838-65e619c9d33d.png</url>
      <title>DEV Community: orelzion</title>
      <link>https://dev.to/orelzion</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/orelzion"/>
    <language>en</language>
    <item>
      <title>Phonebook: a Storybook-style preview gallery for SwiftUI and Compose, built for agents</title>
      <dc:creator>orelzion</dc:creator>
      <pubDate>Tue, 25 Aug 2026 17:26:17 +0000</pubDate>
      <link>https://dev.to/orelzion/phonebook-a-storybook-style-preview-gallery-for-swiftui-and-compose-built-for-agents-6g9</link>
      <guid>https://dev.to/orelzion/phonebook-a-storybook-style-preview-gallery-for-swiftui-and-compose-built-for-agents-6g9</guid>
      <description>&lt;p&gt;TL;DR I built &lt;a href="https://github.com/stag-build/phonebook" rel="noopener noreferrer"&gt;Phonebook&lt;/a&gt;, an open-source CLI and MCP server that turns existing Jetpack Compose &lt;code&gt;@Preview&lt;/code&gt;s and SwiftUI &lt;code&gt;#Preview&lt;/code&gt;s into a static, self-hosted HTML gallery.&lt;/p&gt;

&lt;p&gt;Think Storybook-style component catalog, but for native mobile previews you already have in your codebase.&lt;/p&gt;

&lt;p&gt;No SaaS account.&lt;br&gt;
No extra Storybook files to maintain.&lt;br&gt;
No asking designers to install Xcode, Android Studio, or your debug app just to review a loading state.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnx5auk26mvx6jjgjfrso.png" 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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnx5auk26mvx6jjgjfrso.png" alt="Phonebook gallery screenshot" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also see the &lt;a href="https://stag-build.github.io/phonebook/" rel="noopener noreferrer"&gt;live gallery&lt;/a&gt; or watch the &lt;a href="https://youtu.be/trHbUaG784w" rel="noopener noreferrer"&gt;promo video&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Ok, ok, let's start from the beginning.&lt;/p&gt;

&lt;p&gt;Mobile developers actually have pretty good tools for rendering UI in isolation.&lt;/p&gt;

&lt;p&gt;If you're building Android with Jetpack Compose, you probably already have something like this:&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;@Preview&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="s"&gt;"Button/Enabled"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&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;PrimaryButtonEnabledPreview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;PrimaryButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Continue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;isEnabled&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="n"&gt;onClick&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="nd"&gt;@Preview&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="s"&gt;"Button/Disabled"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&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;PrimaryButtonDisabledPreview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;PrimaryButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Continue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;isEnabled&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="n"&gt;onClick&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;And if you're building iOS with SwiftUI, you probably have something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="cp"&gt;#Preview("Button/Enabled", traits: .sizeThatFitsLayout) {&lt;/span&gt;
    &lt;span class="kt"&gt;PrimaryButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Continue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;isEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;action&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="cp"&gt;#Preview("Button/Disabled", traits: .sizeThatFitsLayout) {&lt;/span&gt;
    &lt;span class="kt"&gt;PrimaryButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Continue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;isEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;action&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 is great when you are the developer sitting inside the IDE.&lt;/p&gt;

&lt;p&gt;But then a designer asks:&lt;/p&gt;

&lt;p&gt;"Can I see all the button states?"&lt;/p&gt;

&lt;p&gt;Or a PM asks:&lt;/p&gt;

&lt;p&gt;"What does the empty state look like now?"&lt;/p&gt;

&lt;p&gt;Or QA asks:&lt;/p&gt;

&lt;p&gt;"Do we have a dark mode version of this?"&lt;/p&gt;

&lt;p&gt;And suddenly the preview is not enough.&lt;/p&gt;

&lt;p&gt;Because the preview is trapped inside a developer workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The awkward part
&lt;/h2&gt;

&lt;p&gt;On web teams, this problem has a very familiar answer: Storybook.&lt;/p&gt;

&lt;p&gt;You put components in stories, publish the Storybook, and now everyone has a place to see buttons, forms, empty states, loading states, weird edge cases, and all the other things that are annoying to reach through the real app.&lt;/p&gt;

&lt;p&gt;But native mobile is different.&lt;/p&gt;

&lt;p&gt;Brad Frost, who has written a lot about design systems, described the native tooling layer as still being immature and mentioned the recurring question: &lt;a href="https://bradfrost.com/blog/post/the-design-system-ecosystem/" rel="noopener noreferrer"&gt;"is there a Storybook for iOS and Android?"&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That question comes up because the pain is real, and you can see the same pattern in public write-ups from teams who already hit it.&lt;/p&gt;

&lt;p&gt;The team at rebuy wrote about building their own &lt;a href="https://medium.com/rebuy-recommerce/a-breeze-of-storybook-for-the-apps-366a45d9650e" rel="noopener noreferrer"&gt;"Storybook light" for apps&lt;/a&gt; after they couldn't find a suitable off-the-shelf tool. Doist wrote about building an &lt;a href="https://www.doist.dev/android_component_catalog/" rel="noopener noreferrer"&gt;automated Android component catalog&lt;/a&gt; because their design team had trouble knowing what reusable UI existed and how implementation compared to specs.&lt;/p&gt;

&lt;p&gt;Pavel Sharanda's &lt;a href="https://hackernoon.com/nativebook-unifying-the-native-ios-development-experience-with-storybookjs" rel="noopener noreferrer"&gt;NativeBook&lt;/a&gt; took another run at the same idea for iOS: catalog app, previews, snapshot tests, and HTML docs from the same snippets.&lt;/p&gt;

&lt;p&gt;So this is not a new problem.&lt;/p&gt;

&lt;p&gt;Actually, that is part of why I wanted to build Phonebook.&lt;/p&gt;

&lt;p&gt;When enough teams keep building their own internal version of the same thing, there is probably a missing tool hiding there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I wanted
&lt;/h2&gt;

&lt;p&gt;I didn't want to invent a new way to describe mobile UI.&lt;/p&gt;

&lt;p&gt;Developers already have previews.&lt;/p&gt;

&lt;p&gt;They are already close to the production component.&lt;br&gt;
They are already written in the same language as the app.&lt;br&gt;
They already know how to render disabled buttons, error states, empty states, loading states, dark mode, and all the other tiny pieces we keep pretending are "just UI".&lt;/p&gt;

&lt;p&gt;So my thought was:&lt;/p&gt;

&lt;p&gt;Why not harvest those?&lt;/p&gt;

&lt;p&gt;Not write a second Storybook-style codebase.&lt;br&gt;
Not maintain a separate sample app until everyone forgets about it.&lt;br&gt;
Not upload screenshots to some external service and hope the pricing still makes sense next year.&lt;/p&gt;

&lt;p&gt;Just take the previews already in the repo and turn them into a static website.&lt;/p&gt;

&lt;p&gt;That's Phonebook.&lt;/p&gt;
&lt;h2&gt;
  
  
  How Phonebook works
&lt;/h2&gt;

&lt;p&gt;Phonebook has two main commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;phonebook generate
phonebook build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;generate&lt;/code&gt; renders the native previews and writes a bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;phonebook-out/
  manifest.json
  images/
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;build&lt;/code&gt; takes that bundle and writes a static site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;phonebook-out/
  index.html
  manifest.json
  images/
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;Plain HTML, CSS, JavaScript, and image files.&lt;/p&gt;

&lt;p&gt;You can open it from &lt;code&gt;file://&lt;/code&gt;.&lt;br&gt;
You can upload it to GitHub Pages.&lt;br&gt;
You can attach it as a CI artifact.&lt;br&gt;
You can put it on S3, Vercel, Netlify, an internal static host, or whatever your company already uses.&lt;/p&gt;

&lt;p&gt;Phonebook does not care.&lt;/p&gt;

&lt;p&gt;And honestly, that is the point.&lt;/p&gt;
&lt;h2&gt;
  
  
  Android: Compose previews into a gallery
&lt;/h2&gt;

&lt;p&gt;On Android, Phonebook uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/takahirom/roborazzi" rel="noopener noreferrer"&gt;Roborazzi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sergio-sastre/ComposablePreviewScanner" rel="noopener noreferrer"&gt;ComposablePreviewScanner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Robolectric/JVM rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So it can run on Linux CI without an emulator.&lt;/p&gt;

&lt;p&gt;The rough setup looks like this:&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="c1"&gt;// app/build.gradle.kts&lt;/span&gt;
&lt;span class="nf"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.takahirom.roborazzi"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;roborazzi&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;generateComposePreviewRobolectricTests&lt;/span&gt; &lt;span class="p"&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;span class="n"&gt;packages&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="s"&gt;"com.example.app"&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="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;testImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.robolectric:robolectric:4.14.1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;testImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.takahirom.roborazzi:roborazzi:1.72.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;testImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.takahirom.roborazzi:roborazzi-compose:1.72.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;testImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.sergio-sastre.ComposablePreviewScanner:android:0.9.3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;testImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.takahirom.roborazzi:roborazzi-compose-preview-scanner-support:1.72.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;testImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx.compose.ui:ui-test-junit4"&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;Then add &lt;code&gt;phonebook.config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&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;"appName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My Android App"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"android"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"android"&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;"modules"&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="s2"&gt;":app"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"variant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"debug"&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;p&gt;And run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @stag-build/phonebook generate &lt;span class="nt"&gt;-C&lt;/span&gt; /path/to/your/android/repo
npx @stag-build/phonebook build &lt;span class="nt"&gt;-C&lt;/span&gt; /path/to/your/android/repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  iOS: SwiftUI previews into a gallery
&lt;/h2&gt;

&lt;p&gt;On iOS, Phonebook uses &lt;a href="https://github.com/getsentry/SnapshotPreviews" rel="noopener noreferrer"&gt;SnapshotPreviews&lt;/a&gt; through &lt;code&gt;xcodebuild test&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You add a small XCTest target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;SnapshottingTests&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;PhonebookSnapshotTests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;SnapshotTest&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;class&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;snapshotPreviews&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&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="kc"&gt;nil&lt;/span&gt; &lt;span class="c1"&gt;// record every #Preview&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;Then add &lt;code&gt;phonebook.config.json&lt;/code&gt; next to your &lt;code&gt;.xcodeproj&lt;/code&gt;:&lt;br&gt;
&lt;/p&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;"appName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My iOS App"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ios"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ios"&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;"project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyApp.xcodeproj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scheme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyApp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"simulator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iPhone 17 Pro"&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;p&gt;And run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @stag-build/phonebook generate &lt;span class="nt"&gt;-C&lt;/span&gt; /path/to/your/ios/repo
npx @stag-build/phonebook build &lt;span class="nt"&gt;-C&lt;/span&gt; /path/to/your/ios/repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;phonebook-out/index.html&lt;/code&gt;, and there's your gallery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naming trick
&lt;/h2&gt;

&lt;p&gt;Phonebook does not require a new annotation.&lt;/p&gt;

&lt;p&gt;It reads the preview names and turns them into &lt;code&gt;component / state&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So this:&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;@Preview&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="s"&gt;"UserCard/Loading"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&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;UserCardLoadingPreview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;UserCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isLoading&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="cp"&gt;#Preview("UserCard/Loading", traits: .sizeThatFitsLayout) {&lt;/span&gt;
    &lt;span class="kt"&gt;UserCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;Both become:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;component: &lt;code&gt;User Card&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;state: &lt;code&gt;Loading&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UserCard/Default
UserCard/Loading
UserCard/Error
UserCard/Dark
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Phonebook groups them together as one component card with multiple states.&lt;/p&gt;

&lt;p&gt;This sounds small.&lt;/p&gt;

&lt;p&gt;But in practice, this is where the value is.&lt;/p&gt;

&lt;p&gt;Because people usually don't ask, "Can you render one preview?"&lt;/p&gt;

&lt;p&gt;They ask, "Can I see all of it?"&lt;/p&gt;

&lt;p&gt;All the buttons.&lt;br&gt;
All the badges.&lt;br&gt;
All the cards.&lt;br&gt;
All the empty states.&lt;br&gt;
All the dark mode states.&lt;br&gt;
All the weird combinations we forget to check until the release is almost out.&lt;/p&gt;

&lt;p&gt;The grid is the product.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why not just use Storybook?
&lt;/h2&gt;

&lt;p&gt;If you are using React Native, Storybook already has native and web options. The &lt;a href="https://storybook.js.org/docs/get-started/frameworks/react-native-web-vite" rel="noopener noreferrer"&gt;React Native Storybook docs&lt;/a&gt; explain the tradeoff pretty clearly: native is higher fidelity because it runs in the actual app, while web is easier to publish and share.&lt;/p&gt;

&lt;p&gt;But Phonebook is aimed at a different workflow.&lt;/p&gt;

&lt;p&gt;It is for native Compose and SwiftUI codebases where the team already uses previews and wants a shareable artifact without creating a parallel story system.&lt;/p&gt;

&lt;p&gt;So instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;component code
preview code
storybook story code
catalog app code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;component code
preview code
generated gallery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Less ceremony.&lt;br&gt;
Less drift.&lt;br&gt;
Less "wait, does anyone still update this?"&lt;/p&gt;
&lt;h2&gt;
  
  
  Why static?
&lt;/h2&gt;

&lt;p&gt;Because static things are boring in the best way.&lt;/p&gt;

&lt;p&gt;They are easy to host.&lt;br&gt;
Easy to archive.&lt;br&gt;
Easy to attach to CI.&lt;br&gt;
Easy to send in Slack.&lt;br&gt;
Easy to open on a designer's machine.&lt;br&gt;
Easy to diff across builds if you want to get fancy later.&lt;/p&gt;

&lt;p&gt;There is no Phonebook cloud.&lt;br&gt;
There is no login.&lt;br&gt;
There is no organization setup.&lt;br&gt;
There is no "please ask your admin to approve this workspace integration" moment.&lt;/p&gt;

&lt;p&gt;You own the output.&lt;/p&gt;

&lt;p&gt;For a lot of mobile teams, this matters.&lt;/p&gt;

&lt;p&gt;Not because SaaS is bad.&lt;br&gt;
SaaS is great when you want a service.&lt;/p&gt;

&lt;p&gt;But sometimes all you need is an HTML folder with screenshots of your actual components.&lt;/p&gt;
&lt;h2&gt;
  
  
  The agent part
&lt;/h2&gt;

&lt;p&gt;This is the part I care about more than I expected.&lt;/p&gt;

&lt;p&gt;Phonebook is not only a CLI. It also ships an MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @stag-build/phonebook mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a coding agent can help with the workflow inside your actual mobile repo.&lt;/p&gt;

&lt;p&gt;For example, you can ask an agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use the phonebook MCP and create a catalog for my designer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the MCP server exposes tools for things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checking setup&lt;/li&gt;
&lt;li&gt;analyzing preview coverage&lt;/li&gt;
&lt;li&gt;getting preview guidance&lt;/li&gt;
&lt;li&gt;running generation&lt;/li&gt;
&lt;li&gt;building the static gallery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The MCP server does not need to be magical.&lt;/p&gt;

&lt;p&gt;Actually, I prefer that it isn't.&lt;/p&gt;

&lt;p&gt;It should tell the agent what exists, what is missing, what command to run, and what generated successfully. The coding agent can then make the repo changes: add missing previews, fix naming, add the config file, or wire CI.&lt;/p&gt;

&lt;p&gt;This feels like the right boundary to me.&lt;/p&gt;

&lt;p&gt;Phonebook knows Phonebook.&lt;br&gt;
The agent knows how to edit your repo.&lt;/p&gt;
&lt;h2&gt;
  
  
  What this looks like in CI
&lt;/h2&gt;

&lt;p&gt;For Android, a GitHub Actions job can run on Ubuntu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;phonebook-android&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gallery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temurin&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gradle/actions/setup-gradle@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx @stag-build/phonebook generate -C .&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx @stag-build/phonebook build -C .&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;phonebook-site&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;phonebook-out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For iOS, use a macOS runner because SnapshotPreviews needs a simulator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;phonebook-ios&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gallery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-15&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Select Xcode&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sudo xcode-select -s /Applications/Xcode.app&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx @stag-build/phonebook generate -C .&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx @stag-build/phonebook build -C .&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;phonebook-site&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;phonebook-out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can publish the same static folder to GitHub Pages or any internal host.&lt;/p&gt;

&lt;p&gt;Phonebook does not publish anything by itself. It just gives you the artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I don't want Phonebook to become
&lt;/h2&gt;

&lt;p&gt;I don't want this to become another place where teams have to describe the same UI again.&lt;/p&gt;

&lt;p&gt;That is how tools rot.&lt;/p&gt;

&lt;p&gt;Someone adds a component.&lt;br&gt;
Someone forgets to add the story.&lt;br&gt;
The design system site looks complete, but the app has moved on.&lt;br&gt;
Then everyone stops trusting it.&lt;/p&gt;

&lt;p&gt;And once people stop trusting the catalog, it becomes a museum.&lt;/p&gt;

&lt;p&gt;Very professional.&lt;br&gt;
Very nicely organized.&lt;br&gt;
Completely useless.&lt;/p&gt;

&lt;p&gt;Phonebook's bet is that previews are already close enough to the source of truth that harvesting them is better than asking teams to maintain a second catalog manually.&lt;/p&gt;

&lt;p&gt;Will this solve every design-system problem?&lt;/p&gt;

&lt;p&gt;Of course not.&lt;/p&gt;

&lt;p&gt;It will not replace Figma.&lt;br&gt;
It will not tell you whether the component API is good.&lt;br&gt;
It will not magically clean up years of duplicated buttons.&lt;br&gt;
It will not make naming conventions appear by themselves. Sadly.&lt;/p&gt;

&lt;p&gt;But it gives you a real, shareable, desktop-friendly view of the mobile UI that already exists.&lt;/p&gt;

&lt;p&gt;And that is a surprisingly big step.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @stag-build/phonebook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or run it without installing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @stag-build/phonebook generate &lt;span class="nt"&gt;-C&lt;/span&gt; /path/to/your/repo
npx @stag-build/phonebook build &lt;span class="nt"&gt;-C&lt;/span&gt; /path/to/your/repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or add it to your agent as an MCP server:&lt;br&gt;
&lt;/p&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;"mcpServers"&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;"phonebook"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@stag-build/phonebook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp"&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/stag-build/phonebook" rel="noopener noreferrer"&gt;https://github.com/stag-build/phonebook&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/@stag-build/phonebook" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@stag-build/phonebook&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live gallery: &lt;a href="https://stag-build.github.io/phonebook/" rel="noopener noreferrer"&gt;https://stag-build.github.io/phonebook/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Promo video: &lt;a href="https://youtu.be/trHbUaG784w" rel="noopener noreferrer"&gt;https://youtu.be/trHbUaG784w&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have a real Android or iOS repo with previews already in it, try generating a gallery from that repo and open the result with someone who does not live inside your IDE.&lt;/p&gt;

&lt;p&gt;That's the test I care about.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>android</category>
      <category>swiftui</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I tried vibe coding, and survived to tell (well kinda) - Part 1</title>
      <dc:creator>orelzion</dc:creator>
      <pubDate>Sun, 10 Aug 2025 09:38:26 +0000</pubDate>
      <link>https://dev.to/orelzion/i-tried-vibe-coding-and-survived-to-tell-well-kinda-5815</link>
      <guid>https://dev.to/orelzion/i-tried-vibe-coding-and-survived-to-tell-well-kinda-5815</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; I'm still in the early stages of the project; vibe coding is fine and all — until you need to do something that's slightly customized. Then, a developer's eye is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  📖 The Why
&lt;/h2&gt;

&lt;p&gt;Ok, ok, let me start the story like it should—from the beginning.&lt;/p&gt;

&lt;p&gt;So, I'm writing a book (it's about being moral, but you never asked, so I won't tell), and when I was searching for ways to properly edit and design it, I came across several options but didn't like any of them.&lt;/p&gt;

&lt;p&gt;They were either too complicated or cost more than I wished to afford.&lt;br&gt;&lt;br&gt;
I spent hours researching and asking ChatGPT for brilliant ways to overcome this, but to no avail.&lt;/p&gt;

&lt;p&gt;But then myself told me: "Orel! You're a programmer, right?"&lt;br&gt;&lt;br&gt;
"Well, yes," I replied, as I am quite polite.&lt;br&gt;&lt;br&gt;
"Then why," myself continued, "don't you develop an app tailored to your needs by yourself?!"&lt;/p&gt;

&lt;p&gt;I must admit that myself posed a good question, but I didn't want to admit it — so I came up with an even better idea:&lt;br&gt;&lt;br&gt;
"You know what?" I told myself, "why write an app when you clearly don't know enough JS, when you can vibe code it?"&lt;/p&gt;

&lt;p&gt;Oh, did I not mention? I'm a mobile developer; I barely know the basics of web development. So I went to ask the smartest person I know.&lt;/p&gt;

&lt;p&gt;No, not me, I'm not that pretentious.&lt;br&gt;&lt;br&gt;
I meant ChatGPT.&lt;/p&gt;

&lt;p&gt;So I asked him, and he agreed with myself. Now they were two against one, so I had to do the democratic thing and agree.&lt;/p&gt;

&lt;p&gt;And thus began the crazy journey of a mobile developer vibe coding his first desktop app. Stay tuned—there will be bears 🐻.&lt;/p&gt;
&lt;h2&gt;
  
  
  🛠️ The Plan
&lt;/h2&gt;

&lt;p&gt;I started by asking ChatGPT to come up with a plan. We discussed my needs, technologies, and features until I thought it was enough.&lt;/p&gt;

&lt;p&gt;Then, I asked it to craft a PRD and created a new directory on my computer.&lt;/p&gt;

&lt;p&gt;I ran &lt;a href="https://www.anthropic.com/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; from the VSCode terminal and asked it to read the PRD first and explain what its plan was to develop it.&lt;/p&gt;

&lt;p&gt;Claude Code is my favorite AI code assistant. I like that it's a CLI tool and that it seems to really understand the requirements of the project.&lt;/p&gt;

&lt;p&gt;It gave me a comprehensive plan, broken down into steps and divided into MVP and future implementation milestones. After some back and forth with it, I agreed to the plan.&lt;/p&gt;

&lt;p&gt;Then I asked it to write that plan down into a &lt;code&gt;development_plan.md&lt;/code&gt; file so we would have a reference.&lt;/p&gt;

&lt;p&gt;We decided to use &lt;code&gt;react&lt;/code&gt; for the frontend, built on top of &lt;a href="https://v2.tauri.app/" rel="noopener noreferrer"&gt;Tauri&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I saved my project and was ready to start coding—or, actually, vibe coding.&lt;/p&gt;
&lt;h2&gt;
  
  
  🚩 The Struggle
&lt;/h2&gt;

&lt;p&gt;The first struggle came when I asked Claude to start with installation. To its credit, it did try — but it kept failing. Eventually, I told it to just give me instructions and I'd handle the heavy lifting myself.&lt;/p&gt;

&lt;p&gt;I reverted all generated code and went through the installation process on my own: the Tauri CLI, the NPM packages, everything I needed. At last, everything was ready, and I did my first commit.&lt;/p&gt;

&lt;p&gt;Then I asked it to save our progress to a &lt;code&gt;development_log.md&lt;/code&gt; file so we could track progress. I ran &lt;code&gt;/clear&lt;/code&gt; to reset the context and &lt;a href="https://youtu.be/EssztxE9P28?si=Yb-hvsKA5hkS9koC" rel="noopener noreferrer"&gt;save money&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, I asked what our next step was, and it went ahead and started developing. I approved everything without reading the code. I mean, I could’ve, but this is vibe coding we’re doing—and like I said, I’m not a web developer.&lt;/p&gt;

&lt;p&gt;Anyway, it tried to run the app but had a hard time doing it from the CLI. So I told it I'd run it from the terminal myself. A good choice, and you’ll see why later.&lt;/p&gt;

&lt;p&gt;The first iteration was great: the editor was usable, I could write text, emphasize it, and do text-editor-y stuff. Excited, I asked it to add a chapters menu—and it did!&lt;/p&gt;

&lt;p&gt;I asked for more steps, but then my session was stopped. You see, my Claude subscription is the $20/month one, and I have a limit on tokens. So after a while, I got blocked. Kindly, it showed me when I could return to vibe — so I waited.&lt;/p&gt;
&lt;h2&gt;
  
  
  😍 The Honeymoon
&lt;/h2&gt;

&lt;p&gt;Waiting can teach you patience. And patience is everything, or something — I don’t know, I’m not great at Buddhism. Anyway, after my detention time ended, I returned to programming.&lt;/p&gt;

&lt;p&gt;We reviewed the structure of the saved files and agreed on how to store the markdown content. But something kept breaking. No matter how hard Claude tried to get the saving feature working, it wouldn’t cooperate.&lt;/p&gt;

&lt;p&gt;Then I realized a permission was probably missing. I mentioned this, and we searched together. Turns out, it did request the permission—but the Tauri package was missing, so I installed it manually. Once I did that, saving worked smoothly.&lt;/p&gt;

&lt;p&gt;We went back and forth on other features and made great progress. We implemented themes, I18N, tons of text features, chapters, and subchapters — I really felt like I didn’t need to do anything. An entire app was being built by a computer. Amazing.&lt;/p&gt;

&lt;p&gt;I got so excited I told anyone reckless enough to ask how it was going.&lt;/p&gt;

&lt;p&gt;And then I read the code...&lt;/p&gt;

&lt;p&gt;I discovered it didn’t always implement I18N and had hardcoded strings scattered throughout. It also didn’t consistently follow the theme approach and used inline styling in random places.&lt;/p&gt;

&lt;p&gt;So I read more about it and realized I needed to add a &lt;code&gt;CLAUDE.md&lt;/code&gt; file with proper instructions. I used Claude’s code to generate it, naturally.&lt;/p&gt;

&lt;p&gt;In this file, I stated some rules, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Important Rules
- **NEVER run the Tauri app** – always tell the user what to test and let them test manually
- **ALWAYS use i18n text** – never hard code strings, use translation keys from i18n config
- **Structure components in folders** – organize components into logical folder structures
- **No automatic tests** – we don’t run automated tests, manual testing only
- **ONLY use react-icons** – never use SVGs, always use icons from react-icons/fa6 package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also explained the project purpose, the technologies, and basically everything Claude needed to know. Now it mostly complies with my preferences — and if not, I just mention it broke the rules, and it apologizes and fixes things immediately.&lt;/p&gt;

&lt;p&gt;I mean, I’d prefer it didn’t break the rules in the first place, but you know what they say: “You can’t have it all.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyca0gzh314zvjh34mvq9.png" 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/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyca0gzh314zvjh34mvq9.png" alt="Claude Code interface" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  💻 The Debug
&lt;/h2&gt;

&lt;p&gt;Debugging with Claude is surprisingly pleasant. No, I really mean it.&lt;/p&gt;

&lt;p&gt;After some trial and error, I discovered the best way to actually fix problems: tell it to add logs.&lt;/p&gt;

&lt;p&gt;Yes, plain old logs.&lt;/p&gt;

&lt;p&gt;I just review the logs, paste them into Claude, and suddenly it understands exactly where the problem stems from.&lt;/p&gt;

&lt;p&gt;This feels a lot like pair programming to me.&lt;/p&gt;

&lt;p&gt;Of course, sometimes — or should I say too many times — Claude claims it figured out the problem and fixed it, only for you to test the app and discover it's gotten worse.&lt;/p&gt;

&lt;p&gt;But let's be honest: how many times has that happened to you? Don’t answer me.&lt;/p&gt;

&lt;p&gt;Still, this isn't quite the same. Like I said, I’m not a web developer, so I can't really verify that Claude isn't writing complete nonsense. But sometimes you just sense something's off.&lt;/p&gt;

&lt;p&gt;You know what I mean? Like you can't read TypeScript code, but you recognize a code smell when you see one.&lt;/p&gt;

&lt;p&gt;You notice how it sprinkles &lt;code&gt;setTimeout&lt;/code&gt; calls throughout the code and you're not sure why, but you're pretty sure that's not how things should work.&lt;/p&gt;

&lt;p&gt;So you ask why, and Claude responds with something like "You're right, let's completely change that." Then it rewrites everything and the app stops working entirely.&lt;/p&gt;

&lt;p&gt;Moments like this lead to great frustration. Honestly, there were times when I thought this whole thing was a big mistake, and I just couldn't fight with it anymore.&lt;/p&gt;

&lt;p&gt;Over time, I learned how to question it properly.&lt;/p&gt;

&lt;p&gt;If it tries to fix some problem in a way that looks odd to you, just stop and ask why — like why did it choose this particular solution?&lt;/p&gt;

&lt;p&gt;Sometimes you need to tell it to go look up the problem on the internet. Hell, sometimes I did it myself, came up with a solution, told Claude about it, and it implemented it perfectly.&lt;/p&gt;

&lt;p&gt;So yes, don’t automatically accept every one of its fixes. Question it.&lt;/p&gt;

&lt;p&gt;For example, if there's an error with a library it's using, Claude might just completely change it and use another library. You need to stop it from doing so and tell it to show pros and cons of that library — or to search for the issue online first. Just so you know, Claude can search the internet—it just won’t do it unless you specifically ask for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  🐻 The Bears
&lt;/h2&gt;

&lt;p&gt;Ok, ok, so there were no bears, I’m sorry for that. But we did reach the end of this post, and that's something, isn't it?&lt;/p&gt;

&lt;p&gt;If you followed along, you saw that I have my moments with Claude's code. Sometimes I'm like “God, this is genius - I love you!” and other times I'm like “How can you be so stupid?”&lt;/p&gt;

&lt;p&gt;But this isn't really the end. I'm only at the beginning of the project, so other posts will surely follow.&lt;/p&gt;

&lt;p&gt;Maybe there weren’t any bears — yet. But give it time. Claude might generate one.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>programming</category>
    </item>
    <item>
      <title>First impressions on Maestro - The simplest mobile testing framework</title>
      <dc:creator>orelzion</dc:creator>
      <pubDate>Sat, 17 Sep 2022 19:13:51 +0000</pubDate>
      <link>https://dev.to/orelzion/first-impressions-on-maestro-the-simplest-mobile-testing-framework-2neg</link>
      <guid>https://dev.to/orelzion/first-impressions-on-maestro-the-simplest-mobile-testing-framework-2neg</guid>
      <description>&lt;p&gt;&lt;small&gt;Photo by &lt;a href="https://unsplash.com/@markwilliamspics?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Mark Williams&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/conductor?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Last week I participated in &lt;a href="https://twitter.com/droidconNYC" rel="noopener noreferrer"&gt;Droidcon NYC&lt;/a&gt; and outside the main conference room was a stand of &lt;a href="https://www.mobile.dev/" rel="noopener noreferrer"&gt;mobile.dev&lt;/a&gt;. I usually don't stop at stands (unless I see some great t-shirts or socks 😄) but something caught my eyes.&lt;/p&gt;

&lt;p&gt;It was this demo&lt;br&gt;
&lt;iframe class="tweet-embed" id="tweet-1565004345355079680-189" src="https://platform.twitter.com/embed/Tweet.html?id=1565004345355079680"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1565004345355079680-189');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1565004345355079680&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;What caught my eye was the simplicity of this framework. I have some experience with &lt;a href="https://developer.android.com/training/testing/espresso" rel="noopener noreferrer"&gt;Espresso&lt;/a&gt; and I know how hard it is to write the same test in it.&lt;/p&gt;

&lt;p&gt;So I promised myself to go and check it out when I return, and so I did.&lt;/p&gt;

&lt;p&gt;In this post I'll share my first impressions and maybe this article will actually become a series 😄&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Installation was actually a breeze, I head over to the &lt;a href="https://maestro.mobile.dev/getting-started/installing-maestro" rel="noopener noreferrer"&gt;guide&lt;/a&gt; and everything is explained there.&lt;/p&gt;

&lt;p&gt;I decided to first install it locally, and since I want to test both Android and iOS I didn't install it into &lt;code&gt;gradle&lt;/code&gt; but via &lt;code&gt;homebrew&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Android
&lt;/h3&gt;

&lt;p&gt;Running my first test on Android was easy.&lt;/p&gt;

&lt;p&gt;I created a new file &lt;code&gt;simple_login_test.yml&lt;/code&gt; that looks like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai.viz.notifier"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;launchApp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;clearState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, I headed over to Android Studio to open an emulator.&lt;/p&gt;

&lt;p&gt;And then I run this line from my terminal&lt;br&gt;
&lt;code&gt;maestro test simple_login_flow.yml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And that was it! &lt;br&gt;
The app has launched!&lt;/p&gt;
&lt;h3&gt;
  
  
  iOS
&lt;/h3&gt;

&lt;p&gt;For iOS though things have been a little bit harder.&lt;/p&gt;

&lt;p&gt;I first needed to add another installation from &lt;code&gt;homebrew&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew tap facebook/fb
brew install idb-companion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a library from Facebook that will allow us to connect to an iOS simulator.&lt;/p&gt;

&lt;p&gt;After installing run this command in your terminal&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idb_companion --list 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it will list all of the available simluators, find one that you like (for me it was iPhone 13 mini) and copy its &lt;code&gt;udid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then you will launch it with this command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idb_companion --boot 85921D77-E655-4FA4-B202-D705E66E0D8A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After it's launch you'll need to connect to it with this command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idb_companion --udid 85921D77-E655-4FA4-B202-D705E66E0D8A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you should be able to start your test.&lt;/p&gt;

&lt;p&gt;So I created another file &lt;code&gt;simple_login_flow-ios.yml&lt;/code&gt; with the same code just replacing the android's appId with a iOS' bundle id.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;viz.ai.VizAINotifier"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;launchApp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;clearState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it didn't launch 😥&lt;/p&gt;

&lt;p&gt;Apparently I had to shut down the Android emulator first, and only then it launched.&lt;/p&gt;

&lt;p&gt;I guess they first try to find if there's an Android emulator and if there is they will try to launch on it. Then, it didn't find any app with that appId, so it just quite the test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running a simple flow
&lt;/h2&gt;

&lt;p&gt;So the first flow we're going to run is a simple login flow. I want to launch the app, enter wrong credentials, see an error, fix the credentials and login successfully.&lt;/p&gt;

&lt;p&gt;Simple, right?&lt;/p&gt;

&lt;p&gt;Well, with other frameworks (Espresso ☕️ I'm looking at you 👀), this would take one class to set all the views to their IDs, another to launch the app and dozen functions or so to interact and assert it.&lt;/p&gt;

&lt;p&gt;If you have to wait between actions, you'd have to also implement a timer to keep checking if your view is already displayed.&lt;/p&gt;

&lt;p&gt;Not with maestro!&lt;/p&gt;

&lt;p&gt;In here, to start the app all I need is this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai.viz.notifier"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;launchApp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;clearState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To tap a button to continue to the login screen, all I need is&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Welcome screen (yes, this is a comment)&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET STARTED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To do some assertions on views, to check that the views that suppose to be there are actually there, I needed&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Email"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertNotVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Password"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertNotVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Password"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The way that this works is that instead of looking for a view id, it looks for the text, the same way as your QA engineer will do it.&lt;/p&gt;

&lt;p&gt;But what if you &lt;strong&gt;don't have a text&lt;/strong&gt;? I wanted to assert if some icon was showing.&lt;/p&gt;

&lt;p&gt;Fortunately, you &lt;strong&gt;can use&lt;/strong&gt; IDs as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai.viz.notifier:id/fingerPrintButton"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Maestro hierarchy
&lt;/h3&gt;

&lt;p&gt;How did I get this id? Do I need yo open Android Studio and try to find it there? Hell no!&lt;/p&gt;

&lt;p&gt;We want simplicity and simplicity will shall get 😄&lt;/p&gt;

&lt;p&gt;To get all the views from a current screen, just run &lt;code&gt;maestro hierarchy&lt;/code&gt; in your command line. The output will be all your view tree, with texts and IDs.&lt;/p&gt;

&lt;p&gt;This is how I knew what ID to use here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other assertions
&lt;/h3&gt;

&lt;p&gt;I did have one more thing I wanted to test.&lt;/p&gt;

&lt;p&gt;In our app we disable the next button if you don't have text, this is part of the QA process and I wanted Maestro to do the same, but I didn't find any option to assert whether or not a view is clickable. &lt;/p&gt;

&lt;p&gt;So I asked in their &lt;a href="https://join.slack.com/t/mobile-dev-inc/shared_invite/zt-1f0nl7zsx-DndKtQmR0YvK9QdvSAgpwA" rel="noopener noreferrer"&gt;Slack channel&lt;/a&gt; whether they plan on having more assertions types.&lt;/p&gt;

&lt;p&gt;And this is how they answered&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmflahci4xmpfsa4szz8.png" 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/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmflahci4xmpfsa4szz8.png" alt="maestro philosophy" width="409" height="744"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Make sense!&lt;/p&gt;

&lt;p&gt;So what I did was to tap on the button and check that the screen hasn't changed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Email"&lt;/span&gt;
&lt;span class="c1"&gt;# Test next button is disable when no text entered&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Next"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Email"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  iOS
&lt;/h2&gt;

&lt;p&gt;I have other assertions in my file but I won't bore you with details 😄.&lt;/p&gt;

&lt;p&gt;I now wanted to have the same test with iOS.&lt;/p&gt;

&lt;p&gt;I started with the same test&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;viz.ai.VizAINotifier"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;launchApp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;clearState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Enter&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;your&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;email"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertNotVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Password"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But my first assertion failed. Although the text was there as a hint in the edit text, Maestro didn't find it.&lt;/p&gt;

&lt;p&gt;So I posted another question in the slack channel.&lt;/p&gt;

&lt;p&gt;Just 3 days after reporting, the issue got fixed!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrm86gso9ynqjlw5ihgw.png" 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/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrm86gso9ynqjlw5ihgw.png" alt="fixing maestro" width="568" height="563"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is an amazing pace for a library, and I think that this shows how hard they work to improve it and this is indeed a good sign.&lt;/p&gt;

&lt;p&gt;But to test the icon that was there, I didn't test by its ID, in iOS it actually use the image id as the text, so I was able to test it like this,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sfIcon60"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Problems?
&lt;/h3&gt;

&lt;p&gt;But then I got stuck, after the initial login flow, we have a view pager for a walkthrough. But no matter what I tried I wasn't able to test it.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;maestro hierarchy&lt;/code&gt; on this screen gave me absolutely nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;orelzion@N&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="err"&gt;QFJYD&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="err"&gt;GW&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;maestro&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;maestro&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hierarchy&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;"attributes"&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;span class="nl"&gt;"children"&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;span class="nl"&gt;"attributes"&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;"text"&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="s2"&gt;"VizDebug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bounds"&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="s2"&gt;"[0,0][375,812]"&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;"children"&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;span class="nl"&gt;"clickable"&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="kc"&gt;null&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;"attributes"&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;"resource-id"&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="s2"&gt;"screen.onboarding.container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bounds"&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="s2"&gt;"[0,0][375,812]"&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;"children"&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;span class="nl"&gt;"clickable"&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="kc"&gt;null&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;"clickable"&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="kc"&gt;null&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;p&gt;I was also trying to ask in the Slack channel, but this time, although they did try to help me, it just didn't work.&lt;/p&gt;

&lt;p&gt;I'm guessing I'm gonna need to investigate that some more to understand what is so special about this screen 😄.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;This post will probably have a follow up, the maestro framework is great! Super simple and intuitive, and I liked the philosophy of it.&lt;/p&gt;

&lt;p&gt;This is defiantly a library that I will continue to investigate, and I look forward to integrate that into our projects.&lt;/p&gt;

&lt;p&gt;Stay tuned! &lt;/p&gt;

</description>
      <category>mobile</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Optimising markers performance on Google Maps using RxJava</title>
      <dc:creator>orelzion</dc:creator>
      <pubDate>Sun, 14 Oct 2018 17:22:30 +0000</pubDate>
      <link>https://dev.to/orelzion/optimising-markers-perofrmence-on-google-maps-using-rxjava-5622</link>
      <guid>https://dev.to/orelzion/optimising-markers-perofrmence-on-google-maps-using-rxjava-5622</guid>
      <description>&lt;p&gt;So today I ran into an interesting problem. I needed to display a large amount of markers on Google Map, and we decided that too many markers will just not look good, and so we are eliminating markers that intersect by over 70 percent, and display just one of them instead.&lt;/p&gt;

&lt;p&gt;And so I wrote this little function that takes two Rect objects to see if they intersect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fuf9pmnczuwh6pamgjwop.png" 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/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fuf9pmnczuwh6pamgjwop.png" width="800" height="847"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But how do you get a rect object if all you have is a Lat/Lng of where you want to position your marker?&lt;br&gt;
Well, luckily for us Google Maps already has a utility function, that converts a lat/lng on map into a Point object. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;mGoogleMap.getProjection().toScreenLocation(latLng)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And so, I was able to create a Rect object based on that point plus the icon width and height.&lt;/p&gt;

&lt;p&gt;Now we need to iterate over all lat/lngs and check which of them are intersecting.&lt;/p&gt;

&lt;p&gt;Since this can be heavy (iterating over each object and check it against all other object in the list), I decided to run it inside an RxJava observable. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fz6w3izorywxbdzppcq0d.png" 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/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fz6w3izorywxbdzppcq0d.png" width="800" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Seems legit, right?&lt;/p&gt;

&lt;p&gt;Well, I thought, let's give it a run. Oh boy, that sucked terribly. I ran the app on a Google Pixel 2 device, and whenever I moved the screen and the markers needed redrawing, the app just stuck for a second.&lt;/p&gt;

&lt;p&gt;So I looked at my code again, and although the method 'getPoiForVisibleRegion' is running in the background, the map operation that comes next is not! This is so because I only added the subscribeOn and observeOn methods inside 'getPoiForVisibleRegion' so the map operation that was called later was not included.&lt;/p&gt;

&lt;p&gt;Ok, so I just need to include the subscribe method after the map declaration, right?&lt;/p&gt;

&lt;p&gt;Not so fast, my friend.&lt;br&gt;
Here's the catch. Google Maps can't get you the screen point while in background, so that means that I was able to run every other parts of my algorithm in RxJava but the intersection filter. &lt;/p&gt;

&lt;p&gt;Back to the drawing table then.&lt;/p&gt;

&lt;p&gt;Now I thought, what if I could get all the points for the currently visible markers before the filter, and make the actual filter run in background when it already has all the points?&lt;/p&gt;

&lt;p&gt;And so I did just that. I created a HashMap that updates every time the map bounds changes (so that means zoom level has changed, or the map moves). I get the currently visible region of the map while still on the UI thread&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mGoogleMap.getProjection().getVisibleRegion().latLngBounds;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then I go and filter out only the markers that I should draw now. And I save a mapping between those locations to their point on screen&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fktflh1zb3nsl26qo3kjf.png" 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/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fktflh1zb3nsl26qo3kjf.png" width="800" height="309"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Viola! &lt;/p&gt;

&lt;p&gt;The result was very satisfying! The map moves smoothly and the markers are getting removed or appeared on screen with a nice animation (not described in this post), without the user has to wait more than a fraction of a second, and more importantly without the screen ever gets stuck&lt;/p&gt;

</description>
      <category>android</category>
      <category>googlemaps</category>
      <category>rxjava</category>
    </item>
  </channel>
</rss>
