<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Evgeny Khramov</title>
    <description>The latest articles on DEV Community by Evgeny Khramov (@hram).</description>
    <link>https://dev.to/hram</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4142952%2F368dd7ed-35dc-43c5-968d-6e04db62e780.png</url>
      <title>DEV Community: Evgeny Khramov</title>
      <link>https://dev.to/hram</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hram"/>
    <language>en</language>
    <item>
      <title>I Let a Coding Agent Render Android UI Without an Emulator. A Screenshot Wasn't Enough.</title>
      <dc:creator>Evgeny Khramov</dc:creator>
      <pubDate>Fri, 25 Sep 2026 17:16:17 +0000</pubDate>
      <link>https://dev.to/hram/i-let-a-coding-agent-render-android-ui-without-an-emulator-a-screenshot-wasnt-enough-25o</link>
      <guid>https://dev.to/hram/i-let-a-coding-agent-render-android-ui-without-an-emulator-a-screenshot-wasnt-enough-25o</guid>
      <description>&lt;p&gt;When a coding agent changes Android XML, it is fairly confident about what it can read: it finds the layout, understands the constraints, and produces a diff. But there was almost always one more manual step afterwards. I would open the screen and decide whether the button, the text, or the card had actually ended up in the right place.&lt;/p&gt;

&lt;p&gt;I wanted to remove that repeated switch. I was not trying to hand over responsibility for the product or to declare the emulator unnecessary. I wanted the result of the agent's changes to be available inside the same workflow in which it edited the code.&lt;/p&gt;

&lt;p&gt;My first idea was a screenshot: the agent changes XML, gets an image, and looks at it. In practice, that turned out to be only half of the solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The loop I wanted for XML/View screens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;change XML → render → check the result → fix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I put the rendering into a local MCP server built with Kotlin/JVM and Robolectric. The agent supplies a layout and explicit data for it, and gets the rendered result back. MCP is not the interesting part here. It is just a way to take me out of a repetitive intermediate step: launching the render by hand and passing the result back to the agent.&lt;/p&gt;

&lt;p&gt;The scope is deliberately narrow. This is for XML/View projects, not Compose. It is not a replacement for a device, it does not promise a pixel-for-pixel match with a physical phone, and it does not try to emulate hardware surfaces, video, or a camera.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PNG worked, and the tool was still not finished
&lt;/h2&gt;

&lt;p&gt;The screenshot made the result visible immediately. The agent could notice that text was clipped, that a block had unexpectedly disappeared, or that two elements were clearly overlapping. That was already better than editing XML blind.&lt;/p&gt;

&lt;p&gt;At that point the project looked almost done. Screenshots were generated, the MCP server responded, and an &lt;code&gt;inspect_view&lt;/code&gt; tool existed.&lt;/p&gt;

&lt;p&gt;A closer review broke that impression. The screenshot was real, but the sidecar returned an artificial root node as the View tree. There was no real child View to query, so a tool called &lt;code&gt;inspect_view&lt;/code&gt; was barely useful.&lt;/p&gt;

&lt;p&gt;That showed how much an image alone leaves unresolved. It is poor at answering the questions that often decide whether a UI change is correct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is the actual distance between two Views?&lt;/li&gt;
&lt;li&gt;Has an element gone beyond the screen boundary?&lt;/li&gt;
&lt;li&gt;Where exactly is the View with a particular &lt;code&gt;id&lt;/code&gt;?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A person can estimate these by eye. The agent needs verifiable data, not just an illustration.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second wrong assumption: the warm JVM
&lt;/h2&gt;

&lt;p&gt;Around the same time the coding agent pointed out another oversimplified idea in my design. You cannot just start the Robolectric runtime once and keep reusing it after changes to XML or Kotlin: you risk inspecting stale resources and an outdated classpath.&lt;/p&gt;

&lt;p&gt;That observation changed the architecture. It removed the temptation to treat a "warm JVM" as a problem we had already solved. The default sidecar still is not the persistently warm JVM from my early architectural plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: measure what you render
&lt;/h2&gt;

