<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Mykola Miroshnychenko</title><description>Writing about Android, Kotlin Multiplatform and building software that lasts.</description><link>https://mirosh.dev/</link><item><title>Building your own Agentic Skill</title><link>https://mirosh.dev/building-your-own-agentic-skill/</link><guid isPermaLink="true">https://mirosh.dev/building-your-own-agentic-skill/</guid><description>In one of my previous posts, I covered what an MCP server is. Now I’ll explore another option for improving your interaction with LLMs &amp; agents - more specifically, we’ll look into Skills.

There’s no better way of understanding something than by building it yourself. So in this article, we will use all of our knowledge from the previous two articles on screenshot testing, and we’ll try to create our own skill to be used by others.


What is a skill?

As previously An MCP server is a “tool” that</description><pubDate>Tue, 16 Jun 2026 18:22:32 GMT</pubDate><content:encoded>&lt;p&gt;In one of my previous posts, &lt;a href=&quot;https://mirosh.dev/rapid-ui-prototyping-with-figma-android-mcp-servers/&quot;&gt;I covered what an MCP server is&lt;/a&gt;. Now I’ll explore another option for improving your interaction with LLMs &amp;#x26; agents - more specifically, we’ll look into Skills.&lt;/p&gt;&lt;p&gt;There’s no better way of understanding something than by building it yourself. So in this article, we will use all of our knowledge from the &lt;a href=&quot;https://mirosh.dev/setting-up-android-screenshot-testing-in-ci-with-git-lfs/&quot;&gt;previous two articles&lt;/a&gt; on screenshot testing, and we’ll try to create our own skill to be used by others.&lt;/p&gt;&lt;h1 id=&quot;what-is-a-skill&quot;&gt;What &lt;em&gt;is&lt;/em&gt; a skill?&lt;/h1&gt;&lt;p&gt;As previously An MCP server is a “tool” that your agent can use, while a “skill” is a modular instruction with samples to give your LLM of choice a better understanding of certain tasks you want it to perform.&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!qZqN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F827ac623-22f0-4a2e-98af-60ea724e10bc_1649x923.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1456&quot; height=&quot;815&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;Agents load skills &lt;strong&gt;in steps&lt;/strong&gt;:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Discovery&lt;/strong&gt;: Initially, an agent loads &lt;em&gt;&lt;strong&gt;only&lt;/strong&gt;&lt;/em&gt; the description and the name of the skill. All it needs to know at this point is &lt;em&gt;what&lt;/em&gt; the skill does and &lt;em&gt;when&lt;/em&gt; to trigger it.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Activation&lt;/strong&gt;: Once the agent comes across a task that can be a potential match for a skill, it reads the full &lt;code&gt;SKILL.md&lt;/code&gt; file and puts it into context.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Execution&lt;/strong&gt;: The agent proceeds with the instructions described in &lt;code&gt;SKILL.md&lt;/code&gt;. If necessary, the agent may choose to execute the bundled code and use files in the referenced directories.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Full instructions load &lt;em&gt;only&lt;/em&gt; when a task matches the description, so agents can be aware of dozens or hundreds of skills, whilst using only a small fraction of context.&lt;/p&gt;&lt;h3 id=&quot;what%E2%80%99s-inside&quot;&gt;What’s inside&lt;/h3&gt;&lt;p&gt;A “skill” is just what’s contained in the &lt;code&gt;SKILL.md &lt;/code&gt;file. As per the standard, the &lt;strong&gt;only&lt;/strong&gt; mandatory fields are the &lt;code&gt;name&lt;/code&gt; and the &lt;code&gt;description&lt;/code&gt;. After that, you should have the actual &lt;em&gt;implementation&lt;/em&gt; of the skill - this can be anything from code samples to instructions to a truth table, etc.&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!IhCa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d275808-f7e9-4e64-b159-555c2af0681e_1406x857.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1406&quot; height=&quot;857&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;h1 id=&quot;best-practices-for-designing-a-skill&quot;&gt;Best practices for designing a Skill&lt;/h1&gt;&lt;p&gt;There are &lt;a href=&quot;https://agentskills.io/skill-creation/best-practices?ref=cms.mirosh.dev&quot;&gt;dozens of best practices for creating a skill.&lt;/a&gt; But the most important one is providing as many useful details as possible without polluting the context . It can be tempting to put all of the knowledge of humankind into a &lt;a href=&quot;http://skill.md/?ref=cms.mirosh.dev&quot;&gt;SKIL&lt;/a&gt;&lt;a href=&quot;http://l.md/?ref=cms.mirosh.dev&quot;&gt;L.md&lt;/a&gt; , but remember that your LLM has limited context. And once activated, the LLM will load the whole &lt;a href=&quot;http://skill.md/?ref=cms.mirosh.dev&quot;&gt;SKILL.md&lt;/a&gt; file. According to &lt;a href=&quot;https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them?ref=cms.mirosh.dev&quot;&gt;this page&lt;/a&gt; by OpenAI:&lt;/p&gt;&lt;p&gt;1 token ~ 4 chars&lt;/p&gt;&lt;p&gt;If your skill is massive - say 100k chars, that equates to ~25k tokens. This means you have used up 1/8th of your context window (considering it’s 200k). And if the agent loads multiple skills for a specific task, it’ll have even less for your codebase. So I think you can see where this is going. You have to be frugal with what you put in your &lt;a href=&quot;http://skill.md/?ref=cms.mirosh.dev&quot;&gt;SKILL.md&lt;/a&gt; .&lt;/p&gt;&lt;p&gt;Here’s some nice advice from the official skill documentation:&lt;/p&gt;&lt;blockquote&gt;Read agent execution traces, not just final outputs. If the agent wastes time on unproductive steps, common causes include instructions that are too vague (the agent tries several approaches before finding one that works), instructions that don’t apply to the current task (the agent follows them anyway), or too many options presented without a clear default.&lt;/blockquote&gt;&lt;p&gt;Skills work best when they’re specific and modular. I was thinking of making a skill for screenshot testing as a whole, but such a skill might be too broad. That’s why we’ll solve a super-specific problem like integrating screenshot testing into Github Actions with Paparazzi. I already covered this &lt;a href=&quot;https://nickmiroshnychenko.substack.com/p/introduction-to-screenshot-testing?r=2zfs35&quot;&gt;in my previous article&lt;/a&gt;. So it makes sense to put this knowledge inside a skill.&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card kg-card-hascaption&quot;&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1587582423116-ec07293f0395?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxidWlsZHxlbnwwfHx8fDE3ODEzNjcwNzd8MA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=1080&quot; class=&quot;kg-image&quot; alt=&quot;construction worker in hard hat on building frame&quot; loading=&quot;lazy&quot; title=&quot;construction worker in hard hat on building frame&quot; width=&quot;3649&quot; height=&quot;2433&quot; srcset=&quot;https://images.unsplash.com/photo-1587582423116-ec07293f0395?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxidWlsZHxlbnwwfHx8fDE3ODEzNjcwNzd8MA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=600 600w, https://images.unsplash.com/photo-1587582423116-ec07293f0395?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxidWlsZHxlbnwwfHx8fDE3ODEzNjcwNzd8MA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=1000 1000w, https://images.unsplash.com/photo-1587582423116-ec07293f0395?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxidWlsZHxlbnwwfHx8fDE3ODEzNjcwNzd8MA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=1600 1600w, https://images.unsplash.com/photo-1587582423116-ec07293f0395?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxidWlsZHxlbnwwfHx8fDE3ODEzNjcwNzd8MA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=2400 2400w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;figcaption&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Photo by &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/@josholalde?ref=cms.mirosh.dev&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Josh Olalde&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt; on &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/?ref=cms.mirosh.dev&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Unsplash&lt;/span&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;h1 id=&quot;building-the-skill&quot;&gt;Building the skill&lt;/h1&gt;&lt;p&gt;Ok, let’s get our hands dirty with building the skill. The &lt;code&gt;name&lt;/code&gt; and the &lt;code&gt;description&lt;/code&gt; are the &lt;strong&gt;only&lt;/strong&gt; mandatory parts of the declaration. But we’ll also add some metadata.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;name: paparazzi-github-actions
description: Integrates screenshot testing with Paparazzi in Github Actions
metadata: 
	author: Mykola Miroshnychenko
	keywords:
	  - Screenshot testing
	  - CI
	  - Continuous integration
	  - Paparazzi
	  - Github 
	  - Github Actions
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;adding-prerequisites&quot;&gt;Adding prerequisites&lt;/h2&gt;&lt;p&gt;Before we start putting the execution instruction, code snippets, references, etc., it’s best to let the agent know what needs to be in place before we proceed.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;## Prerequisites