&lt;p&gt;The turning point was simple to state: the PNG and the data must come from the very same layout, after layout has been performed.&lt;/p&gt;

&lt;p&gt;After &lt;code&gt;inflate → fixture → measure → layout → draw&lt;/code&gt;, the renderer saves both the image and the real View tree. Each node includes its identifier, type, text and state, padding, margins, and absolute bounds.&lt;/p&gt;

&lt;p&gt;Three small operations make up the loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;render_layout  → creates a render session
get_view_tree  → returns the tree of that session
inspect_view   → returns a single node by id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API names matter less than what they allow. The agent can move from "these look a little too close together" to a measurable fact, such as comparing the edge of a button with the edge of its container without estimating anything from a PNG.&lt;/p&gt;

&lt;p&gt;That gave us a geometry feedback loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;change → render → inspect → evidence → fix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image helps reveal a problem, the tree provides the geometry, and the next change goes through the same render again.&lt;/p&gt;

&lt;p&gt;Underneath there are also a source fingerprint and an allowlisted Gradle pipeline. They exist so the loop cannot silently render stale state or run arbitrary shell commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it on a real layout
&lt;/h2&gt;

&lt;p&gt;After these changes I connected the MCP server to an actual Android project I was working on. The coding agent used it to fix a real product-card layout with a required-items section.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi2b3hwwrqtfnhntbuuzk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi2b3hwwrqtfnhntbuuzk.png" alt="Product card before the fix, with a " width="720" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Before the fix. The UI is in Russian.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgfnok870thmwia7babuc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgfnok870thmwia7babuc.png" alt="Product card after the fix, with the " width="643" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;After the fix. The UI is in Russian. The two screenshots show different items, so they are not a pixel-level comparison.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For me this was the key acceptance test. The tool had moved beyond its RFC and its own unit tests into a real UI task, and a coding agent used the loop in an actual UI change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the experiment did and did not prove
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I wanted to check&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Can a real Android XML/View layout be rendered through MCP + Robolectric?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can a real, machine-readable View tree be returned alongside the PNG?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did a coding agent use this loop in a real UI change?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can the agent's actions be fully reproduced from the saved data?&lt;/td&gt;
&lt;td&gt;Not yet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I do not consider this a replacement for an emulator or a device. Robolectric does not guarantee a pixel-perfect match with hardware. I have no compatibility matrix covering custom Views and complex themes. For the next experiment of this kind I want to keep the complete record: the prompt, the tool calls, render IDs, the XML diff, the measurements, and the final verdict.&lt;/p&gt;

&lt;p&gt;These are reasonable limits for a first version. What matters more is that they can now be described as facts rather than as the impression left by a good screenshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who did what
&lt;/h2&gt;

&lt;p&gt;I did not give the agent the authority to "decide the UI." I defined a bounded problem, set the limits of support, refused to accept a PNG as sufficient evidence, and kept bringing the work back to verifiable criteria.&lt;/p&gt;

&lt;p&gt;The coding agent was more than a code generator. It helped implement the tool, flagged the risk of a stale Robolectric runtime, and helped build and use the loop itself.&lt;/p&gt;

&lt;p&gt;For me the main result is not that the agent now has an image. A screenshot makes an interface visible. A PNG together with a View tree makes it verifiable.&lt;/p&gt;

&lt;p&gt;That is how manual visual review can leave the routine loop: not because I decided to trust the agent, but because it gets artifacts that can be measured and checked again.&lt;/p&gt;

&lt;p&gt;The renderer is open source: &lt;a href="https://github.com/hram/android-ui-renderer-mcp" rel="noopener noreferrer"&gt;android-ui-renderer-mcp on GitHub&lt;/a&gt;. The full case study is on my site: &lt;a href="https://hram.github.io/en/articles/android-ui-renderer-mcp/" rel="noopener noreferrer"&gt;hram.github.io/en/articles/android-ui-renderer-mcp&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>android</category>
      <category>mcp</category>
      <category>ai</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>A Coding Agent Tried to Fix My CameraX Rotation Bug by Waiting. I Asked for Proof Instead.</title>
      <dc:creator>Evgeny Khramov</dc:creator>
      <pubDate>Fri, 25 Sep 2026 16:26:51 +0000</pubDate>
      <link>https://dev.to/hram/a-coding-agent-tried-to-fix-my-camerax-rotation-bug-by-waiting-i-asked-for-proof-instead-3fh9</link>
      <guid>https://dev.to/hram/a-coding-agent-tried-to-fix-my-camerax-rotation-bug-by-waiting-i-asked-for-proof-instead-3fh9</guid>
      <description>&lt;p&gt;My Android barcode scanner had a CameraX bug: after rotating a tablet through 180°, the preview still looked correct, but the frames received by the analyzer were upside down.&lt;/p&gt;

&lt;p&gt;The interesting part wasn't the final fix. It was deciding when we actually had enough evidence to trust it.&lt;/p&gt;

&lt;p&gt;A coding agent handled most of the debugging: reading the code, adding instrumentation, running &lt;code&gt;adb logcat&lt;/code&gt;, changing the implementation, and checking the results. I handled the physical tablet.&lt;/p&gt;

&lt;p&gt;The debugging loop was unusual.&lt;/p&gt;

&lt;p&gt;The agent started &lt;code&gt;adb logcat&lt;/code&gt;. I opened the scanner and physically rotated&lt;br&gt;
the tablet. Then the agent collected the new values and adjusted the&lt;br&gt;
instrumentation.&lt;/p&gt;

&lt;p&gt;We repeated this several times until we had the states we needed to compare.&lt;/p&gt;

&lt;p&gt;These were the values that eventually explained the bug. Before rotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Configuration.orientation=2
displayRotation=1
analysisTargetRotation=1
imageRotation=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After rotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Configuration.orientation=2
displayRotation=3
analysisTargetRotation=1
imageRotation=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The display had moved on. &lt;code&gt;ImageAnalysis&lt;/code&gt; had not.&lt;/p&gt;

&lt;p&gt;Getting to that evidence required stopping a different line of work first: waiting for a rotation value to update, then rebinding the camera.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we nearly ended up waiting for the wrong thing
&lt;/h2&gt;

&lt;p&gt;During an earlier experiment, the agent observed that the display-change callback had already fired while &lt;code&gt;previewView.display.rotation&lt;/code&gt; still held the old value. About half a second later, that value became correct.&lt;/p&gt;

&lt;p&gt;The agent concluded:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;post is not enough. We need to wait for previewView.display.rotation to actually change before rebinding.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The proposed direction looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DisplayListener
→ callback
→ display.rotation still has the old value
→ post
→ still the old value
→ wait
→ get the new rotation
→ rebind CameraX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I stopped the work at that point. I did not yet know the correct fix. We might have needed another listener, different &lt;code&gt;ImageAnalysis&lt;/code&gt; settings, or a deeper investigation of YUV processing.&lt;/p&gt;

&lt;p&gt;But the timing approach left basic questions unanswered. Why did rotating the tablet require recreating the camera use cases? What exactly were we waiting for? Half a second on this tablet—how long on another?&lt;/p&gt;

&lt;p&gt;I asked the agent to stop fixing things and prove the cause first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the states that should agree
&lt;/h2&gt;

&lt;p&gt;We added these values to the diagnostics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Configuration.orientation
displayRotation
Preview.targetRotation
ImageAnalysis.targetRotation
ImageProxy.rotationDegrees
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we repeated the physical experiment. The agent started a fresh log capture; I opened the scanner screen and rotated the tablet. It collected the output, adjusted the instrumentation, and asked me to repeat the experiment when needed.&lt;/p&gt;

&lt;p&gt;That produced the before-and-after logs at the top of this article.&lt;/p&gt;