Make sure that Paparazzi Gradle plugin is installed. The latest version can be checked here - &amp;#x3C;https://cashapp.github.io/paparazzi/changelog/&gt; 
Git LFS needs to be installed. Ask the user for permission to install it if it&apos;s not installed. URL - &amp;#x3C;https://git-lfs.com/&gt;  Command - brew install git-lfs
The project needs to have Paparazzi tests in place before proceeding.
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;steps&quot;&gt;Steps&lt;/h2&gt;&lt;p&gt;This is the part that contains the actual execution algorithm. I like the step-by-step approach. It gives the agent a clear path to follow instead of grasping at straws. It’s also easier to structure your own thoughts like that as well.&lt;/p&gt;&lt;h4 id=&quot;step-1&quot;&gt;Step 1&lt;/h4&gt;&lt;pre&gt;&lt;code&gt;A Yaml file with the current Github Actions workflow should 
be in  `./github/workflows`. If it&apos;s not there - create these directories and 
inside create a `github-actions.yml` file. In such case, use 
[this reference](references/basic-github-actions-workflow.md) for a standard workflow to put inside this file. 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Notice that I’m referencing the skill folder structure here - specifically the &lt;code&gt;references &lt;/code&gt;directory.&lt;/p&gt;&lt;p&gt;Since we don’t want to waste our tokens, it’s best not to overload our agent with unnecessary information. The structure for a basic GitHub Actions workflow &lt;code&gt;.yml&lt;/code&gt; file should &lt;strong&gt;&lt;em&gt;only&lt;/em&gt;&lt;/strong&gt; be read by the agent in case there is no CI workflow at all. Therefore, we don’t put the whole workflow inside &lt;code&gt;SKILL.md&lt;/code&gt; itself, but instead into the &lt;code&gt;references&lt;/code&gt; directory for the agent to access if needed. This saves valuable context window and tokens.&lt;/p&gt;&lt;p&gt;Another reason for extracting code into different directories is that the LLM might already know what to do - so your solution might be redundant. However, if you absolutely want to guarantee that the agent uses &lt;strong&gt;&lt;em&gt;your&lt;/em&gt;&lt;/strong&gt; solution - then make sure the wording is explicit, like “Use”, “Read”, “Extract”, etc.&lt;/p&gt;&lt;h4 id=&quot;step-2&quot;&gt;Step 2&lt;/h4&gt;&lt;pre&gt;&lt;code&gt;If the user has has an existing workflow file in `./github/workflows` - amend the 

	checkout Repo step (actions/checkout@v4) by adding `lfs:true` at the end like so: 

      - name: Checkout Repo
        uses: actions/checkout@v4
        with:
          lfs: true
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;steps-3-4-5-6%E2%80%A6&quot;&gt;Steps 3, 4, 5, 6…&lt;/h4&gt;&lt;p&gt;I’m not going to bore you with the whole sequence of events - you get the idea. In general, this whole skill is based &lt;a href=&quot;https://nickmiroshnychenko.substack.com/p/0c9fd854-81d2-4441-b281-1c3fd665d7c5?postPreview=paid&amp;#x26;updated=2026-05-24T13%3A24%3A18.248Z&amp;#x26;audience=everyone&amp;#x26;free_preview=false&amp;#x26;freemail=true&quot;&gt;on my previous article&lt;/a&gt; . You can check out the full &lt;code&gt;SKILL.md&lt;/code&gt; file &lt;a href=&quot;https://gist.github.com/nsmirosh/bccecc79b5a5391f76e8cf3adb1eade4?ref=cms.mirosh.dev&quot;&gt;here&lt;/a&gt; .&lt;/p&gt;&lt;p&gt;This is what the structure of our skill looks like so far.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;paparazzi-github-actions/
├── SKILL.md  # Our actual instructions
├── references/workflow.yml #separate &quot;standard&quot; github actions workflow
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card kg-card-hascaption&quot;&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1582719471384-894fbb16e074?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzNHx8dGVzdGluZ3xlbnwwfHx8fDE3ODEzNjc0OTR8MA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=1080&quot; class=&quot;kg-image&quot; alt=&quot;woman in white robe sitting on black office rolling chair&quot; loading=&quot;lazy&quot; title=&quot;woman in white robe sitting on black office rolling chair&quot; width=&quot;3456&quot; height=&quot;5184&quot; srcset=&quot;https://images.unsplash.com/photo-1582719471384-894fbb16e074?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzNHx8dGVzdGluZ3xlbnwwfHx8fDE3ODEzNjc0OTR8MA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=600 600w, https://images.unsplash.com/photo-1582719471384-894fbb16e074?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzNHx8dGVzdGluZ3xlbnwwfHx8fDE3ODEzNjc0OTR8MA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=1000 1000w, https://images.unsplash.com/photo-1582719471384-894fbb16e074?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzNHx8dGVzdGluZ3xlbnwwfHx8fDE3ODEzNjc0OTR8MA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=1600 1600w, https://images.unsplash.com/photo-1582719471384-894fbb16e074?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzNHx8dGVzdGluZ3xlbnwwfHx8fDE3ODEzNjc0OTR8MA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=2400 2400w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;figcaption&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Photo by &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/@nci?ref=cms.mirosh.dev&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;National Cancer Institute&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt; on &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/?ref=cms.mirosh.dev&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Unsplash&lt;/span&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;h1 id=&quot;a-skill-to-test-our-skill&quot;&gt;A skill to test our skill&lt;/h1&gt;&lt;p&gt;Just like with your everyday coding, you can have a standardized way of testing your skill. You can do this with a JSON file called &lt;code&gt;evals.json&lt;/code&gt; . However, testing a skill is different from testing code because you will have to check the output yourself. So, this is more akin to “fine-tuning” rather than testing.&lt;/p&gt;&lt;p&gt;Below is the &lt;code&gt;eval.json&lt;/code&gt; which consist of:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Prompt&lt;/strong&gt;: Something a real user might type which would trigger your skill&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Expected output&lt;/strong&gt;: This is the part that needs to be verified by a human. Essentially, this is the “ideal” scenario that has to be checked.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Input files&lt;/strong&gt;: &lt;strong&gt;&lt;em&gt;An optional&lt;/em&gt;&lt;/strong&gt; argument that contains a path to assets that might be used during the test.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;We’ll start with two main scenarios as of now.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;{
  &quot;skill_name&quot;: &quot;paparazzi-github-actions&quot;,
  &quot;evals&quot;: [
    {
      &quot;id&quot;: 1,
      &quot;prompt&quot;: &quot;I want my paparazzi screenshot tests to run on github actions. The repo already has the tests and the generated snapshots committed, but nothiing is yet done on CI&quot;,
      &quot;expected_output&quot;: &quot;Verifies the prerequisites first: the Paparazzi Gradle plugin is applied, Git LFS is available, and the paparazzi.snapshot { } tests each have a matching PNG under app/src/test/snapshots/images. Since there is no .github/workflows/ directory, creates .github/workflows/screenshot-tests.yml containing a checkout step with lfs: true, JDK + Gradle setup, a verifyPaparazzi&amp;#x3C;Variant&gt; Gradle step, and an upload-artifact step that runs only on failure. Initializes LFS (git lfs install), tracks **/snapshots/**/*.png and commits .gitattributes, then — because the PNGs are already committed to history — moves them into LFS with git lfs migrate import and warns that this rewrites history and will need a force-push. Confirms tracking with git lfs ls-files.&quot;,
    },
    {
      &quot;id&quot;: 2,
      &quot;prompt&quot;: &quot;My repo already has a CI workflow at .github/workflows/android.yml that builds the app and runs unit tests. I want to add Paparazzi screenshot verification to it. Do not replace my existing workflow.&quot;,
      &quot;expected_output&quot;: &quot;Does NOT overwrite or recreate the existing workflow file. Edits android.yml in place: adds lfs: true to the actions/checkout@v4 step in the relevant job, and ensures a verifyPaparazzi&amp;#x3C;Variant&gt; Gradle step plus a failure-only upload-artifact step exist (adding them if missing), adjusted to the repo&apos;s module and build variant. Then performs the Git LFS setup (git lfs install, track the snapshots glob, migrate the already-committed PNGs into LFS) and verifies with git lfs ls-files.&quot;,
    }  
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We’ll put our &lt;code&gt;evals.json&lt;/code&gt; into a separate directory so our structure will look like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;paparazzi-github-actions/
├── SKILL.md
└── references/
    └── workflow.yml
└── evals/
    └── evals.json&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now you can either “manually” run these prompts, or you can use an “orchestrator” skill. By “orchestration” I simply mean &lt;a href=&quot;https://github.com/anthropics/skills/tree/main/skills/skill-creator?ref=cms.mirosh.dev&quot;&gt;another set of instructions i.e. a “skill-creator” skill&lt;/a&gt;. This skill was developed by Anthropic with the specific intent of creating and improving new skills. It’s an incredibly thorough set of instructions - so we will cover only the basics here.&lt;/p&gt;&lt;p&gt;Here’s the prompt that I’m using to orchestrate the testing of my skill:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;I want you to use skill-creator skill and run the evals in the paparazzi-github-actions/evals within the current project   
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once the orchestrator finishes the evaluation, you’ll get a &lt;code&gt;*skill*-workspace&lt;/code&gt; directory alongside your skill one - in my case, it’s the &lt;code&gt;paparazzi-github-actions-workspace&lt;/code&gt; .&lt;/p&gt;&lt;p&gt;Depending on how many times you’ve run/improved your skill, you might get several &lt;code&gt;iteration&lt;/code&gt; directories. In my case, I ran it only once, so I only have one. Inside, you’ll find a bunch of files providing you insight into the performance of your skill:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!DQbk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e92440f-b97a-4558-b420-4005896f9492_1302x1012.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1302&quot; height=&quot;1012&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;But the most interesting is the &lt;code&gt;benchmark.md&lt;/code&gt;:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!zCjH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45405d7f-8567-4d9e-b03c-dbdee5b37bf7_1276x666.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1276&quot; height=&quot;666&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;And the most important point is the &lt;code&gt;Delta&lt;/code&gt; . So in my case, apparently, the success rate “With Skill” is 100% - which is reassuring. Yet in terms of the token usage and the time passed - it’s not that great. But, this is the first iteration - which means there’s a ton of room for improvement.&lt;/p&gt;&lt;p&gt;Now, skill optimization is another story, and it deserves an article of its own. I’ll leave you some resources that you can look into in case you’re interested.&lt;/p&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;While creating your skill is definitely an interesting thing to try, I feel like it’s a bit too “vague” in terms of building the actual skill and optimizing it. While writing this article, I’ve looked inside skills that were built by different companies, and it looks like there’s no single coherent standard. It seems you can put anything inside the instructions, and the way it’s actually tested is at the mercy of the “skill-creator” skill. Iterational improvement of skills is also very costly - a single iteration of &lt;code&gt;paparazzi-github-actions&lt;/code&gt; skill took &gt; 40k tokens. This is considering it’s &lt;em&gt;tiny&lt;/em&gt; compared to other skills. So multiple improvement iterations might be in the millions of tokens.&lt;br&gt;&lt;/p&gt;&lt;p&gt;What are your thoughts on this? Have you found a good way of building skills for LLMS? Leave your thoughts below.&lt;/p&gt;</content:encoded></item><item><title>Setting up Android screenshot testing in CI with Git LFS.</title><link>https://mirosh.dev/setting-up-android-screenshot-testing-in-ci-with-git-lfs/</link><guid isPermaLink="true">https://mirosh.dev/setting-up-android-screenshot-testing-in-ci-with-git-lfs/</guid><description>In the previous article, we looked at how to integrate screenshot testing with Paparazzi, how to create tests that are not brittle, and how we can use certain parameters to make our tests more effective.

In this article, we’ll look into another problem associated with screenshot testing - more specifically, storing the screenshots remotely. As your app grows and gets more complex, so does the number of your screenshot tests. If you’re working on a real production application, the number of imag</description><pubDate>Mon, 01 Jun 2026 16:45:47 GMT</pubDate><content:encoded>&lt;p&gt;In &lt;a href=&quot;https://nickmiroshnychenko.substack.com/p/introduction-to-screenshot-testing?r=2zfs35&quot;&gt;the previous article&lt;/a&gt;, we looked at how to integrate screenshot testing with Paparazzi, how to create tests that are not brittle, and how we can use certain parameters to make our tests more effective.&lt;/p&gt;&lt;p&gt;In this article, we’ll look into another problem associated with screenshot testing - more specifically, storing the screenshots remotely. As your app grows and gets more complex, so does the number of your screenshot tests. If you’re working on a real production application, the number of images taken can easily spike into hundreds or even thousands.&lt;/p&gt;&lt;p&gt;Storing thousands of images in your repository is a bad idea. This will bloat your repository size, and you’ll run into problems with your remote repository provider. To solve this problem - we can use &lt;a href=&quot;https://git-lfs.com/?ref=cms.mirosh.dev&quot;&gt;Git LFS&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Here’s a high-level overview of what we’ll do:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Understand how Git LFS works&lt;/li&gt;&lt;li&gt;Install Git LFS&lt;/li&gt;&lt;li&gt;Track Snapshots with LFS&lt;/li&gt;&lt;li&gt;Migrate existing screenshots to LFS&lt;/li&gt;&lt;li&gt;Configure CI to use Git LFS and produce a report&lt;/li&gt;&lt;/ol&gt;&lt;h1 id=&quot;how-git-lfs-works&quot;&gt;How Git LFS works&lt;/h1&gt;&lt;p&gt;LFS stands for Large File Storage. Usually, LFS is used in the gaming industry, because the assets they use can easily exceed Gigabytes or even tens of Gigabytes For this purpose Git LFS was invented.&lt;/p&gt;&lt;p&gt;Most Remote Git Servers will limit your push file size to 50 or 100 Mb per push. And the limit for repo size on free plans can be 1 or 2 GB. Since screenshots are big files by Android standards, it’s best to allow LFS to handle this.&lt;/p&gt;&lt;p&gt;Here’s a diagram of how Git LFS works:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!Ittg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb3f332b-71bb-4231-a563-05de564f89dc_1325x1036.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1325&quot; height=&quot;1036&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;When you push your large files with LFS - the remote repository will transfer these large files to an LFS server and will then only store references or pointers to the actual files in the codebase.&lt;/p&gt;&lt;p&gt;So the files themselves live locally on your machine and on the LFS server.&lt;/p&gt;&lt;p&gt;When you need to retrieve from the remote repository - the opposite happens. The remote repo will looks through the “pointers” it has and will query the actual files from the LFS server. Then it will transfer them to your machine.&lt;/p&gt;&lt;p&gt;The great thing about this is that you don’t need to worry about all of this complex set up. This is all handled by remote repo providers like Github, GitLab, Bitbucket and others.&lt;/p&gt;&lt;h1 id=&quot;installing-git-lfs&quot;&gt;Installing Git LFS&lt;/h1&gt;&lt;p&gt;The &lt;a href=&quot;https://git-lfs.com/?ref=cms.mirosh.dev&quot;&gt;installation&lt;/a&gt; is super simple. Simply run:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;git lfs install&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we need to make LFS track the files that we want uploaded to the LFS server. In our case, it’s going to be PNG files stored in the tests → snapshots → images directory. We could specify to track &lt;em&gt;specifically&lt;/em&gt; the files in the &lt;code&gt;snapshots&lt;/code&gt; directory, but for simplicity, we’ll simply store &lt;em&gt;all&lt;/em&gt; PNG files in LFS:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;git lfs track &quot;*.png&quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now running this command will start tracking files that you have NOT committed yet. Which means that we’ll need to manually “import” the existing snapshots that we already have from previous tests.&lt;/p&gt;&lt;p&gt;After running this command, you should see the &lt;code&gt;.gitattributes&lt;/code&gt; file. This is the actual configuration file that tells git what to do with PNG files in your repository. Also, if we check the staging status of our file in Git with &lt;code&gt;git status&lt;/code&gt; we’ll see that all of our PNGs have become “staged for commit”:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!Qkg9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa9286e3-24a8-4f51-9a65-51d053a980f2_2422x666.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1456&quot; height=&quot;400&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;Let’s go ahead and add them to our commit and push to our repo with &lt;code&gt;git push&lt;/code&gt; . If all is well, you’ll see this in your terminal:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;git p
Uploading LFS objects: 100% (6/6), 162 KB | 0 B/s, done. &lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;verifying-our-lfs-works&quot;&gt;Verifying our LFS works&lt;/h2&gt;&lt;p&gt;Now, to confirm that Git is no longer tracking our PNGs but only has a pointer to them, we can ask it to show the contents of the “PNG file” that we have. So let’s ask it to show any of the 4 screenshots that we have by running this command:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;git show HEAD:app/src/test/snapshots/images/dev.mirosh.topusers.ui.screenshots_ScreenShotTest_testListItemWithLongNameFollowing.png&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which will show us 3 lines:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;version &amp;#x3C;https://git-lfs.github.com/spec/v1&gt;
oid sha256:fbdc1fc6eaf974dfaa6a7522a2f7e2db4d49d9b0f41c132d4a589601e23aa27d &amp;#x3C;- our pointer
size 41554 &amp;#x3C;- size of the image on the LFS server&lt;/code&gt;&lt;/pre&gt;&lt;h1 id=&quot;configuring-our-ci-to-work-with-lfs-files&quot;&gt;Configuring our CI to work with LFS files&lt;/h1&gt;&lt;p&gt;By default, CI tools that you use will actually check out the pointers to the LFS files, instead of the files themselves. But for our screenshot testing to run on the CI - we need to reference the actual files behind these pointers. Luckily, this is super easy.&lt;/p&gt;&lt;p&gt;In your &lt;code&gt;.yaml&lt;/code&gt; file add the &lt;code&gt;lfs:true&lt;/code&gt; line to the checkout repository action:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;steps:
  - name: Checkout repository
    uses: actions/checkout@v4
	    with:
	      lfs: true&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For our demonstration, I will use GitHub Actions to run the pipeline. Here’s the full list of &lt;code&gt;steps&lt;/code&gt; in my &lt;code&gt;.yml&lt;/code&gt; file:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - name: Checkout Repo
        uses: actions/checkout@v4
        with:
          lfs: true

      - name: Verify Paparazzi screenshots
        run: ./gradlew :app:verifyPaparazziDebug

      - name: Upload Paparazzi failure report
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: index.html
          path: app/build/reports/tests/testDebugUnitTest/
          if-no-files-found: ignore&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The last step upload the report as an artifact in case of test failure. Make sure to, of course, adjust this script according to the names of your build variants.&lt;/p&gt;&lt;h1 id=&quot;running-the-tests-on-ci&quot;&gt;Running the tests on CI&lt;/h1&gt;&lt;p&gt;Paraparazzi will only generate a report in case there is a failure of some test. So to confirm that our screenshot tests work on Github Actions, we’ll change some of our UI. In our &lt;code&gt;ListItemWithShortNameNotFollowing()&lt;/code&gt; Preview Composable I’ll just change it to &lt;code&gt;following&lt;/code&gt; :&lt;/p&gt;&lt;pre&gt;&lt;code&gt;@Preview
@Composable
fun ListItemWithShortNameNotFollowing() {
    val userUIModel = UserUiModel(
        id = 1,
        displayName = &quot;Nick Mirosh&quot;,
        reputation = &quot;1000k&quot;,
        profileImage = &quot;&quot;,
        following = true // &amp;#x3C;- Changed
    )
    ListItem(user = userUIModel) {}
}

&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now, let’s make a commit and push, and observe our pipeline…&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!rvU4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70b48af4-912e-49ba-98f2-7650a91a13cb_1012x532.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1012&quot; height=&quot;532&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;Our &lt;code&gt;./gradlew :app:verifyPaparazziDebug&lt;/code&gt; task fails as expected, it will produce a report. Then our next task runs:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!scSN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d0e42ee-839b-4014-9b9a-6c9a6de2acd5_648x98.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;98&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;And we get an artifact download URL:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;Artifact download URL: &amp;#x3C;https://github.com/nsmirosh/ScreenshotTestingSample/actions/runs/26213027963/artifacts/7130214525&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And after unpacking the archive and opening the &lt;code&gt;index.html&lt;/code&gt; we get our good ol’ report:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!OFAD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcad5e31-a424-47ea-8f43-d30b9bbf6bc6_2946x632.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1456&quot; height=&quot;312&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;&lt;p&gt;And there you have it. In these two articles, we have covered everything from the basics of screenshot testing, all the way up to deploying it on your CI.&lt;/p&gt;&lt;p&gt;Screenshot testing definitely has its place in your day-to-day Android development. It can make your life easier during migrations, catching subtle regressions and testing scenarios you wouldn’t physically be able to test.&lt;/p&gt;&lt;p&gt;However, this type of testing also comes with the dangers of creating flaky tests. Screenshot testing should not be the core of your test suite.&lt;/p&gt;&lt;p&gt;All in all, it’s one of the “funnest” ways of verifying your UI, considering how explicit the visuals are. I urge you to integrate it into your project and play around with it - you now have everything you need to do that!&lt;/p&gt;</content:encoded></item><item><title>Introduction to Screenshot testing with Paparazzi.</title><link>https://mirosh.dev/introduction-to-screenshot-testing-with-paparazzi/</link><guid isPermaLink="true">https://mirosh.dev/introduction-to-screenshot-testing-with-paparazzi/</guid><description>Screenshot testing can be a powerful tool, but it can also wreck your tests - here&apos;s how to use it properly.




Introduction

You’ve probably heard about screenshot testing but haven’t tried it. Or you tried it and it broke everything in your workflow, so you decided it was a bad idea. There’s a lot of hype around the topic, and many teams seem to be adopting this type of testing into their workflow. Integrating screenshot testing is fairly easy. What’s NOT easy is applying it properly without</description><pubDate>Fri, 15 May 2026 08:20:42 GMT</pubDate><content:encoded>&lt;h3 id=&quot;screenshot-testing-can-be-a-powerful-tool-but-it-can-also-wreck-your-testsheres-how-to-use-it-properly&quot;&gt;Screenshot testing can be a powerful tool, but it can also wreck your tests - here&apos;s how to use it properly.&lt;/h3&gt;&lt;p&gt;&lt;/p&gt;&lt;h1 id=&quot;introduction&quot;&gt;Introduction&lt;/h1&gt;&lt;p&gt;You’ve probably heard about screenshot testing but haven’t tried it. Or you tried it and it broke everything in your workflow, so you decided it was a bad idea. There’s a lot of hype around the topic, and many teams seem to be adopting this type of testing into their workflow. Integrating screenshot testing is fairly easy. What’s NOT easy is applying it properly without messing up your development process.&lt;/p&gt;&lt;p&gt;In this article, we’re going to look at :&lt;/p&gt;&lt;ul&gt;&lt;li&gt;What screenshot testing is&lt;/li&gt;&lt;li&gt;How to integrate it into your project and CI&lt;/li&gt;&lt;li&gt;How to write screenshot tests that add value to your project&lt;/li&gt;&lt;li&gt;Where you should avoid it&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Ok, first let’s look at what it is. The code that we’ll be using in this article can be found here: &lt;a href=&quot;https://github.com/nsmirosh/ScreenshotTestingSample?ref=cms.mirosh.dev&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;https://github.com/nsmirosh/ScreenshotTestingSample&lt;/a&gt;&lt;/p&gt;&lt;h1 id=&quot;what-is-screenshot-testing&quot;&gt;What is screenshot testing?&lt;/h1&gt;&lt;p&gt;Screenshot testing is simply the process of comparing your views with the reference images stored previously. It’s literally the process of comparing images pixel by pixel. After you have the reference (Golden value) screenshots in place, you can then run the tests to see whether your UI is looking different from what you intended. After running the tests, you will get a report with the “diffs” between your pre-recorded “golden value” and what’s actually happening in your UI.&lt;/p&gt;&lt;h2 id=&quot;where-screenshot-testing-works-well&quot;&gt;Where screenshot testing works well.&lt;/h2&gt;&lt;p&gt;Screenshot testing should be used for catching regressions in your UI.&lt;/p&gt;&lt;p&gt;In general, it is really useful when:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;You have components that don’t change often&lt;/li&gt;&lt;li&gt;You have a shared design system or component library where one change can influence many different “consumers”.&lt;/li&gt;&lt;li&gt;Your app supports multiple themes, locales, fonts, or screen sizes — scenarios where it’s not realistic to check everything manually.&lt;/li&gt;&lt;li&gt;You’re refactoring infrastructure, and you don’t expect anything to change visually.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;where-it%E2%80%99s-a-pain&quot;&gt;Where it’s a pain:&lt;/h2&gt;&lt;p&gt;While screenshot testing may be useful in situations described above, it can also lead to very brittle tests if applied incorrectly. Do NOT use screenshot testing for:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Validating complex screens that change often.&lt;/li&gt;&lt;li&gt;Validating behavior of your components (you can’t do that with screenshot testing any way).&lt;/li&gt;&lt;li&gt;Anything that a Unit or a UI test will catch instead. Unit and UI tests are far less costly in terms of maintenance than screenshot testing.&lt;/li&gt;&lt;/ul&gt;&lt;figure class=&quot;kg-card kg-image-card kg-card-hascaption&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!lExe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d65096c-38e6-4940-902a-e8fffb2a060f_1080x858.jpeg&quot; class=&quot;kg-image&quot; alt=&quot;a table topped with lots of different types of cameras&quot; loading=&quot;lazy&quot; title=&quot;a table topped with lots of different types of cameras&quot; width=&quot;1080&quot; height=&quot;858&quot; decoding=&quot;async&quot;&gt;&lt;figcaption&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Photo by &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/@mackiec?ref=cms.mirosh.dev&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Christian Mackie&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt; on &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/?ref=cms.mirosh.dev&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Unsplash&lt;/span&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;h1 id=&quot;the-tools&quot;&gt;The tools&lt;/h1&gt;&lt;p&gt;There are maaany libraries for screenshot testing. Here’s a list of just a few:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Google &lt;a href=&quot;https://developer.android.com/studio/preview/compose-screenshot-testing?ref=cms.mirosh.dev&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;Compose Preview Screenshot Testing tool&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Cashapp &lt;a href=&quot;https://github.com/cashapp/paparazzi?ref=cms.mirosh.dev&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;Paparazzi&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/takahirom/roborazzi?ref=cms.mirosh.dev&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;Roborazzi from takahirom&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Dropbox &lt;a href=&quot;https://github.com/dropbox/dropshots?ref=cms.mirosh.dev&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;Dropshots&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/pedrovgs/Shot?ref=cms.mirosh.dev&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;Shot from pedrovgs&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Ndtp &lt;a href=&quot;https://github.com/ndtp/android-testify?ref=cms.mirosh.dev&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;Android-testify&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Facebook &lt;a href=&quot;https://github.com/facebook/screenshot-tests-for-android?ref=cms.mirosh.dev&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;screenshot-tests-for-android&lt;/a&gt;&lt;/li&gt;&lt;li&gt;QuickBird &lt;a href=&quot;https://github.com/QuickBirdEng/kotlin-snapshot-testing?ref=cms.mirosh.dev&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;Snappy&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;This list is taken from Segio Sastres’s &lt;a href=&quot;https://github.com/sergio-sastre/Android-screenshot-testing-playground?ref=cms.mirosh.dev&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;screenshot testing playground&lt;/a&gt; . Make sure to check it out as well if you want to learn about different frameworks.&lt;/p&gt;&lt;p&gt;I’ve tried 3 of them: Paparazzi, Roborazzi and Google’s own screenshot testing library. Google’s library is still in the early alpha - so I don’t consider it a viable production library. And Roborazzi - I don’t have anything against it, but I’m always happy to use a library that is developed by Square. So I’ll be covering Paparazzi.&lt;/p&gt;&lt;h1 id=&quot;deciding-what-to-test&quot;&gt;Deciding what to test&lt;/h1&gt;&lt;p&gt;I’ll show the appropriate scenarios for using screenshot testing and how they can make your life harder if you do it wrong.&lt;/p&gt;&lt;p&gt;Let’s say we’re building an app that tracks the top 20 users of StackOverflow by rating, and we can follow and unfollow them.&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!LFG0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1574f124-51b7-4418-bc90-89cbffd8be6c_608x832.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;608&quot; height=&quot;832&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;Some scenarios where we can apply screenshot testing to a single list item:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Following / Not Following + Short / Long name&lt;/li&gt;&lt;li&gt;Image Loading Error / Placeholder&lt;/li&gt;&lt;li&gt;Light / Dark Theme&lt;/li&gt;&lt;li&gt;Large font scale&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Each of these can catch a real regression, and none of these states are subject to frequent change in our app.&lt;/p&gt;&lt;p&gt;Here are the previews generated for Following / Not Following states:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!IzUo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65b919a7-fa87-439c-93d3-95e8faea3ade_1048x400.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1048&quot; height=&quot;400&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;Now let’s implement the actual tests.&lt;/p&gt;&lt;h1 id=&quot;set-up&quot;&gt;Set up&lt;/h1&gt;&lt;p&gt;First, we need to set up Paparazzi.&lt;/p&gt;&lt;p&gt;As of the time of writing of this article, Paparazzi doesn’t support Gradle 9.3.0 and higher, so for this example, I’m using Gradle 9.2.1. If you use anything above you might run into problems &lt;a href=&quot;https://github.com/cashapp/paparazzi/issues/2227?ref=cms.mirosh.dev&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;https://github.com/cashapp/paparazzi/issues/2227&lt;/a&gt; ``&lt;/p&gt;&lt;p&gt;My &lt;code&gt;gradle-wrapper.properties&lt;/code&gt; set up looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;distributionSha256Sum=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f
distributionUrl=https\\://services.gradle.org/distributions/gradle-9.2.1-bin.zip

&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After modifying Gradle, go ahead and import the Paparazzi plugin into your app-level &lt;code&gt;build.gradle&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;plugins {
  id &apos;app.cash.paparazzi&apos; version &apos;2.0.0-alpha04&apos;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Paparazzi docs are &lt;a href=&quot;https://cashapp.github.io/paparazzi/?ref=cms.mirosh.dev#releases&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;h1 id=&quot;building-the-tests&quot;&gt;Building the tests&lt;/h1&gt;&lt;p&gt;Paparazzi &lt;a href=&quot;https://cashapp.github.io/paparazzi/?ref=cms.mirosh.dev#supporting-multiple-test-frameworks&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;supports multiple testing frameworks&lt;/a&gt; . For our example, we’ll use the good ol’ JUnit.&lt;/p&gt;&lt;p&gt;Building tests is simple. We want to take snapshots of the 4 states of the Composable as we mentioned above. And this is what it looks like:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;package dev.mirosh.topusers.ui.screenshots

import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_5
import app.cash.paparazzi.Paparazzi
import dev.mirosh.topusers.ui.main.ListItemWithLongNameFollowing
import dev.mirosh.topusers.ui.main.ListItemWithLongNameNotFollowing
import dev.mirosh.topusers.ui.main.ListItemWithShortNameFollowing
import dev.mirosh.topusers.ui.main.ListItemWithShortNameNotFollowing
import org.junit.Rule
import org.junit.Test

class ScreenShotTest {

    @get:Rule
    val paparazzi = Paparazzi(
        deviceConfig = PIXEL_5,
        theme = &quot;android:Theme.Material.Light.NoActionBar&quot;,
    )

    @Test
    fun testListItemWithLongNameFollowing() {
        paparazzi.snapshot {
            ListItemWithLongNameFollowing()
        }
    }

    @Test
    fun testListItemWithLongNameNotFollowing() {
        paparazzi.snapshot {
            ListItemWithLongNameNotFollowing()
        }
    }

    @Test
    fun testListItemWithShortNameFollowing() {
        paparazzi.snapshot {
            ListItemWithShortNameFollowing()
        }
    }

    @Test
    fun testListItemWithShortNameNotFollowing() {
        paparazzi.snapshot {
            ListItemWithShortNameNotFollowing()
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Each test takes a screenshot of the Previews that we have defined previously. After we have our tests set up, we need to record the “Golden” reference images - i.e. the images we will run our tests against. To record the references, we run the following Gradle task:&lt;/p&gt;&lt;p&gt;&lt;code&gt;./gradlew :app:recordPaparazziDebug&lt;/code&gt;&lt;/p&gt;&lt;p&gt;Make sure to replace &lt;code&gt;app&lt;/code&gt; with the name of your module if it’s different&lt;/p&gt;&lt;p&gt;The generated report will land in &lt;code&gt;app/build/reports/paparazzi/debug/index.html&lt;/code&gt;&lt;/p&gt;&lt;p&gt;and the individual screenshots in &lt;code&gt;src/test/snapshots&lt;/code&gt; . Here’s what &lt;code&gt;index.html&lt;/code&gt; looks like:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!EI3C!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F393ef2ee-6140-4323-934d-7a2f22b3ab5c_2496x1370.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1456&quot; height=&quot;799&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;To make sure your tests and previews render the same images you can use Coil’s &lt;code&gt;FakeImageLoader&lt;/code&gt; for &lt;a href=&quot;https://coil-kt.github.io/coil/testing/?ref=cms.mirosh.dev#testing&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;testing with Coil&lt;/a&gt;. And you can use &lt;code&gt;AsyncimagePreviewHandler&lt;/code&gt; for rendering Previews.&lt;/p&gt;&lt;h1 id=&quot;running-the-tests&quot;&gt;Running the tests&lt;/h1&gt;&lt;p&gt;Ok, we have the tests set up, now let’s actually run them using the following command: &lt;code&gt;./gradlew :app:verifyPaparazziDebug&lt;/code&gt;&lt;/p&gt;&lt;p&gt;This command will take a snapshot of the current state of our views - and will compare them to the “golden” pre-saved images from the previous command.&lt;/p&gt;&lt;p&gt;You will see the report with the diffs between the “Golden” image and the actual result in &lt;code&gt;app/build/reports/tests/testDebugUnitTest/index.html&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!kPdg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4637455-49b5-4d29-bd58-db1e410abb04_1378x472.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1378&quot; height=&quot;472&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;Obviously, the test succeeded - cause we’re comparing identical images. Nothing really interesting to see here. Now, let’s break our test.&lt;/p&gt;&lt;h2 id=&quot;breaking-the-tests&quot;&gt;Breaking the tests&lt;/h2&gt;&lt;p&gt;Let’s say that we decided to allow 3 lines for the name instead of 2 - just in case of a super extra long name like mine. So with that change - this is how the super long name will look like:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!cZrN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbd7a0a3-fc0b-4f2c-9981-10b24cea7338_394x160.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;696&quot; height=&quot;282&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;Let’s run the tests again. And as expected, two tests with long names fail.&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!sREF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53515062-bb73-4ac5-a38a-300181a19bdc_876x310.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;876&quot; height=&quot;310&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;And if we click the first test, we’ll see a long stack trace. However, what we care about is this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;java.lang.AssertionError: Images differ (by 1.282920%) - see details in file:///Users/mykolamiroshnychenko/coding/work/TopUsers/app/build/paparazzi/failures/delta-dev.mirosh.topusers.ui.screenshots_ScreenShotTest_testListItemWithLongNameFollowing.png

&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This comment is self-explanatory. Our images differ by 1.28%. But that doesn’t tell us anything in terms of what &lt;em&gt;exactly&lt;/em&gt; failed. Let’s open the file path with the details:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!-4bM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2e29118-d9c8-4337-ab72-529f9dae7c20_2260x1594.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1456&quot; height=&quot;1027&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;But… the difference between our Composables is huge - why does it say that it’s only ~1% difference?&lt;/p&gt;&lt;h1 id=&quot;%E2%80%9Cshrinking%E2%80%9D-composables&quot;&gt;“Shrinking” Composables&lt;/h1&gt;&lt;p&gt;Currently, our tests are set up to render the whole screen with our Composable. That’s why we get only 1% difference between our screenshots. To fix this, we need to apply a &lt;code&gt;SHRINK&lt;/code&gt; rendering mode to our Test Rule. By applying this argument, Paparazzi will record only our Composable without rendering the full screen:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;    @get:Rule
    val paparazzi = Paparazzi(
        deviceConfig = PIXEL_5,
        theme = &quot;android:Theme.Material.Light.NoActionBar&quot;,
        renderingMode = RenderingMode.SHRINK
    )
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We’ll need to re-record our Golden images and run the tests again:&lt;/p&gt;&lt;p&gt;&lt;code&gt;./gradlew :app:recordPaparazziDebug&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;./gradlew :app:verifyPaparazziDebug&lt;/code&gt;&lt;/p&gt;&lt;p&gt;If we open the generated report, we’ll see:&lt;/p&gt;&lt;p&gt;&lt;code&gt;java.lang.AssertionError: Images differ (by 14.988646%) - see details in file:&lt;/code&gt;&lt;/p&gt;&lt;p&gt;Now this is much closer to the truth.&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!NsAS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72c5aa39-e5a7-4b37-8ecb-875aed2a0459_3024x344.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1456&quot; height=&quot;166&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;And this leads me to my next point…&lt;/p&gt;&lt;h1 id=&quot;tiny-differences-will-cause-failures&quot;&gt;Tiny differences will cause failures&lt;/h1&gt;&lt;p&gt;Now, 15% is a lot, and such a test &lt;em&gt;should&lt;/em&gt; fail. But what if somewhere along the way, we decide to format the rating of our StackOverflow users? In cases where we have a short name, we want to display &lt;code&gt;1,000k&lt;/code&gt; instead of &lt;code&gt;1000k&lt;/code&gt; .&lt;/p&gt;&lt;p&gt;If we make this change, our tests will still fail - even though the difference will be an insignificant 0.32% (I ran it for you to save the reading time).&lt;/p&gt;&lt;p&gt;So, how can we add a bit of leniency for such cases? By default, the “margin error” for Paparazzi tests is 0.1%, or &lt;code&gt;0.1&lt;/code&gt; . But, if for some reason you have a flaky test, you can adjust this by tweaking the &lt;code&gt;maxDifferencePercentage&lt;/code&gt; parameter:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;    @get:Rule
    val paparazzi = Paparazzi(
        deviceConfig = PIXEL_5,
        theme = &quot;android:Theme.Material.Light.NoActionBar&quot;,
        renderingMode = RenderingMode.SHRINK,
        maxPercentDifference = 2.0 // 2% diff allowed
    )&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With this change, any tests that have a deviation of 2% between the Golden image and the test image will succeed.&lt;/p&gt;&lt;p&gt;But! Use this option sparingly! In fact, try to avoid using it at all. If you do have to use it - it’s best to isolate the flaky test into its own test class, because passing &lt;code&gt;maxDifferencePercentage&lt;/code&gt; it to our Rule will apply it to all of our tests, and we don’t want that.&lt;/p&gt;&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;&lt;p&gt;Screenshot testing is a great tool for catching visual regressions in your app. It can help you verify parts of your app/codebase that are hard to meaningfully test with UI or Unit tests. It’s also extremely easy to set up.&lt;/p&gt;&lt;p&gt;However, while this type of testing is undoubtedly useful, it can also be very brittle if set up incorrectly. So make sure you’re very careful in choosing Composable / Views that will not change often and don’t have many elements. Also, remember that other types of testing tend to be “cheaper” in terms of maintenance, so do not treat this as a silver bullet for UI testing.&lt;/p&gt;&lt;p&gt;In the next article, I’ll explore how you can integrate screenshot testing into your CI without bloating your repository by using Git LFS.&lt;/p&gt;</content:encoded></item><item><title>Rapid UI prototyping with Figma + Android MCP servers.</title><link>https://mirosh.dev/rapid-ui-prototyping-with-figma-android-mcp-servers/</link><guid isPermaLink="true">https://mirosh.dev/rapid-ui-prototyping-with-figma-android-mcp-servers/</guid><description>I’m not a designer, so my designs usually suck. I will always choose to work with a professionally-made design if I have the opportunity. And now that we have agents — we can ask them to quickly build our app UI based on a Figma design.

In this article, I’ll show you how to utilize MCP servers to build UIs that look exactly (well, almost) like the Figma design.


What is an MCP server?

From Wikipedia:

The Model Context Protocol (MCP) is an open standard and open-source framework introduced by</description><pubDate>Tue, 21 Apr 2026 07:27:11 GMT</pubDate><content:encoded>&lt;p&gt;I’m not a designer, so my designs usually suck. I will always choose to work with a professionally-made design if I have the opportunity. And now that we have agents — we can ask them to quickly build our app UI based on a Figma design.&lt;/p&gt;&lt;p&gt;In this article, I’ll show you how to utilize MCP servers to build UIs that look exactly (well, almost) like the Figma design.&lt;/p&gt;&lt;h2 id=&quot;what-is-an-mcp-server&quot;&gt;What is an MCP server?&lt;/h2&gt;&lt;p&gt;From Wikipedia:&lt;/p&gt;&lt;blockquote&gt;&lt;em&gt;The &lt;/em&gt;&lt;strong&gt;Model Context Protocol&lt;/strong&gt;&lt;em&gt; (&lt;/em&gt;&lt;strong&gt;MCP&lt;/strong&gt;&lt;em&gt;) is an &lt;/em&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Open_standard?ref=cms.mirosh.dev&quot; rel=&quot;noopener ugc nofollow&quot;&gt;open standard&lt;/a&gt;&lt;em&gt; and &lt;/em&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Open-source?ref=cms.mirosh.dev&quot; rel=&quot;noopener ugc nofollow&quot;&gt;open-source&lt;/a&gt;&lt;em&gt; &lt;/em&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Software_framework?ref=cms.mirosh.dev&quot; rel=&quot;noopener ugc nofollow&quot;&gt;framework&lt;/a&gt;&lt;em&gt; introduced by &lt;/em&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Anthropic?ref=cms.mirosh.dev&quot; rel=&quot;noopener ugc nofollow&quot;&gt;Anthropic&lt;/a&gt;&lt;em&gt; in November 2024 to standardize the way &lt;/em&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Artificial_intelligence?ref=cms.mirosh.dev&quot; rel=&quot;noopener ugc nofollow&quot;&gt;artificial intelligence&lt;/a&gt;&lt;em&gt; (AI) systems like &lt;/em&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Large_language_model?ref=cms.mirosh.dev&quot; rel=&quot;noopener ugc nofollow&quot;&gt;large language models&lt;/a&gt;&lt;em&gt; (LLMs) integrate and share data with external tools, systems, and data sources&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;Essentially, an MCP servers are tools that your AI agents can use in addition to what they have “out of the box”.&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/image.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1278&quot; height=&quot;864&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/image.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/image.png 1000w, https://cms.mirosh.dev/content/images/2026/04/image.png 1278w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;By default, Claude Code has access to tools like: bash, web search, stopping or starting tasks, spawning sub-agents, etc… You can see the full list &lt;a href=&quot;https://code.claude.com/docs/en/tools-reference?ref=cms.mirosh.dev&quot; rel=&quot;noopener ugc nofollow&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;While Claude Code is powerful by default, you can make it an ABSOLUTE UNIT of a coding agent for your specific task with MCP servers. For example, to build a UI for our app, it would be great if Claude could take screenshots, launch our app, debug it, and reference design details in Figma all by itself. Enter Android and Figma MCP servers.&lt;/p&gt;&lt;h2 id=&quot;installing-mcp-servers-local-vs-remote&quot;&gt;Installing MCP servers. Local vs Remote.&lt;/h2&gt;&lt;p&gt;There are two types of MCP servers — Local and Remote. The names are pretty self-explanatory. Whenever you use a remote server, your agent will make an “API” call that gets routed to a 3rd party server. The local server stays on your machine.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Disclaimer:&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Whenever you install an MCP server, &lt;strong&gt;make sure to exercise the same caution as you would with any new library&lt;/strong&gt;. You wouldn’t install a library written by an unknown developer without inspecting it — so make sure you really know what the MCP server does as well.&lt;/p&gt;&lt;h3 id=&quot;installing-an-android-server&quot;&gt;Installing an Android server&lt;/h3&gt;&lt;p&gt;Installing a server is &lt;em&gt;usually&lt;/em&gt; pretty easy. For this example, we’ll use local &lt;a href=&quot;https://github.com/martingeidobler/android-mcp-server?ref=cms.mirosh.dev&quot; rel=&quot;noopener ugc nofollow&quot;&gt;Android MCP server&lt;/a&gt; developed by &lt;a href=&quot;https://github.com/martingeidobler?ref=cms.mirosh.dev&quot; rel=&quot;noopener ugc nofollow&quot;&gt;martingeidobler&lt;/a&gt; . There are other, more popular ones, but I found Martin’s server to be super easy to set up. Also, it has 25 Android tools, which is very helpful.&lt;/p&gt;&lt;p&gt;To install for Claude Code, simply run this command in your terminal:&lt;/p&gt;
&lt;!--kg-card-begin: html--&gt;
claude mcp add - scope user android - npx -y android-mcp-server
&lt;!--kg-card-end: html--&gt;
&lt;p&gt;If you use a different agent, the installation instructions are &lt;a href=&quot;https://github.com/martingeidobler/android-mcp-server?tab=readme-ov-file&amp;#x26;ref=cms.mirosh.dev#setup&quot; rel=&quot;noopener ugc nofollow&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;You can also control the application scope of the server. More details about that &lt;a href=&quot;https://code.claude.com/docs/en/mcp?ref=cms.mirosh.dev#mcp-installation-scopes&quot; rel=&quot;noopener ugc nofollow&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;To verify the installation, first launch your emulator. Then, in Claude Code, run the &lt;code&gt;/mcp&lt;/code&gt;command. You should see something like:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/image-1.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;956&quot; height=&quot;374&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/image-1.png 600w, https://cms.mirosh.dev/content/images/2026/04/image-1.png 956w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;&lt;br&gt;If it shows up as &lt;code&gt;connected&lt;/code&gt;— that means Claude can now access it. Awesome, now let’s do the same thing for Figma.&lt;/p&gt;&lt;h3 id=&quot;installing-figma&quot;&gt;Installing Figma&lt;/h3&gt;&lt;p&gt;The installation instructions for Figma MCP for Claude Code are &lt;a href=&quot;https://developers.figma.com/docs/figma-mcp-server/remote-server-installation/?ref=cms.mirosh.dev#claude-code&quot; rel=&quot;noopener ugc nofollow&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Run this in your terminal to install:&lt;/p&gt;
&lt;!--kg-card-begin: html--&gt;
claude plugin install figma@claude-plugins-official

&lt;!--kg-card-end: html--&gt;
&lt;p&gt;Figma MCP is a remote server, so just keep that in mind.&lt;/p&gt;&lt;h2 id=&quot;the-design&quot;&gt;The design&lt;/h2&gt;&lt;p&gt;Ok, the tools are in place. Let’s take a look at what we want built. Here’s the main screen of an Android habit-tracking app that I’ve been building lately:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/image-2.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;312&quot; srcset=&quot;https://cms.mirosh.dev/content/images/2026/04/image-2.png 600w&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;Pretty ugly, if you ask me. I want my app to look elegant instead. Thankfully, we can find free designs on Figma and Dribble. And God bless those designers who put out their work for us to use for free.&lt;/p&gt;&lt;p&gt;One such sample is the &lt;a href=&quot;https://www.figma.com/community/file/1140538095096589968?ref=cms.mirosh.dev&quot; rel=&quot;noopener ugc nofollow&quot;&gt;HABITA Habit Tracker UI Kit&lt;/a&gt;. This design is free to use, and it was developed by Hertafui.&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/image-3.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;652&quot; height=&quot;646&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/image-3.png 600w, https://cms.mirosh.dev/content/images/2026/04/image-3.png 652w&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;h2 id=&quot;running-it-altogether&quot;&gt;Running it altogether&lt;/h2&gt;&lt;p&gt;Ok, we have our MCP servers set up with Claude Code, and we found the UI that we want. Now we need to provide Claude with a link to the design. This is done by clicking &lt;code&gt;Copy link to selection&lt;/code&gt; on the section that you want to reference in Figma.&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/image-4.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;751&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/image-4.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/image-4.png 1000w, https://cms.mirosh.dev/content/images/2026/04/image-4.png 1400w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;And here’s the prompt that I’m giving Claude:&lt;/p&gt;&lt;p&gt;Implement this Figma design: &lt;a href=&quot;https://www.figma.com/design/g207whsYY6odvltu0ObGgq/HABITA-Habit-Tracker-UI-Kit--Community-?node-id=3-660&amp;#x26;t=z6JCnk8QoaLTxMNh-4&amp;#x26;ref=cms.mirosh.dev&quot;&gt;https://www.figma.com/design/g207whsYY6odvltu0ObGgq/HABITA-Habit-Tracker-UI-Kit--Community-?node-id=3-660&amp;#x26;t=z6JCnk8QoaLTxMNh-4&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Scope:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Only apply this to HabitListScreen.kt&lt;/li&gt;
&lt;li&gt;Do not touch other screens&lt;/li&gt;
&lt;li&gt;Skip the “See All” button in the Figma design&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fidelity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1:1 visual match&lt;/li&gt;
&lt;li&gt;Try to use Figma&apos;s typography/color tokens exactly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After implementing, build with ./gradlew assembleDebug and launch on the emulator via MCP to verify.&lt;/p&gt;
&lt;p&gt;Let’s run!&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/image-5.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;519&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/image-5.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/image-5.png 1000w, https://cms.mirosh.dev/content/images/2026/04/image-5.png 1400w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/image-6.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1050&quot; height=&quot;366&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/image-6.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/image-6.png 1000w, https://cms.mirosh.dev/content/images/2026/04/image-6.png 1050w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;h2 id=&quot;result-conclusion&quot;&gt;Result &amp;#x26; Conclusion&lt;/h2&gt;&lt;p&gt;Ok, after running for 4.5 minutes — here’s what Claude built:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/image-7.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;590&quot; height=&quot;452&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;Not bad. If I were writing this by hand, it would’ve definitely taken me &gt; 20 mins. Maybe even more. However, remember that there’s always a tradeoff when you delegate to AI. If this were a production app, I would now need to spend time verifying that the code is flexible, safe, reusable, etc. But for prototyping — this is a gold mine.&lt;/p&gt;&lt;p&gt;In conclusion, MCP servers can be used to help AI agents perform tasks that are out of reach for them by default. In this article, we used Android + Figma MCP servers to build a new UI for my habit-tracking app. You can check out a whole bunch of other MCP servers &lt;a href=&quot;https://github.com/modelcontextprotocol/servers?tab=readme-ov-file&amp;#x26;ref=cms.mirosh.dev&quot; rel=&quot;noopener ugc nofollow&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;blockquote&gt;Thank you for reading!&lt;br&gt;&lt;br&gt;If you want to stay up-to-date with modern Android &amp;#x26; KMP development — sign up to my “Multiplatform Mindset” newsletter &lt;a href=&quot;https://medium.com/subscribepage.io/ODZYLC?ref=cms.mirosh.dev&quot; rel=&quot;noopener ugc nofollow&quot;&gt;here&lt;/a&gt; or on &lt;a href=&quot;https://mirosh.dev/&quot; rel=&quot;noopener ugc nofollow&quot;&gt;my website&lt;/a&gt;.&lt;/blockquote&gt;</content:encoded></item><item><title>Overly relying on AI diminishes critical thinking and increases cognitive debt.</title><link>https://mirosh.dev/overly-relying-on-ai-diminishes-critical-thinking-and-increases-cognitive-debt/</link><guid isPermaLink="true">https://mirosh.dev/overly-relying-on-ai-diminishes-critical-thinking-and-increases-cognitive-debt/</guid><description>I&apos;ve noticed this myself.

It&apos;s very tempting to keep on pushing Claude Code or other agents to generate code for your project. But at a certain point, you notice that you&apos;re avoiding reviewing the generated code. The thought process is: &quot;Oh, it probably got it right - I&apos;ll review it later&quot;. Spoiler - you won&apos;t.

That&apos;s why I decided I will have days where I don&apos;t use any AI at all. I will make sure that during these days I go over the code previously generated and actually think whether it make</description><pubDate>Fri, 17 Apr 2026 09:31:01 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve noticed this myself.&lt;br&gt;&lt;br&gt;It&apos;s very tempting to keep on pushing Claude Code or other agents to generate code for your project. But at a certain point, you notice that you&apos;re avoiding reviewing the generated code. The thought process is: &quot;Oh, it probably got it right - I&apos;ll review it later&quot;. Spoiler - you won&apos;t. &lt;br&gt;&lt;br&gt;That&apos;s why I decided I will have days where I don&apos;t use any AI at all. I will make sure that during these days I go over the code previously generated and actually think whether it makes sense to have it. &lt;br&gt;&lt;br&gt;Cognitive debt is real. Here is a quote from the study: &quot;Cognitive debt defers mental effort in the short term but results in long-term costs, such as diminished critical inquiry, increased vulnerability to manipulation, decreased creativity. When participants reproduce suggestions without evaluating their accuracy or relevance, they not only forfeit ownership of the ideas but also risk internalizing shallow or biased perspectives.&quot;&lt;br&gt;&lt;br&gt;In short, use your brain more often. &lt;br&gt;&lt;br&gt;Here are some studies to refer to: &lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://arxiv.org/pdf/2506.08872v1?ref=cms.mirosh.dev&quot;&gt;https://arxiv.org/pdf/2506.08872v1&lt;/a&gt;&lt;/p&gt;&lt;figure class=&quot;kg-card kg-bookmark-card&quot;&gt;&lt;a class=&quot;kg-bookmark-container&quot; href=&quot;https://www.nsta.org/blog/think-or-not-think-impact-ai-critical-thinking-skills?srsltid=AfmBOooDi5h8xuBwscxvhzplbRKHLkLmRJYs412d5QxaegcApttOrGz5&amp;#x26;ref=cms.mirosh.dev&quot;&gt;&lt;div class=&quot;kg-bookmark-content&quot;&gt;&lt;div class=&quot;kg-bookmark-title&quot;&gt;To Think or Not to Think: The Impact of AI on Critical-Thinking Skills&lt;/div&gt;&lt;div class=&quot;kg-bookmark-description&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-bookmark-metadata&quot;&gt;&lt;img class=&quot;kg-bookmark-icon&quot; src=&quot;https://static.ghost.org/v5.0.0/images/link-icon.svg&quot; alt=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;span class=&quot;kg-bookmark-author&quot;&gt;NSTA - National Science Teachers Association&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;kg-bookmark-thumbnail&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/thumbnail/Mar25_AI-Blog_Think__1024.jpg&quot; alt=&quot;&quot; onerror=&quot;this.style.display = &amp;#x27;none&amp;#x27;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;</content:encoded></item><item><title>How a $5 tub of cottage cheese exposed a million-dollar UX flaw.</title><link>https://mirosh.dev/how-a-5-tub-of-cottage-cheese-exposed-a-million-dollar-ux-flaw/</link><guid isPermaLink="true">https://mirosh.dev/how-a-5-tub-of-cottage-cheese-exposed-a-million-dollar-ux-flaw/</guid><description>I’m a massive fan of @Villa Market JP. They’re easily the best place in Bangkok for high-quality imports (and my specific breakfast obsession). But a recent late-night quest for cottage cheese turned into a masterclass in why &quot;small&quot; mobile UX flaws can cost a business serious money

I love cottage cheese. In fact I love cottage cheese so much I ate it for a couple of years straight for breakfast and I didn’t get bored with it. For some reason, though, cottage cheese (good one) is hard to come b</description><pubDate>Thu, 02 Apr 2026 06:46:19 GMT</pubDate><content:encoded>&lt;p&gt;I’m a massive fan of &lt;strong&gt;@Villa Market JP&lt;/strong&gt;. They’re easily the best place in Bangkok for high-quality imports (and my specific breakfast obsession). But a recent late-night quest for cottage cheese turned into a masterclass in why &quot;small&quot; mobile UX flaws can cost a business serious money&lt;/p&gt;&lt;p&gt;I love cottage cheese. In fact I love cottage cheese so much I ate it for a couple of years straight for breakfast and I didn’t get bored with it. For some reason, though, cottage cheese (good one) is hard to come by in Bangkok. There are very few places that sell it here.&lt;/p&gt;&lt;p&gt;Yesterday evening, however, I was determined to get it and watch “The Man in The High Castle”. And there is one place that sells it - a supermarket chain called Villa Market.&lt;/p&gt;&lt;p&gt;So happily having strolled into one of Villa Markets I quickly approached the dairy aisle. To my horror - they were out of stock. My mind started racing as I was thinking where else can I get it right now. I immediately checked Grab to see if any other place might be selling it. Nothing… I noticed however, that Grab wasn’t searching through Villa Markets in the area. Bingo! I just needed to download the Villa Market app, and order it from another store - surely they have it there!&lt;/p&gt;&lt;p&gt;The clock was ticking though - it was around 9:30 pm and Villa closes at 10, so I needed act fast.&lt;/p&gt;&lt;p&gt;I downloaded the app, registered, received the OTP… I could feel I was getting close to my beloved cottage cheese with every step.&lt;/p&gt;&lt;p&gt;Now I needed to find it. This is where I encountered the first weird design decision. You had to check each store individually for the item you’re looking for. Whatever, ok, this wasn’t going to stop me.&lt;/p&gt;&lt;p&gt;But no so fast said the app... &lt;/p&gt;&lt;h2 id=&quot;the-first-friction&quot;&gt;The First Friction&lt;/h2&gt;&lt;p&gt;Instead of showing me a map - I was presented with a list of addresses of stores - including those in other cities. Huh, ok, you’re not going to stop me this easily. I found a store with a street name that sounded familiar - so I knew it was somewhere close&lt;/p&gt;&lt;p&gt;Praise be! My cottage cheese was found! There it was, in its full glory, looking at me from Villa Market Gaysorn. Overjoyed, I swiftly added it to my cart, and was dashing towards check out.&lt;/p&gt;&lt;p&gt;I was at the finish line. The credit card was out. This is the moment every business dreams of—and the moment their app decided to kick me out of the store. And just as I was about to press “Pay” - a window (dialog in mobile terminology) pops-up asking me if I want to switch to store that is closer to me. Ooooh, a nice little optimization I thought. Don’t mind if I do. I press “Switch” - and my cottage cheese dissapears….. Ehhhh what?&lt;/p&gt;&lt;p&gt;Apparently, this “closer” store is also out of stock. What? Why would you recommend me a “closer” store - if it doesn’t have what I need? Confused, but not demoralized, I went to the home page, switched back to the store that had it in stock, found my cheese again, went to check out and BAM! That window again. But, you won’t get me this time you trickster of application!&lt;/p&gt;&lt;h2 id=&quot;broken-logic-abandoned-cart&quot;&gt;Broken Logic = Abandoned Cart&lt;/h2&gt;&lt;p&gt;I tap outside of the window dialog expecting the window to go away (as it should), but it doesn’t. After a dozen or so taps I understood that this app has a deadly flaw in the world of mobile development. It has the dreaded undismissable dialog. Something that you should never have in your app. Now I was getting desperate. My belief in me getting the cottage cheese started to get shaky.&lt;/p&gt;&lt;p&gt;Ok, there’s still one thing I can do. Let’s revoke the location permission. The app won’t know my location now, so they shouldn’t show me this dialog altogether. I run through the same flow - and you know what I see? That darned dialog window again. Whyyyy? Have they cached my location and now there’s no way to get rid of it?&lt;/p&gt;&lt;p&gt;At this point, I ran out of ideas. My plan of eating some cottage cheese while watching “The Man in The High Castle” that evening has been crushed. I was defeated. I had to admit that this app bested me.&lt;/p&gt;&lt;p&gt;There was, however, a solution that I discovered later. Deleting the app, installing it, and denying a permission from the start allows you to bypass this dialog.&lt;/p&gt;&lt;p&gt;What is this all about?&lt;/p&gt;&lt;p&gt;I wanted to spend money. The app wouldn&apos;t let me. As a developer, I see this every day: businesses spending thousands on user acquisition, only to lose them due to preventable UX friction.&lt;/p&gt;&lt;p&gt;It’s not just one tub of cottage cheese. It’s the LTV (Lifetime Value) of a customer. Because of this friction, I sought an alternative. In a competitive market, &lt;strong&gt;friction is a gift to your competitors.&lt;/strong&gt;&lt;/p&gt;&lt;h2 id=&quot;3-core-ux-rules&quot;&gt;&lt;strong&gt;3 Core UX Rules&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;All of these problems come down to &lt;strong&gt;3 core UX rules&lt;/strong&gt;:&lt;/p&gt;&lt;p&gt;1. Always show what’s happening - spinners, progress, feedback&lt;/p&gt;&lt;p&gt;2. Never trap the user - cancel, undo, back navigation&lt;/p&gt;&lt;p&gt;3. The app should be predictable - same actions → same results&lt;/p&gt;&lt;h2 id=&quot;how-would-i-fix-this-app&quot;&gt;How would I fix this app?&lt;/h2&gt;&lt;p&gt;Your goal as a business is to guide your customer to check out as effortlessly as possible.&lt;/p&gt;&lt;p&gt;Long list of unknown addresses? A lot of people would give up at this point. Better to quickly show a map with nearby locations.&lt;/p&gt;&lt;p&gt;You want to offer a closer location to your customer? By all means do it - but respect their decision of declining your “offer” by allowing them to dismiss the dialog.&lt;/p&gt;&lt;p&gt;Your user revokes a certain permission? Track that and delete the associated data. Even if you think that keeping that data might turn out useful later - that data might be stale - exactly as this story points out.&lt;/p&gt;&lt;p&gt;Offer the customer to switch to a different store at checkout? Sure, but make sure that that store HAS the products that your customer has in their basket.&lt;/p&gt;&lt;p&gt;To be clear: I’m not bashing the &lt;strong&gt;@Villa Market JP&lt;/strong&gt; dev team. They’ve built a solid service that I use regularly. My goal is simply to highlight how &quot;corner cases&quot; like this can derail an otherwise great customer journey.&lt;/p&gt;&lt;div class=&quot;kg-card kg-cta-card kg-cta-bg-grey kg-cta-immersive kg-cta-centered&quot; data-layout=&quot;immersive&quot;&gt;
            
            &lt;div class=&quot;kg-cta-content&quot;&gt;
                
                
                    &lt;div class=&quot;kg-cta-content-inner&quot;&gt;
                    
                        &lt;div class=&quot;kg-cta-text&quot;&gt;
                            &lt;p dir=&quot;ltr&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Is your app’s checkout as smooth as it could be? Let’s find out.&lt;/span&gt;&lt;/p&gt;
                        &lt;/div&gt;
                    
                    
                        &lt;a href=&quot;https://mirosh.dev/about/&quot; class=&quot;kg-cta-button&quot; style=&quot;background-color: #000000; color: #ffffff;&quot;&gt;
                            Contact me
                        &lt;/a&gt;
                        
                    &lt;/div&gt;
                
            &lt;/div&gt;
        &lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class=&quot;kg-card kg-gallery-card kg-width-wide&quot;&gt;&lt;div class=&quot;kg-gallery-container&quot;&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 0.8000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/1-1.png&quot; width=&quot;1080&quot; height=&quot;1350&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/1-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/1-1.png 1000w, https://cms.mirosh.dev/content/images/2026/04/1-1.png 1080w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 0.8000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/2-1.png&quot; width=&quot;1080&quot; height=&quot;1350&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/2-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/2-1.png 1000w, https://cms.mirosh.dev/content/images/2026/04/2-1.png 1080w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 0.8000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/3-1.png&quot; width=&quot;1080&quot; height=&quot;1350&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/3-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/3-1.png 1000w, https://cms.mirosh.dev/content/images/2026/04/3-1.png 1080w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 0.8000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/4-1.png&quot; width=&quot;1080&quot; height=&quot;1350&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/4-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/4-1.png 1000w, https://cms.mirosh.dev/content/images/2026/04/4-1.png 1080w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 0.8000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/6-1.png&quot; width=&quot;1080&quot; height=&quot;1350&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/6-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/6-1.png 1000w, https://cms.mirosh.dev/content/images/2026/04/6-1.png 1080w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 0.8000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/5-1.png&quot; width=&quot;1080&quot; height=&quot;1350&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/5-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/5-1.png 1000w, https://cms.mirosh.dev/content/images/2026/04/5-1.png 1080w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 0.8000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/7-1.png&quot; width=&quot;1080&quot; height=&quot;1350&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/7-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/7-1.png 1000w, https://cms.mirosh.dev/content/images/2026/04/7-1.png 1080w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 0.8000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/8-1.png&quot; width=&quot;1080&quot; height=&quot;1350&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/8-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/8-1.png 1000w, https://cms.mirosh.dev/content/images/2026/04/8-1.png 1080w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;</content:encoded></item><item><title>6 Legacy App Horrors That I&apos;ve seen - That Might Be Killing Your Business Too.</title><link>https://mirosh.dev/6-legacy-app-horrors-that-ive-seen-that-might-be-killing-your-business-too/</link><guid isPermaLink="true">https://mirosh.dev/6-legacy-app-horrors-that-ive-seen-that-might-be-killing-your-business-too/</guid><description>I’ve spent years working with legacy mobile codebases. Some were stable, but most were &quot;Jenga Towers&quot; held together by hope and deprecated libraries.

Legacy code is a silent tax on your business and a headache for devs. Here are the top 6 horrors I’ve encountered and how I’d fix them in 2026:

 1. The Mother of all requests. It starts out with a home page that keeps growing and growing… While initially it might be a good idea to pre-load data - usually the bloat expands, as the complexity of th</description><pubDate>Wed, 01 Apr 2026 19:26:19 GMT</pubDate><content:encoded>&lt;p&gt;I’ve spent years working with legacy mobile codebases. Some were stable, but most were &quot;Jenga Towers&quot; held together by hope and deprecated libraries.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Legacy code is a silent tax on your business&lt;/strong&gt; and a headache for devs. Here are the top 6 horrors I’ve encountered and how I’d fix them in 2026:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card kg-card-hascaption&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/photo-1727361287752-e52bf6564631.jpeg&quot; class=&quot;kg-image&quot; alt=&quot;A tall building with many balconies and balconies on the sides&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;2369&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/photo-1727361287752-e52bf6564631.jpeg 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/photo-1727361287752-e52bf6564631.jpeg 1000w, https://cms.mirosh.dev/content/images/size/w1600/2026/04/photo-1727361287752-e52bf6564631.jpeg 1600w, https://cms.mirosh.dev/content/images/2026/04/photo-1727361287752-e52bf6564631.jpeg 2000w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;figcaption&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Photo by &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/@shailopez?ref=cms.mirosh.dev&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Shai Lopez&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt; / &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/?utm_source=ghost&amp;#x26;utm_medium=referral&amp;#x26;utm_campaign=api-credit&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Unsplash&lt;/span&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;The Mother of all requests. &lt;/strong&gt;It starts out with a home page that keeps growing and growing… While initially it might be a good idea to pre-load data - usually the bloat expands, as the complexity of the application grows. The user is left hanging while the app is loading ungodly amount information.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;👉 Solution : Instead of having your good ol’ REST APIs you can go with GraphQL. In Kotlin Multiplatform this is easily done with Apollo Kotlin or a Ktor GraphQL client.&lt;/p&gt;&lt;ol start=&quot;2&quot;&gt;&lt;li&gt;&lt;strong&gt;The request stampede. &lt;/strong&gt;Multiple REST requests at the same time, to the same server. There are specific cases where it makes sense to have multiple requests done at the same time, but very often it’s a result of poor planning.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;👉 Solution : GraphQL can help here as well, by requesting only the needed data from the a single endpoint.&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card kg-card-hascaption&quot;&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1501139083538-0139583c060f?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGJhZCUyMHRpbWV8ZW58MHx8fHwxNzc1MDcxMDg2fDA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=2000&quot; class=&quot;kg-image&quot; alt=&quot;selective focus photo of brown and blue hourglass on stones&quot; loading=&quot;lazy&quot; width=&quot;6000&quot; height=&quot;4000&quot; srcset=&quot;https://images.unsplash.com/photo-1501139083538-0139583c060f?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGJhZCUyMHRpbWV8ZW58MHx8fHwxNzc1MDcxMDg2fDA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=600 600w, https://images.unsplash.com/photo-1501139083538-0139583c060f?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGJhZCUyMHRpbWV8ZW58MHx8fHwxNzc1MDcxMDg2fDA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=1000 1000w, https://images.unsplash.com/photo-1501139083538-0139583c060f?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGJhZCUyMHRpbWV8ZW58MHx8fHwxNzc1MDcxMDg2fDA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=1600 1600w, https://images.unsplash.com/photo-1501139083538-0139583c060f?crop=entropy&amp;#x26;cs=tinysrgb&amp;#x26;fit=max&amp;#x26;fm=jpg&amp;#x26;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGJhZCUyMHRpbWV8ZW58MHx8fHwxNzc1MDcxMDg2fDA&amp;#x26;ixlib=rb-4.1.0&amp;#x26;q=80&amp;#x26;w=2400 2400w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;figcaption&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Photo by &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/@aronvisuals?ref=cms.mirosh.dev&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Aron Visuals&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt; / &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/?utm_source=ghost&amp;#x26;utm_medium=referral&amp;#x26;utm_campaign=api-credit&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Unsplash&lt;/span&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;ol start=&quot;3&quot;&gt;&lt;li&gt;&lt;strong&gt;Race conditions. &lt;/strong&gt;By far the worst thing you can have in your app is a race condition. These things are incredibly hard to pin down. In one of the legacy projects I’ve worked on we once spent an entire week figuring out the root cause for a race condition. If your app starts having sporradic race conditions - this an equivalent of a “mayday” call on an aircraft.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;👉 Solution : While there are no silver bullets for this one approach is to utilize UDF to hoist the state upwards to a single source of truth.&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card kg-card-hascaption&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/photo-1592670129766-ed2d2a1de098.jpeg&quot; class=&quot;kg-image&quot; alt=&quot;zebra standing on brown field during daytime&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;1125&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/photo-1592670129766-ed2d2a1de098.jpeg 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/photo-1592670129766-ed2d2a1de098.jpeg 1000w, https://cms.mirosh.dev/content/images/size/w1600/2026/04/photo-1592670129766-ed2d2a1de098.jpeg 1600w, https://cms.mirosh.dev/content/images/2026/04/photo-1592670129766-ed2d2a1de098.jpeg 2000w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;figcaption&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Photo by &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/@valenciascott?ref=cms.mirosh.dev&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Neil and Zulma Scott&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt; / &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/?utm_source=ghost&amp;#x26;utm_medium=referral&amp;#x26;utm_campaign=api-credit&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Unsplash&lt;/span&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;ol start=&quot;4&quot;&gt;&lt;li&gt;&lt;strong&gt;Shadow Duplication. &lt;/strong&gt;Sometimes apps get so big and the team turnover is so fast, that knowledge doesn’t get passed on. I once worked in a bug in a token refresh functionality for a legacy project, only to discover that another part of the app in the background was doing the same thing - but with a different framework.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;👉 Solution : Modularization. Extract core business logic into a shared, platform-agnostic Kotlin module from the start. It makes duplication physically impossible.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card kg-card-hascaption&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/photo-1626417519184-c7b0aee144de-1.jpeg&quot; class=&quot;kg-image&quot; alt=&quot;red green and white candy cane&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;1395&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/photo-1626417519184-c7b0aee144de-1.jpeg 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/photo-1626417519184-c7b0aee144de-1.jpeg 1000w, https://cms.mirosh.dev/content/images/size/w1600/2026/04/photo-1626417519184-c7b0aee144de-1.jpeg 1600w, https://cms.mirosh.dev/content/images/2026/04/photo-1626417519184-c7b0aee144de-1.jpeg 2000w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;figcaption&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Photo by &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/@girlwithredhat?ref=cms.mirosh.dev&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Girl with red hat&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt; / &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/?utm_source=ghost&amp;#x26;utm_medium=referral&amp;#x26;utm_campaign=api-credit&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Unsplash&lt;/span&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;ol start=&quot;5&quot;&gt;&lt;li&gt;&lt;strong&gt;Tight coupling (Founder’s debt).  &lt;/strong&gt;And by tight - I mean it was solid as a rock tight, and not in the good SOLID sense. Whenever you tried changing something - it felt like you’re playing Jenga. I remember working on transportation app that was initially written by the founder of the company - while he was still learning how to code. Yep, the core of the app was written by a junior developer, and left untouched for 10 years. You can imagine the delivery “speed”, to put it mildly.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;👉 Solution : Thankfully, we have tools like DI tools like Koin and Hilt to fix these problems. It will take a great deal of untangling the code first, of course.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class=&quot;kg-card kg-image-card kg-card-hascaption&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2026/04/photo-1521178010706-baefe2334211.jpeg&quot; class=&quot;kg-image&quot; alt=&quot;praying hand neon signage&quot; loading=&quot;lazy&quot; width=&quot;1628&quot; height=&quot;1125&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2026/04/photo-1521178010706-baefe2334211.jpeg 600w, https://cms.mirosh.dev/content/images/size/w1000/2026/04/photo-1521178010706-baefe2334211.jpeg 1000w, https://cms.mirosh.dev/content/images/size/w1600/2026/04/photo-1521178010706-baefe2334211.jpeg 1600w, https://cms.mirosh.dev/content/images/2026/04/photo-1521178010706-baefe2334211.jpeg 1628w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;figcaption&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Photo by &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/@chrisliverani?ref=cms.mirosh.dev&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Chris Liverani&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt; / &lt;/span&gt;&lt;a href=&quot;https://unsplash.com/?utm_source=ghost&amp;#x26;utm_medium=referral&amp;#x26;utm_campaign=api-credit&quot;&gt;&lt;span style=&quot;white-space: pre-wrap;&quot;&gt;Unsplash&lt;/span&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;ol start=&quot;6&quot;&gt;&lt;li&gt;&lt;strong&gt;500k+ LOC with no tests. &lt;/strong&gt;Sadly, about 60-70% of legacy apps I’ve encountered didn’t have any tests. Such projects follow the “don’t touch it if it works” policy. But sooner or later you DO have to touch it if you want any features developed.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;👉 Solution : No quick fix here, but you need to stop the bleeding. Enforce strict CI/CD gates on test coverage for all new code. The best time to plant a tree was 10 years ago - the second best time is today.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The key point&lt;/strong&gt;: Every hour spent untangling code from a decade ago is an hour you aren&apos;t shipping features. My approach is to keep the code flexible with DI, modularize the core and establish clean architecture, share logic via KMP, and turn that Jenga tower into a foundation.&lt;/p&gt;&lt;p&gt;Which of these &quot;Horrors&quot; is currently lurking in your production app? Let&apos;s discuss in the comments.&lt;/p&gt;</content:encoded></item><item><title>After a year of work (yes, a full year), my Kotlin Multiplatform course is finally ready!</title><link>https://mirosh.dev/after-a-year-of-work-yes-a-full-year-my-kotlin-multiplatform-course-is-finally-ready/</link><guid isPermaLink="true">https://mirosh.dev/after-a-year-of-work-yes-a-full-year-my-kotlin-multiplatform-course-is-finally-ready/</guid><description>This course is designed specifically for Android developers who want to move into KMP in a structured way. If you’ve been thinking about getting into KMP, this is exactly the course I wish I had when I started.

You can check it out here:

https://learnkmp.com

Also, if you want to receive monthly tips on KMP &amp; Android to your inbox, you can join my newsletter and you will get a 15% discount on the course if you join now:

https://subscribepage.io/ODZYLC

Happy to answer any questions 🙂 promo v</description><pubDate>Thu, 19 Mar 2026 10:36:25 GMT</pubDate><content:encoded>&lt;p&gt;This course is designed specifically for Android developers who want to move into KMP in a structured way. If you’ve been thinking about getting into KMP, this is exactly the course I wish I had when I started.&lt;br&gt;&lt;br&gt;You can check it out here:&lt;br&gt;&lt;br&gt;&lt;a href=&quot;https://learnkmp.com/?ref=cms.mirosh.dev&quot;&gt;https://learnkmp.com&lt;/a&gt;&lt;br&gt;&lt;br&gt;Also, if you want to receive monthly tips on KMP &amp;#x26; Android to your inbox, you can join my newsletter and you will get a 15% discount on the course if you join now: &lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://subscribepage.io/ODZYLC?ref=cms.mirosh.dev&quot;&gt;https://subscribepage.io/ODZYLC&lt;/a&gt;&lt;br&gt;&lt;br&gt;Happy to answer any questions 🙂  promo video below 👇&lt;/p&gt;&lt;figure class=&quot;kg-card kg-embed-card&quot;&gt;&lt;iframe width=&quot;200&quot; height=&quot;113&quot; src=&quot;https://www.youtube.com/embed/naNRZIOz14k?feature=oembed&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen title=&quot;KMP course with caption 720p&quot; loading=&quot;lazy&quot;&gt;&lt;/iframe&gt;&lt;/figure&gt;</content:encoded></item><item><title>Be careful with commonMain dependencies in Compose Multiplatform!</title><link>https://mirosh.dev/be-careful-with-commonmain-dependencies-in-compose-multiplatform/</link><guid isPermaLink="true">https://mirosh.dev/be-careful-with-commonmain-dependencies-in-compose-multiplatform/</guid><description>I’ve recently had a problem with importing a dependency in commonMain in CMP. More specifically I was trying out the new navigation3 component. Since I first tried out Nav3 in Android - I blindly copied over the dependency to my CMP project.

My Android CMP build was working fine - and I was happy. However, I decided to give my other target a run - specifically the iOS one. I was very surprised when I couldn’t make the project compile.

I quadruple-checked everything, cleaned gradle cache, and r</description><pubDate>Wed, 10 Dec 2025 09:57:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve recently had a problem with importing a dependency in commonMain in CMP. More specifically I was trying out the new navigation3 component. Since I first tried out Nav3 in Android - I blindly copied over the dependency to my CMP project.&lt;/p&gt;&lt;p&gt;My Android CMP build was working fine - and I was happy. However, I decided to give my other target a run - specifically the iOS one. I was very surprised when I couldn’t make the project compile.&lt;/p&gt;&lt;p&gt;I quadruple-checked everything, cleaned gradle cache, and restarted the project around 5 times - but it just wouldn’t compile. So I went checking everything line by line.&lt;/p&gt;&lt;p&gt;And then I saw it:&lt;/p&gt;&lt;p&gt;I was importing an Android dependency in commonMain.&lt;/p&gt;&lt;p&gt;This seemingly minor mistake - can create a lot of pain if you’re not careful. An Android dependency that is put into commonMain.dependencies{} will resolve without issues… on Android. But the same project won’t compile when you try to launch iOS.&lt;/p&gt;&lt;p&gt;Sooo… if you’re working with CMP / KMP - make sure you’re not putting platform-specific libraries into commonMain!&lt;/p&gt;</content:encoded></item><item><title>Make your Gradle builds and Android Studio faster</title><link>https://mirosh.dev/make-your-gradle-builds-and-android-studio-faster/</link><guid isPermaLink="true">https://mirosh.dev/make-your-gradle-builds-and-android-studio-faster/</guid><description>Recently I&apos;ve had some issues with my Android Studio - it was lagging, freezing, etc., which got me thinking about how I can improve the speed of my builds and Android Studio in general.

Here are the settings that I applied to my Gradle builds (global ones at ~/.gradle/gradle.properties), and so far they&apos;ve been working like charm.

Below are the settings of my Gradle. My set up is M1 Max with 64 GB RAM.

What settings do you have to improve performance?</description><pubDate>Fri, 24 Oct 2025 08:29:47 GMT</pubDate><content:encoded>&lt;p&gt;Recently I&apos;ve had some issues with my Android Studio - it was lagging, freezing, etc., which got me thinking about how I can improve the speed of my builds and Android Studio in general. &lt;br&gt;&lt;br&gt;Here are the settings that I applied to my Gradle builds (global ones at &lt;code&gt;~/.gradle/&lt;/code&gt;&lt;a href=&quot;http://gradle.properties/?ref=cms.mirosh.dev&quot;&gt;&lt;code&gt;gradle.properties&lt;/code&gt;&lt;/a&gt;), and so far they&apos;ve been working like charm. &lt;br&gt;&lt;br&gt;Below are the settings of my Gradle.  My set up is M1 Max with 64 GB RAM. &lt;/p&gt;&lt;p&gt;What settings do you have to improve performance? &lt;/p&gt;
&lt;!--kg-card-begin: html--&gt;
&lt;script src=&quot;https://gist.github.com/nsmirosh/da6b60e69c784ad1d63b0485e381d60c.js&quot;&gt;&lt;/script&gt;
&lt;!--kg-card-end: html--&gt;
</content:encoded></item><item><title>Avoid vibe-coded services like a plague for important projects.</title><link>https://mirosh.dev/avoid-vibe-coded-services-like-a-plague-for-important-projects/</link><guid isPermaLink="true">https://mirosh.dev/avoid-vibe-coded-services-like-a-plague-for-important-projects/</guid><description>I was using a certain service for my course with which I was explaining the networking part of KMP. And then a couple of weeks back the service stopped working. I looked into the issues on Github - the owner of the service said they were attacked constantly (because the service didn’t have authentication) so they’re taking steps to integrate protection. Sounds fine to me.

So the service started working again after a couple of days / week. But recently I started getting weird errors once again.</description><pubDate>Fri, 24 Oct 2025 08:19:29 GMT</pubDate><content:encoded>&lt;p&gt;I was using a certain service for my course with which I was explaining the networking part of KMP. And then a couple of weeks back the service stopped working. I looked into the issues on Github - the owner of the service said they were attacked constantly (because the service didn’t have authentication) so they’re taking steps to integrate protection. Sounds fine to me.&lt;/p&gt;&lt;p&gt;So the service started working again after a couple of days / week. But recently I started getting weird errors once again. I thought “huh, that’s strange” - so I started looking through the website and found the answer to my problem in the FAQ.&lt;/p&gt;&lt;p&gt;I followed the instructions, but requests began failing on the website as well.&lt;/p&gt;&lt;p&gt;And then I saw it…&lt;/p&gt;&lt;p&gt;At the very bottom of the page it said:&lt;/p&gt;&lt;p&gt;“Vibe coded with 💜”&lt;/p&gt;&lt;p&gt;I immediately knew I had to get rid of this service in my course or suffer the consequences. Idk what was the point of putting this message at the bottom of the page - to avoid responsibility? But either way I knew the things would go downhill from here.&lt;/p&gt;&lt;p&gt;The sad part is - this service has been up for a good 5 years, so it was not vibe coded in the beginning. But I guess the temptation is too much to use slop.&lt;/p&gt;</content:encoded></item><item><title>What is a circular dependency, and how can you resolve it? 🤔</title><link>https://mirosh.dev/what-is-a-circular-dependency-and-how-can-you-resolve-it/</link><guid isPermaLink="true">https://mirosh.dev/what-is-a-circular-dependency-and-how-can-you-resolve-it/</guid><description>If you’re new to DI, whether it’s Hilt or Koin - sooner or later, you’ll encounter a circular dependency (I hope you don’t, but let’s assume you will 😢).

This post is an explanation of a challenge I posted two days ago

So what is a circular dependency? 🤔

According to Wikipedia:

A circular dependency (or cyclic dependency) is a relation between two or more modules / classes which either directly or indirectly depend on each other to function properly

🔄 Essentially, it’s an infinite loop,</description><pubDate>Tue, 21 Oct 2025 11:05:09 GMT</pubDate><content:encoded>&lt;p&gt; If you’re new to DI, whether it’s Hilt or Koin - sooner or later, you’ll encounter a circular dependency (I hope you don’t, but let’s assume you will 😢).&lt;br&gt;&lt;br&gt;This post is an explanation of a &lt;a href=&quot;https://mirosh.dev/can-you-spot-the-issue-in-this-koin-code/&quot;&gt;challenge I posted two days ago&lt;/a&gt; &lt;/p&gt;&lt;p&gt;So what is a circular dependency? 🤔&lt;/p&gt;&lt;p&gt;According to Wikipedia:&lt;/p&gt;&lt;blockquote&gt;A circular dependency (or cyclic dependency) is a relation between two or more modules / classes which either directly or indirectly depend on each other to function properly&lt;/blockquote&gt;&lt;p&gt;🔄 Essentially, it’s an infinite loop, because when one class requires another class at creation time and vice versa, you have a never-ending cycle.&lt;/p&gt;&lt;p&gt;How do you know you’ve run into a circular dependency? You will probably get a &lt;code&gt;StackOverflowException&lt;/code&gt; because of the never-ending invocation of the class constructor. 💥&lt;/p&gt;&lt;p&gt;How can you resolve or detect it? The best way is to not have one in the first place. And Clean Architecture helps a lot here. See the carousel below for an explanation.&lt;/p&gt;&lt;p&gt;Are there ways to detect circular dependencies if you already have one? Yes, but it’s rather painful (and I might cover that later) - so it’s best to avoid them altogether.&lt;/p&gt;&lt;figure class=&quot;kg-card kg-gallery-card kg-width-wide&quot;&gt;&lt;div class=&quot;kg-gallery-container&quot;&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/10/1-3.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/10/1-3.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/10/1-3.png 1000w, https://cms.mirosh.dev/content/images/2025/10/1-3.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/10/2-3.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/10/2-3.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/10/2-3.png 1000w, https://cms.mirosh.dev/content/images/2025/10/2-3.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;</content:encoded></item><item><title>Can you spot the issue in this Koin code?</title><link>https://mirosh.dev/can-you-spot-the-issue-in-this-koin-code/</link><guid isPermaLink="true">https://mirosh.dev/can-you-spot-the-issue-in-this-koin-code/</guid><description>In the code below we have initialization of a Koin module along with the declaration of dependencies it injects. 💉

🔍 Your task is to find the issue and share it in the comments! I&apos;ll share a post with an explanation in two days.

💡 Hint: this problem applies to all DI frameworks.</description><pubDate>Tue, 21 Oct 2025 11:00:35 GMT</pubDate><content:encoded>&lt;p&gt;In the code below we have initialization of a Koin module along with the declaration of dependencies it injects. 💉&lt;br&gt;&lt;br&gt; 🔍 Your task is to find the issue and share it in the comments! I&apos;ll share a post with an explanation in two days. &lt;/p&gt;&lt;p&gt;💡 Hint: this problem applies to all DI frameworks. &lt;/p&gt;&lt;figure class=&quot;kg-card kg-gallery-card kg-width-wide&quot;&gt;&lt;div class=&quot;kg-gallery-container&quot;&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/10/1-2.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/10/1-2.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/10/1-2.png 1000w, https://cms.mirosh.dev/content/images/2025/10/1-2.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/10/2-2.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/10/2-2.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/10/2-2.png 1000w, https://cms.mirosh.dev/content/images/2025/10/2-2.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;</content:encoded></item><item><title>Memory leaks cost you money. Literally.</title><link>https://mirosh.dev/memory-leaks-cost-you-money-literally/</link><guid isPermaLink="true">https://mirosh.dev/memory-leaks-cost-you-money-literally/</guid><description>As mobile devs, we often don’t care how much memory our app uses — as long as we don’t hit the dreaded OutOfMemory exception.

Let’s be honest: we do care as professionals, but if the app consumes 100 MB more than it should, it doesn’t hurt us. The app runs on our users’ phones — we’re not the ones paying for the extra RAM.

Well… I recently learned to care the hard way with my Telegram Finance Bot.

Yesterday, Railway messaged me that my bill doubled over the last two weeks.

I got curious - wh</description><pubDate>Tue, 14 Oct 2025 08:54:27 GMT</pubDate><content:encoded>&lt;p&gt;As mobile devs, we often don’t care &lt;em&gt;how much&lt;/em&gt; memory our app uses — as long as we don’t hit the dreaded &lt;code&gt;OutOfMemory&lt;/code&gt; exception.&lt;/p&gt;&lt;p&gt;Let’s be honest: we &lt;em&gt;do&lt;/em&gt; care as professionals, but if the app consumes 100 MB more than it should, it doesn’t hurt &lt;em&gt;us&lt;/em&gt;. The app runs on our users’ phones — we’re not the ones paying for the extra RAM.&lt;/p&gt;&lt;p&gt;Well… I recently learned to care the hard way with my Telegram Finance Bot.&lt;/p&gt;&lt;p&gt;Yesterday, Railway messaged me that my bill doubled over the last two weeks. &lt;/p&gt;&lt;p&gt;I got curious - what’s causing this? Memory usage more than doubled over a couple of days - even though I didn’t introduce any big features or libraries. So I started digging…&lt;/p&gt;&lt;p&gt;My first suspicion was that I have a memory leak somewhere . In Android we have the benefit of coroutine scopes that are scoped to a certain component - like &lt;code&gt;viewModelScope&lt;/code&gt;, &lt;code&gt;lifecycleScope&lt;/code&gt;, etc. These scopes do the clean up magic behind the scenes. If the component is destroyed - so is everything inside that scope. A graceful, neat solution.&lt;/p&gt;&lt;p&gt;Well, in a pure Kotlin app you have to manage that yourself. In my bot I would start up &lt;code&gt;CoroutineScopes&lt;/code&gt; for each operation and didn’t think twice of it. My thinking was - well, as long as everything finishes inside that scope - the memory will be released. And that is not wrong, but you have to be very careful with what’s going on inside the lambda.&lt;/p&gt;&lt;p&gt;Each scope created like this (&lt;code&gt;CoroutineScope(Dispatchers.IO).launch { ... }&lt;/code&gt;) lives independently — if you forget to cancel it or it captures large references, they’ll stay in memory long after you expect them to be gone&lt;/p&gt;&lt;p&gt;To be honest, I didn’t find the exact leak — but I decided to optimize my app anyway.&lt;/p&gt;&lt;p&gt;Here’s what helped cut memory usage in half 👇&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Made my data layer a singleton with Koin. Singletons help prevent redundant instantiations, but you also need to ensure they don’t themselves hold onto stale data forever.&lt;/li&gt;&lt;li&gt;I switched some &lt;code&gt;StateFlow&lt;/code&gt;s to &lt;code&gt;SharedFlow&lt;/code&gt;s where I didn’t need to retain the last emitted value — &lt;code&gt;StateFlow&lt;/code&gt; always holds one, which can keep large objects in memory unnecessarily.&lt;/li&gt;&lt;li&gt;Created my own application-wide &lt;code&gt;CoroutineScope&lt;/code&gt; with &lt;code&gt;SupervisorJob&lt;/code&gt; instead of having dozens of independent &lt;code&gt;CoroutineScope&lt;/code&gt;s running wild.&lt;/li&gt;&lt;li&gt;Removed unnecessary logging in production&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;After these changes, memory dropped (see the graph), and my Railway bill went down too.&lt;/p&gt;&lt;figure class=&quot;kg-card kg-gallery-card kg-width-wide&quot;&gt;&lt;div class=&quot;kg-gallery-container&quot;&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 2.0947 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/10/Screenshot-2025-10-14-at-14.31.01.png&quot; width=&quot;1592&quot; height=&quot;760&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/10/Screenshot-2025-10-14-at-14.31.01.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/10/Screenshot-2025-10-14-at-14.31.01.png 1000w, https://cms.mirosh.dev/content/images/2025/10/Screenshot-2025-10-14-at-14.31.01.png 1592w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.3184 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/10/Screenshot-2025-10-14-at-14.31.22.png&quot; width=&quot;944&quot; height=&quot;716&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/10/Screenshot-2025-10-14-at-14.31.22.png 600w, https://cms.mirosh.dev/content/images/2025/10/Screenshot-2025-10-14-at-14.31.22.png 944w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Moral of the story:&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Don’t be reckless with memory. Sloppiness can cost you real money — especially outside mobile apps.&lt;/p&gt;</content:encoded></item><item><title>I just published a new video on Kotlinx Serialization + Ktor 🚀</title><link>https://mirosh.dev/i-just-published-a-new-video-on-kotlinx-serialization-ktor/</link><guid isPermaLink="true">https://mirosh.dev/i-just-published-a-new-video-on-kotlinx-serialization-ktor/</guid><description>I just published a new video on Kotlinx Serialization + Ktor 🚀

In this video, I&apos;ll be covering:
✔️ What Kotlinx Serialization is
✔️ How does serialization work in general
✔️ The advantages over other libraries like Moshi, Jackson, and Gson.
✔️ Some basic features of Kotlinx Serialization
✔️ 💻 Additionally, we&apos;ll build a KMP note-taking app that makes actual network requests and serializes data.</description><pubDate>Sun, 12 Oct 2025 07:13:00 GMT</pubDate><content:encoded>&lt;p&gt;I just published a new video on Kotlinx Serialization + Ktor 🚀 &lt;br&gt;&lt;br&gt;In this video, I&apos;ll be covering: &lt;br&gt;✔️ What Kotlinx Serialization is&lt;br&gt;✔️ How does serialization work in general&lt;br&gt;✔️ The advantages over other libraries like Moshi, Jackson, and Gson. &lt;br&gt;✔️ Some basic features of Kotlinx Serialization &lt;br&gt;✔️ 💻 Additionally, we&apos;ll build a KMP note-taking app that makes actual network requests and serializes data. &lt;/p&gt;&lt;figure class=&quot;kg-card kg-embed-card&quot;&gt;&lt;iframe width=&quot;200&quot; height=&quot;113&quot; src=&quot;https://www.youtube.com/embed/lY78qeHIJds?feature=oembed&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen title=&quot;Ktor + Kotlinx Serialization: Build a Note-Taking App (KMP Tutorial)&quot; loading=&quot;lazy&quot;&gt;&lt;/iframe&gt;&lt;/figure&gt;</content:encoded></item><item><title>What if I told you that you don’t need to use expect/actual in Kotlin Multiplatform?</title><link>https://mirosh.dev/what-if-i-told-you-that-you-dont-need-to-use-expect-actual-in-kotlin-multiplatform/</link><guid isPermaLink="true">https://mirosh.dev/what-if-i-told-you-that-you-dont-need-to-use-expect-actual-in-kotlin-multiplatform/</guid><description>Platform-specific Koin modules can do the job better.

Expect/actual is great when your project is small. However, as your codebase grows, managing platform-specific dependencies quickly becomes a headache.

That’s where Koin modules shine. 💪 They let you replace expect/actual with a more flexible and scalable approach — keeping your architecture nice and clean.

Here&apos;s my KMP project where I implemented this approach.

See the slides with explanation below.</description><pubDate>Sun, 12 Oct 2025 07:06:22 GMT</pubDate><content:encoded>&lt;p&gt;Platform-specific Koin modules can do the job better. &lt;br&gt;&lt;br&gt;Expect/actual is great when your project is small. However, as your codebase grows, managing platform-specific dependencies quickly becomes a headache. &lt;br&gt;&lt;br&gt;That’s where Koin modules shine. 💪 They let you replace expect/actual with a more flexible and scalable approach — keeping your architecture nice and clean. &lt;br&gt;&lt;br&gt;Here&apos;s &lt;a href=&quot;https://github.com/nsmirosh/NewsAppKMP?ref=cms.mirosh.dev&quot;&gt;my KMP project&lt;/a&gt; where I implemented this approach. &lt;/p&gt;&lt;p&gt;See the slides with explanation below. &lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class=&quot;kg-card kg-gallery-card kg-width-wide&quot;&gt;&lt;div class=&quot;kg-gallery-container&quot;&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/10/1.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/10/1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/10/1.png 1000w, https://cms.mirosh.dev/content/images/2025/10/1.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/10/2.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/10/2.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/10/2.png 1000w, https://cms.mirosh.dev/content/images/2025/10/2.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/10/3.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/10/3.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/10/3.png 1000w, https://cms.mirosh.dev/content/images/2025/10/3.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/10/4.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/10/4.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/10/4.png 1000w, https://cms.mirosh.dev/content/images/2025/10/4.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;</content:encoded></item><item><title>How I traumatized myself by storing all the data for my app locally.</title><link>https://mirosh.dev/how-i-traumatized-myself-by-storing-all-the-data-for-my-app-locally/</link><guid isPermaLink="true">https://mirosh.dev/how-i-traumatized-myself-by-storing-all-the-data-for-my-app-locally/</guid><description>Back when I started programming 10 years ago, I traumatized myself by storing all the data for my financial app locally.

Yes, you read that right — I had an app in production, and all of its data lived on the device.It was my first app, called &quot;Daily Budget&quot;, and I uploaded it to Google Play just in case someone else was facing the same money-tracking struggles I was.

To my amazement, it got around 10,000 downloads. But what those poor souls didn’t know is that all of their data was stored onl</description><pubDate>Sun, 12 Oct 2025 06:59:41 GMT</pubDate><content:encoded>&lt;p&gt;Back when I started programming 10 years ago, I traumatized myself by storing all the data for my financial app locally.&lt;br&gt;&lt;br&gt;Yes, you read that right — I had an app in production, and all of its data lived on the device.It was my first app, called &quot;Daily Budget&quot;, and I uploaded it to Google Play just in case someone else was facing the same money-tracking struggles I was.&lt;br&gt;&lt;br&gt;To my amazement, it got around 10,000 downloads. But what those poor souls didn’t know is that all of their data was stored only on their phone. &lt;br&gt;&lt;br&gt;What followed were man-made horrors beyond comprehension. &lt;br&gt;&lt;br&gt;Encouraged by this “success,” I kept adding more features and expanding the app. As my knowledge grew — especially around databases and best practices — so did my desire to fix things. But since everything was local, I had to migrate the data manually on the device. This was back in the days when you had to write raw SQLite queries yourself. &lt;br&gt;&lt;br&gt;I did around 14 migrations before the app started to collapse under its own weight.&lt;br&gt;&lt;br&gt;Keeping track of the users’ data state became impossible. I couldn’t inspect the data — I just had to pray the next migration didn’t break everything. &lt;br&gt;&lt;br&gt;I walked away from that app with a lot of lessons learned (and a fair number of angry users 😅).&lt;br&gt;&lt;br&gt;Biggest lesson:&lt;br&gt; 👉 Don’t make early architectural decisions that are written in stone. &lt;br&gt;👉 If you must make a suboptimal choice, make sure it’s reversible later.&lt;br&gt;&lt;br&gt;Otherwise… you’re going to have a bad time.&lt;/p&gt;</content:encoded></item><item><title>Code coverage is a bad metric</title><link>https://mirosh.dev/code-coverage-is-a-bad-metric/</link><guid isPermaLink="true">https://mirosh.dev/code-coverage-is-a-bad-metric/</guid><description>0:00

/0:58


1×

















100% test coverage… sounds safe, right? Wrong. Here’s why your coverage report might be lying to you:

Unfortunately, test coverage does not accurately reflect the extent to which the code is tested. All it does is show the amount of lines of code executed. It does not indicate that the functionality under test is actually correct.

In fact, you can achieve 100% of test coverage without actually testing anything. How? By simply not doing a</description><pubDate>Wed, 10 Sep 2025 19:05:15 GMT</pubDate><content:encoded>&lt;figure class=&quot;kg-card kg-video-card kg-width-regular&quot; data-kg-thumbnail=&quot;https://cms.mirosh.dev/content/images/media-thumbnails/2025/09/coverage-metrics_thumb.jpg&quot; data-kg-custom-thumbnail=&quot;&quot;&gt;
            &lt;div class=&quot;kg-video-container&quot;&gt;
                &lt;video src=&quot;https://cms.mirosh.dev/content/media/2025/09/coverage-metrics.mp4&quot; poster=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; width=&quot;1200&quot; height=&quot;1200&quot; playsinline preload=&quot;metadata&quot; style=&quot;aspect-ratio: 1200 / 1200; background: transparent url(&amp;#x27;https://cms.mirosh.dev/content/images/media-thumbnails/2025/09/coverage-metrics_thumb.jpg&amp;#x27;) 50% 50% / cover no-repeat;&quot;&gt;&lt;/video&gt;
                &lt;div class=&quot;kg-video-overlay&quot;&gt;
                    &lt;button class=&quot;kg-video-large-play-icon&quot; aria-label=&quot;Play video&quot;&gt;
                        &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 24 24&quot;&gt;
                            &lt;path d=&quot;M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z&quot;&gt;&lt;/path&gt;
                        &lt;/svg&gt;
                    &lt;/button&gt;
                &lt;/div&gt;
                &lt;div class=&quot;kg-video-player-container&quot;&gt;
                    &lt;div class=&quot;kg-video-player&quot;&gt;
                        &lt;button class=&quot;kg-video-play-icon&quot; aria-label=&quot;Play video&quot;&gt;
                            &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 24 24&quot;&gt;
                                &lt;path d=&quot;M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z&quot;&gt;&lt;/path&gt;
                            &lt;/svg&gt;
                        &lt;/button&gt;
                        &lt;button class=&quot;kg-video-pause-icon kg-video-hide&quot; aria-label=&quot;Pause video&quot;&gt;
                            &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 24 24&quot;&gt;
                                &lt;rect x=&quot;3&quot; y=&quot;1&quot; width=&quot;7&quot; height=&quot;22&quot; rx=&quot;1.5&quot; ry=&quot;1.5&quot;&gt;&lt;/rect&gt;
                                &lt;rect x=&quot;14&quot; y=&quot;1&quot; width=&quot;7&quot; height=&quot;22&quot; rx=&quot;1.5&quot; ry=&quot;1.5&quot;&gt;&lt;/rect&gt;
                            &lt;/svg&gt;
                        &lt;/button&gt;
                        &lt;span class=&quot;kg-video-current-time&quot;&gt;0:00&lt;/span&gt;
                        &lt;div class=&quot;kg-video-time&quot;&gt;
                            /&lt;span class=&quot;kg-video-duration&quot;&gt;0:58&lt;/span&gt;
                        &lt;/div&gt;
                        &lt;input type=&quot;range&quot; class=&quot;kg-video-seek-slider&quot; max=&quot;100&quot; value=&quot;0&quot;&gt;
                        &lt;button class=&quot;kg-video-playback-rate&quot; aria-label=&quot;Adjust playback speed&quot;&gt;1×&lt;/button&gt;
                        &lt;button class=&quot;kg-video-unmute-icon&quot; aria-label=&quot;Unmute&quot;&gt;
                            &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 24 24&quot;&gt;
                                &lt;path d=&quot;M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z&quot;&gt;&lt;/path&gt;
                            &lt;/svg&gt;
                        &lt;/button&gt;
                        &lt;button class=&quot;kg-video-mute-icon kg-video-hide&quot; aria-label=&quot;Mute&quot;&gt;
                            &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 24 24&quot;&gt;
                                &lt;path d=&quot;M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z&quot;&gt;&lt;/path&gt;
                            &lt;/svg&gt;
                        &lt;/button&gt;
                        &lt;input type=&quot;range&quot; class=&quot;kg-video-volume-slider&quot; max=&quot;100&quot; value=&quot;100&quot;&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            
        &lt;/figure&gt;&lt;p&gt;100% test coverage… sounds safe, right? Wrong. Here’s why your coverage report might be lying to you: &lt;/p&gt;&lt;p&gt;Unfortunately, test coverage does not accurately reflect the extent to which the code is tested. All it does is show the amount of lines of code executed. It does not indicate that the functionality under test is actually correct.&lt;/p&gt;&lt;p&gt;In fact, you can achieve 100% of test coverage without actually testing anything. How? By simply not doing assertions in your tests, but still invoking the methods.&lt;/p&gt;&lt;p&gt;Test coverage is a good negative metric, but a bad positive one.It&apos;s useful for highlighting gaps (e.g., 0% coverage signals untested code), but it can be misleading when used to claim thorough testing.&lt;/p&gt;&lt;p&gt;An improvement over line coverage is branch coverage. The branch report indicates the amount of conditional statements traversed. This metric is harder to “trick” in comparison to line coverage. &lt;/p&gt;&lt;p&gt;However, even branch coverage does not guarantee the quality of your tests. The only way to do it is to actually write the proper assertions that verify relevant outcomes.&lt;/p&gt;&lt;p&gt;🚨So, don’t be fooled by high test coverage, it does not guarantee in any way that the code is safe. Write tests that verify the needed outcomes and run them often, preferrably on your CI / CD. &lt;/p&gt;&lt;p&gt;Focus on writing meaningful tests with solid assertions. High coverage is nice, but it’s quality testing that keeps your code safe.&lt;/p&gt;</content:encoded></item><item><title>Things I have never shared in public, part 1.</title><link>https://mirosh.dev/things-i-have-never-shared-in-public-part-1/</link><guid isPermaLink="true">https://mirosh.dev/things-i-have-never-shared-in-public-part-1/</guid><description>If I were to judge my technical level - I would say I’m an average engineer. I’m a senior engineer, but this seniority comes from experience, grueling + slow studying and a billion mistakes I made along the way - not because I learned quickly or can comprehend complex ideas easily. I’ve always been amazed and jealous of people who could understand hard things swiftly.

Very often, I would get roles where I felt I wasn’t ready and had to learn new things on the job. I would also get stressed beca</description><pubDate>Fri, 29 Aug 2025 18:38:18 GMT</pubDate><content:encoded>&lt;p&gt;If I were to judge my technical level - I would say I’m an average engineer. I’m a senior engineer, but this seniority comes from experience, grueling + slow studying and a billion mistakes I made along the way - not because I learned quickly or can comprehend complex ideas easily. I’ve always been amazed and jealous of people who could understand hard things swiftly.&lt;/p&gt;&lt;p&gt;Very often, I would get roles where I felt I wasn’t ready and had to learn new things on the job. I would also get stressed because I didn’t want people to “find out” I’m not what I sold myself as.&lt;/p&gt;&lt;p&gt;I have failed many many times during interviews and had people actually walk out on me. I have probably failed around 80% of the DSA interviews I’ve had.&lt;/p&gt;&lt;p&gt;I was mediocre at math in school (once the math started getting hard) and I have started learning programming only when I turned 22 (my university major is Economics, not Computer Science).&lt;/p&gt;&lt;p&gt;I got my first job in IT in an outsourcing company. For the most part, I got it because I spoke really good English - and as I was told later by my boss, I was an “easy sell” to foreign clients. I sounded “smarter” than my peers who were technically more proficient, but had worse English.&lt;/p&gt;&lt;p&gt;And I got really good at English, not because I studied hard, but because my mom won a program that allowed her to study in the US for 2 years. As a child, I came along with her and simply “absorbed” the language.&lt;/p&gt;&lt;p&gt;So, just in case I’m giving off a vibe of someone who “has stuff going for him” -the reality is far from it :)&lt;/p&gt;&lt;p&gt;Why am I writing about this? Because I also have perfectionism (not the good kind), and it’s holding me back in many ways. And this is my way of battling it and confronting my fears of not being perfect in front of my peers.&lt;/p&gt;&lt;p&gt;P.S. I&apos;m not completely disregarding my achievements - but just saying that I&apos;m far from perfect.&lt;/p&gt;</content:encoded></item><item><title>Understanding engine differences, limitations and configurations + KMP integration.</title><link>https://mirosh.dev/understanding-engine-differences-limitations-and-configurations-kmp-integration/</link><guid isPermaLink="true">https://mirosh.dev/understanding-engine-differences-limitations-and-configurations-kmp-integration/</guid><pubDate>Fri, 29 Aug 2025 16:47:49 GMT</pubDate><content:encoded>&lt;p&gt;&lt;/p&gt;&lt;figure class=&quot;kg-card kg-embed-card&quot;&gt;&lt;iframe width=&quot;200&quot; height=&quot;113&quot; src=&quot;https://www.youtube.com/embed/aSA6Antpuwc?feature=oembed&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen title=&quot;Ktor engines. Understanding engine differences, limitations and configurations + KMP integration.&quot; loading=&quot;lazy&quot;&gt;&lt;/iframe&gt;&lt;/figure&gt;</content:encoded></item><item><title>Comparing Flutter, KMP, and React Native in 2025. How They Work. Advantages and Disadvantages.</title><link>https://mirosh.dev/i-just-published-a-video-where-im-comparing-flutter-kmp-and-react-native/</link><guid isPermaLink="true">https://mirosh.dev/i-just-published-a-video-where-im-comparing-flutter-kmp-and-react-native/</guid><pubDate>Tue, 26 Aug 2025 09:13:51 GMT</pubDate><content:encoded>&lt;figure class=&quot;kg-card kg-embed-card&quot;&gt;&lt;iframe width=&quot;200&quot; height=&quot;113&quot; src=&quot;https://www.youtube.com/embed/xYKEKcpIUgg?feature=oembed&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen title=&quot;Comparing Flutter, KMP, and React Native in 2025. How They Work. Advantages and Disadvantages.&quot; loading=&quot;lazy&quot;&gt;&lt;/iframe&gt;&lt;/figure&gt;</content:encoded></item><item><title>Instead of looking for that latest AI tool, learn things that genuinely make you productive.</title><link>https://mirosh.dev/instead-of-looking-for-that-latest-ai-tool-learn-things-that-genuinely-make-you-productive/</link><guid isPermaLink="true">https://mirosh.dev/instead-of-looking-for-that-latest-ai-tool-learn-things-that-genuinely-make-you-productive/</guid><description>1. Learn how to blind type. If you’re still looking down at your keyboard when typing - you are much slower than you can be. There&apos;s a bunch of websites for this. Here is one, for example.
 2. Learn Vim (or emacs). Ever since I learned VIM, I rarely touch the mouse. Editing, navigation, commenting, copying, deleting - Vim is the answer There’s a website where you can practice that too - vimgolf.com . There&apos;s also a wonderful IdeaVIM plugin that you can use in AS and IntelliJ.
 3. Learn IDE shor</description><pubDate>Fri, 08 Aug 2025 18:21:07 GMT</pubDate><content:encoded>&lt;ol&gt;&lt;li&gt; Learn how to blind type. If you’re still looking down at your keyboard when typing - you are much slower than you can be. There&apos;s a bunch of websites for this. &lt;a href=&quot;https://www.typingclub.com/?ref=cms.mirosh.dev&quot;&gt;Here is one&lt;/a&gt;, for example.&lt;/li&gt;&lt;li&gt;Learn Vim (or emacs). Ever since I learned VIM, I rarely touch the mouse. Editing, navigation, commenting, copying, deleting - Vim is the answer There’s a website where you can practice that too - &lt;a href=&quot;http://vimgolf.com/?ref=cms.mirosh.dev&quot;&gt;vimgolf.com&lt;/a&gt; . There&apos;s also a wonderful IdeaVIM plugin that you can use in AS and IntelliJ. &lt;/li&gt;&lt;li&gt;Learn IDE shortcuts. You can use standard ones, or assign them yourself - whatever works for you. &lt;a href=&quot;https://developer.android.com/studio/intro/keyboard-shortcuts?ref=cms.mirosh.dev&quot;&gt;Here are AS shortcuts &lt;/a&gt;. &lt;/li&gt;&lt;/ol&gt;&lt;p&gt;These are the things that improved my productivity. What about you? Anything that has increased your productivity?&lt;/p&gt;</content:encoded></item><item><title>We&apos;ve all heard of extension functions in Kotlin, but have you used an infix function?</title><link>https://mirosh.dev/weve-all-heard-of-extension-functions-in-kotlin-but-have-you-used-an-infix-function/</link><guid isPermaLink="true">https://mirosh.dev/weve-all-heard-of-extension-functions-in-kotlin-but-have-you-used-an-infix-function/</guid><description>An infix function provides better readability for operations that require two arguments/objects.

It also operates the same way as an extension function. This means you can reference the fields of the receiver (in this example it&apos;s the Session class) implicitly without calling &quot;this&quot;.

In the example below, I use the belongTo() function both with and without infix notation. As you can see, the infix function removes the extra parentheses. Infix makes expressions a bit more readable, and it can m</description><pubDate>Thu, 07 Aug 2025 17:29:18 GMT</pubDate><content:encoded>&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/08/websiteinfix.png&quot; class=&quot;kg-image&quot; alt=&quot;&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;1200&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/08/websiteinfix.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/08/websiteinfix.png 1000w, https://cms.mirosh.dev/content/images/2025/08/websiteinfix.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/figure&gt;&lt;p&gt;An infix function provides better readability for operations that require two arguments/objects. &lt;br&gt;&lt;br&gt;It also operates the same way as an extension function. This means you can reference the fields of the receiver (in this example it&apos;s the &lt;code&gt;Session&lt;/code&gt; class) implicitly without calling &quot;this&quot;. &lt;/p&gt;&lt;p&gt;In the example below, I use the &lt;code&gt;belongTo()&lt;/code&gt; function both with and without &lt;code&gt;infix&lt;/code&gt; notation. As you can see, the infix function removes the extra parentheses. Infix makes expressions a bit more readable, and it can make a difference in a large codebase. &lt;br&gt;The only requirement for the &lt;code&gt;infix&lt;/code&gt; function is that you &lt;strong&gt;must&lt;/strong&gt; have a receiver (Session) and a parameter (Chapter). &lt;/p&gt;&lt;p&gt;&lt;/p&gt;</content:encoded></item><item><title>𝗦𝗵𝗼𝗿𝘁𝗲𝗻 𝘆𝗼𝘂𝗿 𝗳𝗲𝗲𝗱𝗯𝗮𝗰𝗸 𝗹𝗼𝗼𝗽 𝗳𝗼𝗿 𝗳𝗮𝘀𝘁𝗲𝗿 𝗿𝗲𝘀𝘂𝗹𝘁𝘀!</title><link>https://mirosh.dev/689337d0700180000156c6fd/</link><guid isPermaLink="true">https://mirosh.dev/689337d0700180000156c6fd/</guid><description>This actually applies everywhere in life - not just in programming. But in programming - it’s crucial to shorten your feedback loop.

Recently, I’ve been working with some obscure Google services - Google Play Asset Delivery and Google Play Feature Delivery. Both are not very popular services and the documentation is kind of terrible (didn’t expect that from Google tbh).

Here’s the problem, though - you can’t debug the code with these services. Because you have to bundle the assets and the modu</description><pubDate>Wed, 06 Aug 2025 11:09:48 GMT</pubDate><content:encoded>&lt;p&gt;This actually applies everywhere in life - not just in programming. But in programming - it’s crucial to shorten your feedback loop.&lt;/p&gt;&lt;p&gt;Recently, I’ve been working with some obscure Google services - Google Play Asset Delivery and Google Play Feature Delivery. Both are not very popular services and the documentation is kind of terrible (didn’t expect that from Google tbh).&lt;/p&gt;&lt;p&gt;Here’s the problem, though - you can’t debug the code with these services. Because you have to bundle the assets and the modules (as split apks) together, you can’t debug through Android Studio. Therefore no access to the debugger. So the quickest feedback loop is thrown out the window straight away.&lt;/p&gt;&lt;p&gt;Here’s how I went from an incredibly slow feedback loop to less than 1 minute to get feedback.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Uploaded signed builds to Internal Testing (~10+ minutes to feedback, considering the build was 1.5 GB)&lt;/li&gt;&lt;li&gt;Uploaded unsigned builds to Internal App Sharing after the app was approved (~5 minutes feedback)&lt;/li&gt;&lt;li&gt;Swapped production assets/modules for test modules/assets, which are 6x less in size (~3 minutes feedback)&lt;/li&gt;&lt;li&gt;Used bundletool for local testing before uploading to Play (~2 minutes feedback)&lt;/li&gt;&lt;li&gt;Wrote a script that adds/removes resources locally, builds the bundle, converts it to APK, and then pushes it onto the device (&amp;#x3C;1 minute feedback)&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;At around point 5 - that’s where the productivity skyrocketed.&lt;/p&gt;&lt;p&gt;Fast feedback loop == faster improvement == faster results.&lt;/p&gt;</content:encoded></item><item><title>Auto-Incerement your builds</title><link>https://mirosh.dev/auto-incerement-your-builds/</link><guid isPermaLink="true">https://mirosh.dev/auto-incerement-your-builds/</guid><description>One irritating thing about Google Play Console is that it will show you need to increment the versionCode AFTER you have uploaded and it has processed your build. I’m currently working with very large .aab and APK sizes for testing - and it takes quite a while to process the build. It’s extremely frustrating to see that you forgot to increment your build version after making and uploading the build.

So I found this straightforward solution on Stack Overflow that auto-increments your build versi</description><pubDate>Mon, 04 Aug 2025 18:43:25 GMT</pubDate><content:encoded>&lt;p&gt;One irritating thing about Google Play Console is that it will show you need to increment the versionCode AFTER you have uploaded and it has processed your build. I’m currently working with very large .aab and APK sizes for testing - and it takes quite a while to process the build. It’s extremely frustrating to see that you forgot to increment your build version after making and uploading the build.&lt;br&gt;&lt;br&gt;So I found &lt;a href=&quot;https://stackoverflow.com/questions/21405457/autoincrement-versioncode-with-gradle-extra-properties/21405744?ref=cms.mirosh.dev#21405744&quot;&gt;this straightforward solution on Stack Overflow&lt;/a&gt; that auto-increments your build version on Android with each compilation - and it works like a charm&lt;br&gt;&lt;br&gt;If you’re using Kotlin for Gradle - you can &lt;a href=&quot;https://gist.github.com/nsmirosh/5ec7aa47289063023cd0af06fdee0c47?ref=cms.mirosh.dev&quot;&gt;use my gist here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;And that’s it - each time you rebuild the app, it will auto-increment the build number, and you will no longer run into these dumb “please use other version code” errors. &lt;/p&gt;</content:encoded></item><item><title>NoSuchMethodError - a headache in a multi-module project.</title><link>https://mirosh.dev/nosuchmethoderror-a-headache-in-a-multi-module-project/</link><guid isPermaLink="true">https://mirosh.dev/nosuchmethoderror-a-headache-in-a-multi-module-project/</guid><description>Lately, I’ve been working with a legacy multi-module project in Android. I aimed to complete all the application initialization in my newly created module.

However, the legacy module had an Application class of it’s own with quite a bit of logic.

I faced a problem - how do I initialize the legacy application class, since you can only have one in Android?

Well, I decided to simply make the legacy Application an ordinary class and pass Context from my Application into the former application.

T</description><pubDate>Wed, 16 Jul 2025 12:13:14 GMT</pubDate><content:encoded>&lt;p&gt;Lately, I’ve been working with a legacy multi-module project in Android. I aimed to complete all the application initialization in my newly created module. &lt;/p&gt;&lt;p&gt;However, the legacy module had an &lt;code&gt;Application&lt;/code&gt; class of it’s own with quite a bit of logic.&lt;/p&gt;&lt;p&gt;I faced a problem - how do I initialize the legacy application class, since you can only have one in Android? &lt;/p&gt;&lt;p&gt;Well, I decided to simply make the legacy Application an ordinary class and pass Context from my Application into the former application.&lt;/p&gt;&lt;p&gt;This worked beautifully on debug. However, when the time came to make a release build, the app started crashing with &lt;code&gt;NoSuchMethodError&lt;/code&gt;. It couldn’t find the method I created to initialize the legacy Application class. I knew the problem was due to optimizations / obfuscation being done, but I couldn’t understand why. My &lt;code&gt;Application&lt;/code&gt; was clearly calling this method during initialization. I added the proguard (optimization) rules to keep the method, turned off minifyEnabled, etc. Still - nothing helped.&lt;/p&gt;&lt;p&gt;Well, after hours of debugging and scratching my head - I finally got it. I correctly specified the proguard rules and minification… but in the wrong module. &lt;/p&gt;&lt;p&gt;Sure, my newly created module was clearly referencing the legacy code. However, the legacy module knew nothing about it. Since modules are treated as separate gradle projects - all code that’s not used within the module itself will be deleted during optimization.&lt;/p&gt;&lt;p&gt;Solution?&lt;/p&gt;&lt;p&gt;Specify the method/classes you want to keep in your &lt;code&gt;proguard-rules.pro&lt;/code&gt; file &lt;strong&gt;INSIDE&lt;/strong&gt; the module where this method/class exists. That way, this code won’t get removed during optimization.&lt;/p&gt;</content:encoded></item><item><title>Ktor engines</title><link>https://mirosh.dev/ktor-engines/</link><guid isPermaLink="true">https://mirosh.dev/ktor-engines/</guid><description>Ktor has many engines, but each one comes with its own unique challenges + configurations. This is the price that must be paid for being a highly flexible framework.

The Ktor team has done a wonderful job. The fact that Ktor can work with so many platforms is amazing.

I&apos;m sharing a part of the slideshow that I&apos;m using in networking section of my KMP course and I hope you find it useful!</description><pubDate>Thu, 10 Jul 2025 13:25:16 GMT</pubDate><content:encoded>&lt;p&gt;Ktor has many engines, but each one comes with its own unique challenges + configurations. This is the price that must be paid for being a highly flexible framework. &lt;/p&gt;&lt;p&gt;The Ktor team has done a wonderful job. The fact that Ktor can work with so many platforms is amazing. &lt;/p&gt;&lt;p&gt;I&apos;m sharing a part of the slideshow that I&apos;m using in networking section of my KMP course and I hope you find it useful! &lt;/p&gt;&lt;figure class=&quot;kg-card kg-gallery-card kg-width-wide&quot;&gt;&lt;div class=&quot;kg-gallery-container&quot;&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/2-2.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/2-2.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/2-2.png 1000w, https://cms.mirosh.dev/content/images/2025/07/2-2.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/3-2.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/3-2.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/3-2.png 1000w, https://cms.mirosh.dev/content/images/2025/07/3-2.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/4-2.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/4-2.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/4-2.png 1000w, https://cms.mirosh.dev/content/images/2025/07/4-2.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/5-2.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/5-2.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/5-2.png 1000w, https://cms.mirosh.dev/content/images/2025/07/5-2.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/6-2.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/6-2.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/6-2.png 1000w, https://cms.mirosh.dev/content/images/2025/07/6-2.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/7-2.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/7-2.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/7-2.png 1000w, https://cms.mirosh.dev/content/images/2025/07/7-2.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/8-1.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/8-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/8-1.png 1000w, https://cms.mirosh.dev/content/images/2025/07/8-1.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;</content:encoded></item><item><title>Source sets or How KMP organizes multi-platform code.</title><link>https://mirosh.dev/source-sets-or-how-kmp-organizes-multi-platform-code/</link><guid isPermaLink="true">https://mirosh.dev/source-sets-or-how-kmp-organizes-multi-platform-code/</guid><description>One of the core aspects of KMP is source sets, which help us organize multi-platform code. In order for the app to compile properly and interact with platform-specific APIs, it&apos;s important to familiarize yourself with this concept. I&apos;ve put together this post to help you understand this better. What have you struggled with in KMP? Let me know in the comments 👇</description><pubDate>Thu, 03 Jul 2025 11:32:02 GMT</pubDate><content:encoded>&lt;p&gt;One of the core aspects of KMP is source sets, which help us organize multi-platform code. In order for the app to compile properly and interact with platform-specific APIs, it&apos;s important to familiarize yourself with this concept. I&apos;ve put together this post to help you understand this better. What have you struggled with in KMP? Let me know in the comments 👇 &lt;/p&gt;&lt;figure class=&quot;kg-card kg-gallery-card kg-width-wide&quot;&gt;&lt;div class=&quot;kg-gallery-container&quot;&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/1-1.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/1-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/1-1.png 1000w, https://cms.mirosh.dev/content/images/2025/07/1-1.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/2-1.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/2-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/2-1.png 1000w, https://cms.mirosh.dev/content/images/2025/07/2-1.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/3-1.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/3-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/3-1.png 1000w, https://cms.mirosh.dev/content/images/2025/07/3-1.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/4-1.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/4-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/4-1.png 1000w, https://cms.mirosh.dev/content/images/2025/07/4-1.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/5-1.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/5-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/5-1.png 1000w, https://cms.mirosh.dev/content/images/2025/07/5-1.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/6-1.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/6-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/6-1.png 1000w, https://cms.mirosh.dev/content/images/2025/07/6-1.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/7-1.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/7-1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/7-1.png 1000w, https://cms.mirosh.dev/content/images/2025/07/7-1.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;</content:encoded></item><item><title>Breaking UI changes in Android 16.</title><link>https://mirosh.dev/breaking-ui-changes-in-android-16/</link><guid isPermaLink="true">https://mirosh.dev/breaking-ui-changes-in-android-16/</guid><description>Fellow Android developers - beware! Breaking changes in Android 16. 🚨 Android will ignore the orientation attribute in your Manifest starting from Android 16.

So if your app is forcing this and you&apos;re targeting the latest Android - your UI might get messed up. While this is, of course, not fun for those who will have to re-do their UI - I see this as a positive change. Many devs abused this by forcing certain orientations in their apps - not realizing that configuration changes can still happe</description><pubDate>Tue, 01 Jul 2025 13:38:13 GMT</pubDate><content:encoded>&lt;p&gt;Fellow Android developers - beware! Breaking changes in Android 16. 🚨 Android will ignore the orientation attribute in your Manifest starting from Android 16. &lt;/p&gt;&lt;p&gt;So if your app is forcing this and you&apos;re targeting the latest Android - your UI might get messed up. While this is, of course, not fun for those who will have to re-do their UI - I see this as a positive change. Many devs abused this by forcing certain orientations in their apps - not realizing that configuration changes can still happen. &lt;/p&gt;&lt;p&gt;You can read more about this here:&lt;/p&gt;&lt;figure class=&quot;kg-card kg-bookmark-card&quot;&gt;&lt;a class=&quot;kg-bookmark-container&quot; href=&quot;https://android-developers.googleblog.com/2025/01/orientation-and-resizability-changes-in-android-16.html?ref=cms.mirosh.dev&quot;&gt;&lt;div class=&quot;kg-bookmark-content&quot;&gt;&lt;div class=&quot;kg-bookmark-title&quot;&gt;The future is adaptive: Changes to orientation and resizability APIs in Android 16&lt;/div&gt;&lt;div class=&quot;kg-bookmark-description&quot;&gt;Build adaptive apps that support various screen sizes and orientations, promoting consistent user experiences across different Android devices.&lt;/div&gt;&lt;div class=&quot;kg-bookmark-metadata&quot;&gt;&lt;img class=&quot;kg-bookmark-icon&quot; src=&quot;https://cms.mirosh.dev/content/images/icon/favicon.ico&quot; alt=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;span class=&quot;kg-bookmark-author&quot;&gt;Android Developers Blog&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;kg-bookmark-thumbnail&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/thumbnail/AfD-20_-20-20Android-2016-20Beta-201-20Blog-20Assets-20_-20Blog-20Header-20and-20Metadata-20Card_meta-20card.png&quot; alt=&quot;&quot; onerror=&quot;this.style.display = &amp;#x27;none&amp;#x27;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;div class=&quot;kg-card kg-callout-card kg-callout-card-blue&quot;&gt;&lt;div class=&quot;kg-callout-emoji&quot;&gt;💡&lt;/div&gt;&lt;div class=&quot;kg-callout-text&quot;&gt;No need to worry too much, though. This will only affect devices with the smallest dimension of &gt;=600 dp&lt;/div&gt;&lt;/div&gt;</content:encoded></item><item><title>Kotlin’s :: operator and kewl things you can do with it</title><link>https://mirosh.dev/kotlins-operator-and-kewl-things-you-can-do-with-it/</link><guid isPermaLink="true">https://mirosh.dev/kotlins-operator-and-kewl-things-you-can-do-with-it/</guid><description>Kotlin has many concise operators, but one of the “confusing” ones is the:: operator.

You’ve probably seen it a bunch of times in projects you’ve worked in. Java has it as well. The :: operator is called the callable reference operator. It gives you a reference to a function, property, or a constructor without invoking it. This has many applications, but in this post I’ll show you just a couple of popular applications.

You can see the carousel below for examples. So, we can use :: (callable re</description><pubDate>Tue, 01 Jul 2025 13:29:45 GMT</pubDate><content:encoded>&lt;p&gt;Kotlin has many concise operators, but one of the “confusing” ones is the&lt;code&gt;::&lt;/code&gt; operator. &lt;/p&gt;&lt;p&gt;You’ve probably seen it a bunch of times in projects you’ve worked in. Java has it as well. The &lt;code&gt;::&lt;/code&gt; operator is called the callable reference operator. It gives you a reference to a function, property, or a constructor without invoking it. This has many applications, but in this post I’ll show you just a couple of popular applications. &lt;/p&gt;&lt;p&gt;You can see the carousel below for examples. So, we can use :: (callable reference operator) for…&lt;/p&gt;&lt;ol&gt;&lt;li&gt; Passing functions as parameters. Then we can treat this reference as any other lambda. In our case, we will get a reference to the &lt;code&gt;isEven()&lt;/code&gt; function. Getting a reference to a function is extremely useful for writing concise and clear code. &lt;/li&gt;&lt;li&gt;Callbacks. Another popular use case of the &lt;code&gt;::&lt;/code&gt; operator is callbacks. Similar to our previous example, we can use this operator to reference a callback that we have defined elsewhere in our file.&lt;/li&gt;&lt;li&gt;Reflection. Reflection is immensely handy in many use cases, such as dependency Injection, logging frameworks, and various libraries. The &lt;code&gt;::&lt;/code&gt; operator allows us to reference constructors, member variables, and other parts of our class. For example, you can do &lt;code&gt;MyClass::class&lt;/code&gt; to access class metadata, or &lt;code&gt;User::name&lt;/code&gt; to get a property reference. These are just a few ways you can use the &lt;code&gt;::&lt;/code&gt;operator in Kotlin — and I barely scratched the surface. &lt;/li&gt;&lt;/ol&gt;&lt;p&gt;What’s your favorite use of the &lt;code&gt;::&lt;/code&gt; operator? &lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class=&quot;kg-card kg-gallery-card kg-width-wide&quot;&gt;&lt;div class=&quot;kg-gallery-container&quot;&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/1.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/1.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/1.png 1000w, https://cms.mirosh.dev/content/images/2025/07/1.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/2.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/2.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/2.png 1000w, https://cms.mirosh.dev/content/images/2025/07/2.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/3.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/3.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/3.png 1000w, https://cms.mirosh.dev/content/images/2025/07/3.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/4.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/4.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/4.png 1000w, https://cms.mirosh.dev/content/images/2025/07/4.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/5.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/5.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/5.png 1000w, https://cms.mirosh.dev/content/images/2025/07/5.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/6.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/6.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/6.png 1000w, https://cms.mirosh.dev/content/images/2025/07/6.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-row&quot;&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/7.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/7.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/7.png 1000w, https://cms.mirosh.dev/content/images/2025/07/7.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/8.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/8.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/8.png 1000w, https://cms.mirosh.dev/content/images/2025/07/8.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;div class=&quot;kg-gallery-image&quot; style=&quot;flex: 1.0000 1 0%;&quot;&gt;&lt;img src=&quot;https://cms.mirosh.dev/content/images/2025/07/Kotlin---s--operator-and-kewl-things-you-can-do-with-it.png&quot; width=&quot;1200&quot; height=&quot;1200&quot; loading=&quot;lazy&quot; alt=&quot;&quot; srcset=&quot;https://cms.mirosh.dev/content/images/size/w600/2025/07/Kotlin---s--operator-and-kewl-things-you-can-do-with-it.png 600w, https://cms.mirosh.dev/content/images/size/w1000/2025/07/Kotlin---s--operator-and-kewl-things-you-can-do-with-it.png 1000w, https://cms.mirosh.dev/content/images/2025/07/Kotlin---s--operator-and-kewl-things-you-can-do-with-it.png 1200w&quot; sizes=&quot;(min-width: 720px) 720px&quot; decoding=&quot;async&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;</content:encoded></item></channel></rss>