&lt;p&gt;The tablet had moved from &lt;code&gt;ROTATION_90&lt;/code&gt; to &lt;code&gt;ROTATION_270&lt;/code&gt;. Both positions were still landscape, so &lt;code&gt;Configuration.orientation&lt;/code&gt; remained &lt;code&gt;2&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORIENTATION_LANDSCAPE → ORIENTATION_LANDSCAPE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The display already reported rotation &lt;code&gt;3&lt;/code&gt;, while the analyzer retained its old target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;display                 ROTATION_270
ImageAnalysis.target    ROTATION_90
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ImageProxy.rotationDegrees&lt;/code&gt; consequently remained &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We now had a specific, measured cause: &lt;strong&gt;&lt;code&gt;ImageAnalysis.targetRotation&lt;/code&gt; was not being updated when the device rotated through 180°.&lt;/strong&gt; That gave us something concrete to fix and a way to verify the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update the existing use cases
&lt;/h2&gt;

&lt;p&gt;The implementation moved to this flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrientationEventListener
→ calculatedRotation
→ Preview.targetRotation = calculatedRotation
→ ImageAnalysis.targetRotation = calculatedRotation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The existing &lt;code&gt;Preview&lt;/code&gt; and &lt;code&gt;ImageAnalysis&lt;/code&gt; instances remained in place. Handling the rotation no longer required &lt;code&gt;delay&lt;/code&gt;, &lt;code&gt;postDelayed&lt;/code&gt;, polling, &lt;code&gt;unbindAll()&lt;/code&gt;, or &lt;code&gt;bindToLifecycle()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There was one subtle mapping detail. The degrees reported by &lt;code&gt;OrientationEventListener&lt;/code&gt; could not be mapped directly to the &lt;code&gt;Surface.ROTATION_*&lt;/code&gt; constant with the same angle in its name. In our case, the range 45..134° had to map to &lt;code&gt;ROTATION_270&lt;/code&gt;, not &lt;code&gt;ROTATION_90&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We verified the change with the same physical experiment. Before rotating the tablet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;displayRotation=1
previewTargetRotation=1
analysisTargetRotation=1
imageRotation=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After rotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;orientationDegrees=70..79
calculatedRotation=3
displayRotation=3
previewTargetRotation=3
analysisTargetRotation=3
imageRotation=180
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chain was consistent. CameraX received the new &lt;code&gt;targetRotation&lt;/code&gt;, and &lt;code&gt;ImageProxy.rotationDegrees&lt;/code&gt; changed from &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;180&lt;/code&gt;, without recreating the use cases or rebinding.&lt;/p&gt;

&lt;p&gt;These are the observations from our tablet and scanner. The mapping detail belongs to this case; it is not a claim that the same implementation has been verified on every device.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent did, and what I still had to decide
&lt;/h2&gt;

&lt;p&gt;The agent performed most of the mechanical work. It read the existing code, added diagnostics, collected device data, analyzed the logs, changed the implementation, repeated the checks, and updated the merge request.&lt;/p&gt;

&lt;p&gt;My contribution included designing the experiment, physically rotating the tablet, evaluating the hypotheses, and deciding what would count as sufficient evidence.&lt;/p&gt;

&lt;p&gt;At the critical moment, I did not know the right five lines of Kotlin. I could still recognize that “the value arrives later, so wait for it” was an incomplete explanation of the bug.&lt;/p&gt;

&lt;p&gt;The useful intervention was to require the agent to show where the state diverged. That changed both the investigation and the fix.&lt;/p&gt;

&lt;p&gt;For me, the reusable lesson is to make the verification criterion explicit: identify the states that should agree, capture their values during the failing behavior, and repeat the same experiment after the change. Here, that meant checking the display rotation, both use-case targets, and the image rotation together.&lt;/p&gt;

&lt;p&gt;A coding agent can run much of that loop. Deciding what the experiment proves—and whether its evidence justifies accepting the fix—remains engineering work.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://hram.github.io/en/articles/camerax-agent/" rel="noopener noreferrer"&gt;full case study&lt;/a&gt; includes the debugging dialogue and a more detailed breakdown of how we divided the work.&lt;/p&gt;

</description>
      <category>android</category>
      <category>camerax</category>
      <category>ai</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
