<?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>Forem: Joseph Sanjaya</title>
    <description>The latest articles on Forem by Joseph Sanjaya (@sanjayajoseph).</description>
    <link>https://forem.com/sanjayajoseph</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2667080%2F03e890ca-70d5-4258-bebe-d5b053e3414c.jpg</url>
      <title>Forem: Joseph Sanjaya</title>
      <link>https://forem.com/sanjayajoseph</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/sanjayajoseph"/>
    <language>en</language>
    <item>
      <title>How I Fixed Sunshine Screen Sharing on macOS Tahoe (The Native Wrapper Method)</title>
      <dc:creator>Joseph Sanjaya</dc:creator>
      <pubDate>Thu, 01 Jan 2026 03:38:17 +0000</pubDate>
      <link>https://forem.com/sanjayajoseph/how-i-fixed-sunshine-screen-sharing-on-macos-tahoe-the-native-wrapper-method-558g</link>
      <guid>https://forem.com/sanjayajoseph/how-i-fixed-sunshine-screen-sharing-on-macos-tahoe-the-native-wrapper-method-558g</guid>
      <description>&lt;p&gt;I have a bit of an unconventional “Work From Cafe” (WFC) setup.&lt;/p&gt;

&lt;p&gt;While most people carry a MacBook, I often travel with a &lt;strong&gt;Mac Mini and a tablet&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You might ask: &lt;em&gt;Why carry a desktop to a cafe?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer comes down to &lt;strong&gt;pure economics&lt;/strong&gt;. A Mac Mini with an M4-series chip offers the exact same raw performance as a MacBook Pro but for a fraction of the price. We are talking about a ~$600 machine versus a $2,000+ laptop.&lt;/p&gt;

&lt;p&gt;Furthermore, I already own a tablet. Why should I pay for a MacBook screen I don’t need when I already have a gorgeous, high-resolution display in my backpack?&lt;/p&gt;

&lt;p&gt;To bridge the two, I don’t use Sidecar or VNC. I use &lt;strong&gt;Sunshine&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sunshine (the host for Moonlight clients) is designed for gaming. That means it prioritizes &lt;strong&gt;ultra-low latency&lt;/strong&gt; above all else. For coding and general UI navigation, it feels almost native, whereas standard VNC or AirPlay can feel like moving your mouse through molasses.&lt;/p&gt;

&lt;p&gt;But after the recent update to &lt;strong&gt;macOS Tahoe (macOS 26)&lt;/strong&gt;, my perfect, budget-friendly setup broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: The “Invisible” Service
&lt;/h2&gt;

&lt;p&gt;Everything was perfect until the recent &lt;strong&gt;macOS Tahoe&lt;/strong&gt; update.&lt;/p&gt;

&lt;p&gt;In previous versions of macOS, if you ran Sunshine as a background service or command-line tool, it might have been flaky, but at least it was &lt;em&gt;visible&lt;/em&gt;. You could go into System Settings, find the binary, and toggle “Screen Recording” on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS Tahoe changes the rules entirely.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The new security policy doesn’t just forget permissions for command-line tools, it ignores them completely. If you run a raw binary or a background service:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;No Prompt:&lt;/strong&gt; The system will not prompt you to allow screen recording.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;No UI Entry:&lt;/strong&gt; If you manually check &lt;code&gt;System Settings &amp;gt; Privacy &amp;amp; Security &amp;gt; Screen Recording&lt;/code&gt;, the service &lt;strong&gt;will not appear&lt;/strong&gt;. You cannot toggle a permission that doesn't exist.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;No Manual Add:&lt;/strong&gt; You often cannot even manually drag a binary into the list.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The system now essentially demands that any process requesting screen pixels must be a &lt;strong&gt;proper Native App Bundle&lt;/strong&gt; with a valid &lt;code&gt;Info.plist&lt;/code&gt; and Bundle Identifier. Without that identity, the service is invisible to the permission system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Building a Native Wrapper
&lt;/h2&gt;

&lt;p&gt;To fix this, I wrote a script that generates a “real” macOS application on the fly. You don’t need Xcode installed just the terminal.&lt;/p&gt;

&lt;p&gt;This wrapper wraps the Sunshine binary inside a legitimate &lt;code&gt;.app&lt;/code&gt; structure. This gives it a stable Bundle Identity (&lt;code&gt;dev.lizardbyte.sunshine.wrapper&lt;/code&gt;), which forces macOS to recognize it as a valid application, triggering the permission prompt and allowing it to appear in the System Settings list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Generator Script
&lt;/h2&gt;

&lt;p&gt;Here is the script I used to restore my WFC setup. It compiles a tiny Swift application that acts as a bridge between the OS and the Sunshine binary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create the file&lt;/strong&gt; Open your Terminal and create a file named &lt;code&gt;sunshine_wrapper.sh&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;touch sunshine_wrapper.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Paste the Code&lt;/strong&gt; Paste the following code.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: The Swift code below includes specific logic to handle the “Quit” command. This ensures that when you quit the wrapper app, it kills the background process, preventing “zombie” instances from eating up your CPU.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash

# Configuration
APP_NAME="Sunshine"
APP_DIR="${APP_NAME}.app"
BUNDLE_ID="dev.lizardbyte.sunshine.wrapper"

# Attempt to find sunshine binary
SUNSHINE_BIN=$(which sunshine)
if [ -z "$SUNSHINE_BIN" ]; then
    if [ -f "/opt/homebrew/bin/sunshine" ]; then
        SUNSHINE_BIN="/opt/homebrew/bin/sunshine"
    else
        echo "❌ Error: 'sunshine' binary not found!"
        exit 1
    fi
fi

echo "✅ Targeting Sunshine binary at: $SUNSHINE_BIN"

# 1. Clean &amp;amp; Create Structure
echo "📂 Creating App Structure..."
rm -rf "$APP_DIR"
mkdir -p "${APP_DIR}/Contents/MacOS"
mkdir -p "${APP_DIR}/Contents/Resources"

# 2. Create the Robust Swift Launcher
# This version imports Cocoa to handle the Quit command properly.
SWIFT_SOURCE="Launcher.swift"
cat &amp;lt;&amp;lt;EOF &amp;gt; "$SWIFT_SOURCE"
import Cocoa
import Foundation

class AppDelegate: NSObject, NSApplicationDelegate {
    var process: Process!

    func applicationDidFinishLaunching(_ notification: Notification) {
        let sunshinePath = "$SUNSHINE_BIN"

        process = Process()
        process.executableURL = URL(fileURLWithPath: sunshinePath)
        process.arguments = CommandLine.arguments.dropFirst().map { String(\$0) }

        // Pipe output so you can debug via Console.app if needed
        process.standardOutput = FileHandle.standardOutput
        process.standardError = FileHandle.standardError

        // If Sunshine crashes/quits on its own, close the wrapper app too
        process.terminationHandler = { _ in
            NSApp.terminate(nil)
        }

        do {
            try process.run()
        } catch {
            print("Failed to launch sunshine: \(error)")
            NSApp.terminate(nil)
        }
    }

    // This is called when you Right Click -&amp;gt; Quit in the Dock
    func applicationWillTerminate(_ notification: Notification) {
        if let proc = process, proc.isRunning {
            // Send SIGTERM to Sunshine to shut it down gracefully
            proc.terminate()
            // Wait a moment for it to cleanup, or it becomes a zombie
            proc.waitUntilExit()
        }
    }
}

// Main entry point
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.setActivationPolicy(.regular) // Shows in Dock, has UI (menu bar)
app.run()
EOF

# 3. Compile
echo "🔨 Compiling Native Wrapper (with AppKit)..."
swiftc "$SWIFT_SOURCE" -o "${APP_DIR}/Contents/MacOS/${APP_NAME}"
rm "$SWIFT_SOURCE"

# 4. Create Info.plist (Updated for Icon)
cat &amp;lt;&amp;lt;EOF &amp;gt; "${APP_DIR}/Contents/Info.plist"
&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&amp;gt;
&amp;lt;plist version="1.0"&amp;gt;
&amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;CFBundleExecutable&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;${APP_NAME}&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;CFBundleIdentifier&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;${BUNDLE_ID}&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;CFBundleName&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;${APP_NAME}&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;CFBundlePackageType&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;APPL&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;CFBundleShortVersionString&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;2.0&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;LSMinimumSystemVersion&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;12.0&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;NSHighResolutionCapable&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;CFBundleIconFile&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;AppIcon&amp;lt;/string&amp;gt;
&amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;
EOF

# 5. Sign
echo "🔐 Signing the application..."
codesign --force --deep --sign - "${APP_DIR}"

echo "------------------------------------------------"
echo "✅ Success! '${APP_DIR}' created."
echo "------------------------------------------------"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Run and Install&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chmod +x sunshine_wrapper.sh
./sunshine_wrapper.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, move the resulting &lt;strong&gt;Sunshine.app&lt;/strong&gt; to your &lt;code&gt;/Applications&lt;/code&gt; folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;After running this script, you may need to reset your permissions one last time to clear out any old conflicts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tccutil reset ScreenCapture dev.lizardbyte.sunshine.wrapper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you launch the new app, macOS Tahoe will see a signed, native application requesting access. Click &lt;strong&gt;Allow&lt;/strong&gt;, and the permission will finally stick.&lt;/p&gt;

&lt;p&gt;My portable Mac Mini setup is back in business. Just like that, you can stream your Mac Mini to &lt;strong&gt;Moonlight&lt;/strong&gt; or &lt;strong&gt;Artemis&lt;/strong&gt; on your tablet with zero fuss and low latency. If you are struggling with screen sharing tools on the new macOS, try wrapping them in a native &lt;code&gt;.app&lt;/code&gt;it makes all the difference.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>coding</category>
      <category>workstations</category>
      <category>developer</category>
    </item>
    <item>
      <title>Stop Waiting for Gradle: Turn Your Old Laptop into an Android/Kotlin Build Sanctuary</title>
      <dc:creator>Joseph Sanjaya</dc:creator>
      <pubDate>Mon, 15 Dec 2025 16:13:58 +0000</pubDate>
      <link>https://forem.com/sanjayajoseph/stop-waiting-for-gradle-turn-your-old-laptop-into-an-androidkotlin-build-sanctuary-26mh</link>
      <guid>https://forem.com/sanjayajoseph/stop-waiting-for-gradle-turn-your-old-laptop-into-an-androidkotlin-build-sanctuary-26mh</guid>
      <description>&lt;p&gt;If you are a geek like me, you live in a multi-device environment. I have my &lt;strong&gt;Gaming PC&lt;/strong&gt; for freelancing and late-night sessions. I have my &lt;strong&gt;Mac&lt;/strong&gt; for professional work and those heavy iOS/CMP compilations. And I have my &lt;strong&gt;Corporate Laptop&lt;/strong&gt; for the 9-to-5 grind.&lt;/p&gt;

&lt;p&gt;Switching between them used to be a nightmare.&lt;/p&gt;

&lt;p&gt;I would push code from the Mac, walk over to the PC, pull the changes, and then… wait. I’d wait for Maven Central to redownload libraries I already had on the other machine. I’d wait for Gradle to rebuild tasks that hadn’t changed.&lt;/p&gt;

&lt;p&gt;I was paying a “Context Switch Tax” of 15 to 20 minutes every single time I changed desks.&lt;/p&gt;

&lt;p&gt;But I also had a secret weapon: an old, dusty laptop sitting in a drawer. Instead of letting it rot, I turned it into a &lt;strong&gt;Build Sanctuary&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Using &lt;a href="https://github.com/IceWhaleTech/CasaOS" rel="noopener noreferrer"&gt;&lt;strong&gt;CasaOS&lt;/strong&gt;&lt;/a&gt;, I transformed that junk hardware into a local powerhouse.&lt;/p&gt;

&lt;p&gt;The best part? It didn’t just solve my build times. Because it’s running CasaOS, this old laptop is now my &lt;strong&gt;Private Cloud&lt;/strong&gt;. It handles my local file storage (bye-bye, cloud storage fees), hosts my backend services for testing, and acts as a central environment for everything I develop.&lt;/p&gt;

&lt;p&gt;Here is how you can do it and why you should stop ignoring your old hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  The “Sanctuary” Concept
&lt;/h2&gt;

&lt;p&gt;Your bandwidth is finite, and your time is expensive. Downloading the same 500MB of libraries three times a day isn’t just inefficient; it’s a distraction.&lt;/p&gt;

&lt;p&gt;Imagine a world where you update a library on your Mac, and when you open your PC, it’s already “there.”&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;No downloading.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;No waiting.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Instant sync.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You create a &lt;strong&gt;Single Source of Truth&lt;/strong&gt; for your entire home network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Foundation (CasaOS):&lt;/strong&gt; We are going to use CasaOS. It’s a clean, visual interface for Linux. It turns a scary terminal into a friendly dashboard that looks like a smartphone screen.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Take your old laptop.&lt;/li&gt;
&lt;li&gt; Install a lightweight Linux distro (like Ubuntu Server).&lt;/li&gt;
&lt;li&gt; Run the CasaOS installer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5hl0m3cfyfreym5bjynm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5hl0m3cfyfreym5bjynm.png" alt="Casa OS Dashboard" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let’s make it useful for Android.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: The Gradle Build Cache Node
&lt;/h2&gt;

&lt;p&gt;Gradle is paranoid. It likes to rebuild things just to be safe. If you switch computers, Gradle assumes it’s a fresh start and recompiles code that you haven’t touched.&lt;/p&gt;

&lt;p&gt;A Remote Build Cache Node acts as the brain of your operation. It remembers what you built on your Mac.&lt;/p&gt;

&lt;p&gt;When you switch to your PC, Gradle asks the Node, “Do you have the output for this task?” The Node says “Yes,” and hands it over instantly via your local network. You skip the compilation step entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Setup:&lt;/strong&gt; In your CasaOS dashboard, we are going to add a custom Docker app.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Create the Custom App
*&lt;em&gt;You don’t need to touch a single line of code or write a YAML file. In the CasaOS dashboard, just click the *&lt;/em&gt;+&lt;/strong&gt; button and select &lt;strong&gt;“Install a Custom App”&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnc5w44qmzhs4f78x3aa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnc5w44qmzhs4f78x3aa.png" alt="captionless image" width="620" height="1114"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here is the Cheat Sheet:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Docker Image:&lt;/strong&gt; &lt;code&gt;gradle/build-cache-node&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Tag:&lt;/strong&gt; &lt;code&gt;latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Port (Host):&lt;/strong&gt; &lt;code&gt;5071&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Volumes (Host):&lt;/strong&gt; &lt;code&gt;/DATA/AppData/gradle-cache&lt;/code&gt; (This connects actual storage to store the Gradle cache files.)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Container Command:&lt;/strong&gt; &lt;code&gt;start&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hit &lt;strong&gt;Install&lt;/strong&gt;. Wait a few seconds until the icon turns green. You are now live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Connect Your Project&lt;/strong&gt;&lt;br&gt;
Open your Android project’s &lt;code&gt;settings.gradle.kts&lt;/code&gt; file. We need to tell the project to talk to your old laptop.&lt;/p&gt;

&lt;p&gt;Now, your Mac compiles the code, uploads the result to the laptop, and your PC downloads the result. It feels like magic. You can check if the project already using the remote cache by using &lt;code&gt;--scan&lt;/code&gt; . On the report it will shown:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2rlpz0jnyh7hw16gns53.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2rlpz0jnyh7hw16gns53.png" alt="captionless image" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: Sonatype Nexus
&lt;/h2&gt;

&lt;p&gt;I live in Indonesia. We are famous for our delicious food, beautiful islands, and unfortunately our terrible internet speeds.&lt;/p&gt;

&lt;p&gt;Yesterday, the worst happened. My ISP went dark. I had a hard deadline for a freelance client in three hours. Usually, this is the part where I panic, tether my phone, and watch a 10MB download take twenty minutes while sweating bullets.&lt;/p&gt;

&lt;p&gt;Because I had Nexus running, I kept working. My Android Studio didn’t even notice the internet was gone. It reached out to my local server, grabbed the dependencies it needed instantly, and the build finished successfully. I met the deadline without a single blocker.&lt;/p&gt;

&lt;p&gt;Nexus acts as a &lt;strong&gt;Proxy&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; You ask for a library (e.g., Retrofit).&lt;/li&gt;
&lt;li&gt; Nexus checks if it has it locally.&lt;/li&gt;
&lt;li&gt; If not, it downloads it &lt;em&gt;once&lt;/em&gt; from the internet and stores it.&lt;/li&gt;
&lt;li&gt; The next time &lt;em&gt;any&lt;/em&gt; device asks for Retrofit or if your internet dies Nexus delivers it at LAN speeds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Setup:&lt;/strong&gt; Click &lt;strong&gt;+&lt;/strong&gt; and &lt;strong&gt;“Install a Custom App”&lt;/strong&gt; again. Use these settings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1fyhih9s3aga0mloogh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1fyhih9s3aga0mloogh.png" alt="captionless image" width="609" height="656"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Cheat Sheet:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Docker Image:&lt;/strong&gt; &lt;code&gt;sonatype/nexus3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Tag:&lt;/strong&gt; &lt;code&gt;latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Port:&lt;/strong&gt; Host &lt;code&gt;8081&lt;/code&gt; ↔ Container &lt;code&gt;8081&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Volume:&lt;/strong&gt; Host &lt;code&gt;/DATA/AppData/nexus-data&lt;/code&gt; ↔ Container &lt;code&gt;/nexus-data&lt;/code&gt; &lt;em&gt;(Tip: You can point the Host path to an external hard drive if you have a massive library collection)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Configure the Proxy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Log in to the Nexus dashboard (default is usually port 8081). The default username is &lt;code&gt;admin&lt;/code&gt;. The first-time password is stored in a file inside the container’s host path open it to get the initial password.&lt;/li&gt;
&lt;li&gt;  Create a “Proxy Repository” for &lt;code&gt;maven-central&lt;/code&gt; and &lt;code&gt;google-maven&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe76a7wdh2tfez179so72.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe76a7wdh2tfez179so72.png" alt="captionless image" width="688" height="671"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Group them into a single repository called &lt;code&gt;maven-public&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Connect Your Project:&lt;/strong&gt; Now, tell your Android project to look at your sanctuary first. Open your root &lt;code&gt;build.gradle.kts&lt;/code&gt; (or &lt;code&gt;settings.gradle.kts&lt;/code&gt; in newer projects) and update the &lt;code&gt;repositories&lt;/code&gt; block:&lt;/p&gt;

&lt;p&gt;Now, your builds fly because they are pulling data via Ethernet, not waiting on a server across the ocean. You can check on the &lt;code&gt;Download Info&lt;/code&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.amazonaws.com%2Fuploads%2Farticles%2Fchccgkdhj8x9klakq2dg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fchccgkdhj8x9klakq2dg.gif" alt="captionless image" width="760" height="369"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3: The “One-Shot” Hack
&lt;/h2&gt;

&lt;p&gt;You probably have dozens of projects. Opening every single &lt;code&gt;build.gradle.kts&lt;/code&gt; file to add your new server IP is tedious. It's boring work, and you will forget to do it for new projects.&lt;/p&gt;

&lt;p&gt;We can use a &lt;strong&gt;Global Initialization Script&lt;/strong&gt;. You write this configuration &lt;em&gt;once&lt;/em&gt; on your machine. Every single project you open or create on that computer will automatically use your local Build Sanctuary. No manual editing required.&lt;/p&gt;

&lt;p&gt;On your Mac or PC, navigate to the &lt;code&gt;.gradle&lt;/code&gt; folder (usually &lt;code&gt;~/.gradle/&lt;/code&gt;) and create a folder named &lt;code&gt;init.d&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nexus.gradle,&lt;/code&gt;this script forces Gradle to look at your local server &lt;em&gt;before&lt;/em&gt; it looks at the internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The “Line Cutter” Logic (&lt;/strong&gt;&lt;code&gt;**repos.add(0, nexus)**&lt;/code&gt;&lt;strong&gt;):&lt;/strong&gt; Gradle is polite; it usually checks repositories in the order they are listed. If your project lists &lt;code&gt;google()&lt;/code&gt; first, Gradle will hit the internet first, defeating the purpose. This script aggressively removes your local repo and re-inserts it at &lt;strong&gt;index 0&lt;/strong&gt; (the top). It guarantees your local server is always the first stop.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;casa-cache.gradle&lt;/code&gt;, this script connects the build cache, but safely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to activate it:&lt;/strong&gt; Now, just add this one line to your &lt;code&gt;~/.gradle/gradle.properties&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;casa.cache.url=http://192.168.1.XXX:5071/cache/
casa.cache.push=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Android Studio. That’s it. Every project you touch is now supercharged.&lt;/p&gt;

&lt;h2&gt;
  
  
  For the Terminal Purists
&lt;/h2&gt;

&lt;p&gt;Maybe you are reading this and thinking, “I don’t want a fancy UI. I don’t want the overhead of CasaOS. I just want raw performance.”&lt;/p&gt;

&lt;p&gt;You don’t &lt;em&gt;need&lt;/em&gt; CasaOS. It’s just a nice wrapper. Under the hood, this is all just Docker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Setup:&lt;/strong&gt; If you prefer the command line, just install Docker and Docker Compose on your server. Create a single &lt;code&gt;docker-compose.yml&lt;/code&gt; file that defines both the Nexus service and the Build Cache service.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;docker-compose up -d&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You get the exact same benefits the speed, the sync, the sanctuary without the graphical interface. It’s your sanctuary; build it how you like it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling to the Enterprise
&lt;/h2&gt;

&lt;p&gt;You might be thinking, “This is cool for my home lab, but does it work at work?”&lt;/p&gt;

&lt;p&gt;This exact architecture is how top-tier tech companies speed up development.&lt;/p&gt;

&lt;p&gt;If this setup saves &lt;em&gt;you&lt;/em&gt; 15 minutes a day, imagine what it does for a team of 50 developers. By setting up a central Gradle Enterprise server or a shared Nexus instance in the office:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Onboarding is instant.&lt;/strong&gt; New employees don’t spend Day 1 downloading the internet. They sync from the local server.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Consistency is enforced.&lt;/strong&gt; Everyone uses the exact same artifacts.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Bandwidth is saved.&lt;/strong&gt; You stop 50 people from downloading the same 1GB files simultaneously.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It creates a &lt;strong&gt;Single Source of Truth&lt;/strong&gt; for the entire engineering department.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monday Morning Action Step
&lt;/h2&gt;

&lt;p&gt;Don’t let this be just another article you bookmark and forget.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Find the hardware.&lt;/strong&gt; Go dig out that old ThinkPad, the unused Dell, or even a Raspberry Pi 4.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Wipe it.&lt;/strong&gt; Install a fresh Linux OS.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Deploy.&lt;/strong&gt; Whether you use the friendly CasaOS interface or stick to pure Docker, just get it running.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Give your old tech a new job, and give yourself the gift of faster builds. Your sanity (and your internet bill) will thank you.&lt;/p&gt;

</description>
      <category>android</category>
      <category>androiddev</category>
      <category>groovy</category>
      <category>gradle</category>
    </item>
    <item>
      <title>KToon: Tiny Tables, Big Savings Plug TOON into Your @Serializable Kotlin Classes</title>
      <dc:creator>Joseph Sanjaya</dc:creator>
      <pubDate>Thu, 04 Dec 2025 16:22:49 +0000</pubDate>
      <link>https://forem.com/sanjayajoseph/ktoon-tiny-tables-big-savings-plug-toon-into-your-serializable-kotlin-classes-4091</link>
      <guid>https://forem.com/sanjayajoseph/ktoon-tiny-tables-big-savings-plug-toon-into-your-serializable-kotlin-classes-4091</guid>
      <description>&lt;p&gt;JSON is comfortable. Familiar. Everywhere.&lt;br&gt;
But it’s also wasteful. Every time we send a list of objects, we pay the cost of repeating the same field names again and again &lt;code&gt;"id"&lt;/code&gt; a hundred times, &lt;code&gt;"name"&lt;/code&gt; a hundred times silently taxing bandwidth and slowing real-world performance.&lt;/p&gt;

&lt;p&gt;Meanwhile, the LLM world quietly optimized around this problem.&lt;br&gt;
&lt;strong&gt;Token-oriented formats like TOON&lt;/strong&gt;, which treat data like a spreadsheet (headers once, values below), already power faster inference and cheaper processing at massive scale. They get real efficiency gains simply by avoiding redundancy.&lt;/p&gt;

&lt;p&gt;That raised the obvious question:&lt;br&gt;
If AI companies already benefit from TOON, why aren’t our apps?&lt;/p&gt;

&lt;p&gt;So for the &lt;strong&gt;Kiroween Hackathon’s Skeleton Crew challenge&lt;/strong&gt;, I set out to build a compact, flexible serialization format that brings those same advantages to everyday Kotlin developers without forcing anyone to rewrite their models or architecture. Using Kiro’s spec-driven workflow to design before coding, I built the foundation for &lt;code&gt;KToon&lt;/code&gt;, a TOON-style format that plugs directly into &lt;code&gt;kotlinx.serialization&lt;/code&gt; and works with your existing &lt;code&gt;@Serializable&lt;/code&gt; classes.&lt;/p&gt;

&lt;p&gt;One clean skeleton. Many possible applications.&lt;/p&gt;

&lt;p&gt;Next, I’ll walk through what makes TOON useful in practice and how KToon turns a normal data class into a dramatically smaller payload without changing a single line inside it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What TOON Really Is (and Why It Works)
&lt;/h2&gt;

&lt;p&gt;Think about a spreadsheet.&lt;/p&gt;

&lt;p&gt;You define your columns once id, name, age and then you just fill in the rows. No one would ever rewrite the column names on every line; that would be absurd. Yet that’s exactly what JSON does every time we serialize a list of structured objects.&lt;/p&gt;

&lt;p&gt;TOON flips that model on its head. It treats structured data collections as tables instead of repeated key-value objects. The structure lives once at the top. Everything below is just values.&lt;/p&gt;

&lt;p&gt;A simple example speaks louder than theory:&lt;/p&gt;

&lt;p&gt;JSON&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
  {"id": 1, "name": "Alice", "age": 30},
  {"id": 2, "name": "Bob", "age": 25}
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TOON&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users[2]{id,name,age}:
  1,Alice,30
  2,Bob,25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same meaning. Same structure.&lt;br&gt;
But dramatically fewer characters especially as datasets grow.&lt;/p&gt;

&lt;p&gt;The results scale fast: when you have thousands of rows, the headers stay exactly the same size, while JSON grows linearly with duplicated keys. TOON cuts the redundancy out of the serialisation pipeline entirely.&lt;/p&gt;

&lt;p&gt;And when data is hierarchical, not tabular, TOON switches into a clear, indentation-based representation instead of forcing everything into a grid. It’s human-readable without being verbose, and structured without being rigid.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Faster network responses (less data, fewer tokens)&lt;/li&gt;
&lt;li&gt;Lower costs in bandwidth-sensitive environments&lt;/li&gt;
&lt;li&gt;Smaller offline storage footprints&lt;/li&gt;
&lt;li&gt;Faster parsing, especially on constrained devices
A structure that aligns with how humans already think about lists
This idea isn’t abstract the LLM ecosystem already runs on token-efficient formats like this because it saves real money at scale. We’re simply borrowing a proven trick and applying it where mobile and multiplatform apps need it just as much.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How KToon Fits into kotlinx.serialization
&lt;/h2&gt;

&lt;p&gt;The best part about building KToon wasn’t inventing a new format.&lt;br&gt;
It was realizing we didn’t need to reinvent the entire serialization ecosystem to support it.&lt;/p&gt;

&lt;p&gt;Kotlin already gives us a powerful abstraction layer with kotlinx.serialization.&lt;br&gt;
At the core of that system are two extensibility points:&lt;/p&gt;

&lt;p&gt;an Encoder that defines how values are written, and&lt;br&gt;
a Decoder that defines how values are read back&lt;br&gt;
Everything else reflection, annotations, polymorphism, nested objects, nullability is handled by the framework. The serialization engine walks through your object graph and calls into whatever format implementation you provide.&lt;/p&gt;

&lt;p&gt;Meaning:&lt;br&gt;
If you implement your own encoder/decoder, you automatically get compatibility with every @Serializable data class without modifying any of them.&lt;/p&gt;

&lt;p&gt;That’s the breakthrough that made KToon possible.&lt;/p&gt;

&lt;p&gt;Rather than invent custom DSLs, code generators, or special DTO structures, KToon simply acts as a StringFormat implementation. The serialization machinery already knows how to traverse a list of User, or a ProductCatalog, or a tree of nested objects. KToon just decides how to write them.&lt;/p&gt;

&lt;p&gt;It’s the software equivalent of replacing the wheels without redesigning the car.&lt;/p&gt;

&lt;p&gt;You don’t rewrite @Serializable data class User(...) you tell serialization to express it differently.&lt;/p&gt;

&lt;p&gt;That’s what makes KToon a skeleton worth building on:&lt;br&gt;
thin, intentional, and flexible enough to power completely different applications without accumulating framework weight.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building the Engine, Encoder, Decoder, and Lexer
&lt;/h2&gt;

&lt;p&gt;The heart of KToon lives in three small but tightly synchronized components. Each exists for one purpose, nothing more. No grand abstractions, no “just in case” layers. Like bones in a good skeleton, they do less so the whole system can do more.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Encoder, Structure Without Buffering
&lt;/h2&gt;

&lt;p&gt;ToonEncoder extends Kotlin’s AbstractEncoder, and that’s where the magic really begins. As the serialization framework walks through the object graph, calling methods like encodeString(), encodeInt(), or beginStructure(), we decide what to write and when. The encoder runs a small state machine with four modes:&lt;/p&gt;

&lt;p&gt;IDLE — waiting for the next field&lt;br&gt;
ENCODING_STRUCTURE — writing nested objects using indentation&lt;br&gt;
ENCODING_COLLECTION — preparing to output a table&lt;br&gt;
ENCODING_VALUE — writing primitive values&lt;br&gt;
Encounter a list? Instead of holding the entire collection in memory, the encoder immediately delegates to ToonCollectionEncoder, which writes a header like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users[5]{id,name,email,age}:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then each element hands control to ToonRowEncoder, which writes CSV values line by line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1,Alice,alice@example.com,28
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The delegations are deliberate:&lt;br&gt;
the main encoder handles structure,&lt;br&gt;
the collection encoder handles headers,&lt;br&gt;
the row encoder handles values.&lt;/p&gt;

&lt;p&gt;No one knows more than it needs to. No buffering. No temporary model. Everything streams straight into a StringBuilder in one pass O(N) time and minimal memory footprint.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Decoder, Context-Aware Parsing and Real Error Messages
&lt;/h2&gt;

&lt;p&gt;Decoding is harder than encoding; anyone who has built a parser knows the pain. You’re not just reading you’re validating structure, honoring indentation, counting rows, matching types, and explaining mistakes gracefully.&lt;/p&gt;

&lt;p&gt;ToonDecoder uses a ToonLexer to tokenize input line by line, tracking indentation level and mapping fields back to properties. When it hits table mode, it verifies the declared size matches reality and parses each row with ToonRowDecoder.&lt;/p&gt;

&lt;p&gt;Its real superpower: human-readable error messages.&lt;/p&gt;

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

&lt;p&gt;Type mismatch at line 4, field 'age': cannot decode 'invalid' as Int&lt;br&gt;
Context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    2: users[5]{id,name,email,age}:
    3:   1,Alice,alice@example.com,28
&amp;gt;&amp;gt;&amp;gt;  4:   2,Bob,bob@example.com,invalid
    5:   3,Charlie,charlie@example.com,42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of stack traces, you get answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lexer Tokenizing With Indentation Awareness
&lt;/h2&gt;

&lt;p&gt;ToonLexer sits between raw text and structured understanding it splits lines, tracks indentation, and identifies patterns like headers, CSV values, and delimiters. Because TOON is line-oriented, indentation gives structure and table headers give shape.&lt;/p&gt;

&lt;p&gt;Separating lexing from decoding keeps both components clean, testable, and predictable.&lt;/p&gt;

&lt;p&gt;Plugging Into Ktor One Line to Swap Formats&lt;br&gt;
Once the engine existed, Ktor integration felt almost embarrassingly simple. Ktor already exposes an extension point for serialization formats via ContentConverter. We wrap the TOON engine and expose one toon() extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;install(ContentNegotiation) {
    json() // Keep JSON
    toon() // Add TOON
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Request Content-Type: application/toonget TOON.&lt;br&gt;
Request JSON get JSON.&lt;br&gt;
Same route code. Zero switching cost (on client side).&lt;/p&gt;

&lt;p&gt;Real-World Impact, 67.4% Fewer Tokens&lt;br&gt;
In our sample endpoint, JSON produced 553 bytes (~138 tokens).&lt;br&gt;
TOON produced 179 bytes (~45 tokens).&lt;br&gt;
A 67.4% reduction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk4o8yqowy3x6jbuw56e8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk4o8yqowy3x6jbuw56e8.png" alt="Demo Result" width="786" height="939"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the bigger the dataset, the bigger the gap.&lt;/p&gt;

&lt;p&gt;This isn’t theoretical efficiency:&lt;br&gt;
for inference pipelines sending product catalogs, search results, or event logs to LLMs, this means faster context windows and lower token bills.&lt;br&gt;
For mobile apps, it means snappier UX and lighter data usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Kiro Shaped the Development Process
&lt;/h2&gt;

&lt;p&gt;This skeleton didn’t emerge from hacking in the dark. It was shaped intentionally using Kiro’s spec-driven workflow.&lt;/p&gt;

&lt;p&gt;Before writing code, every major component encoder, decoder, lexer, Ktor integration lived first as words:&lt;br&gt;
requirements, architecture sketches, constraints, trade-offs.&lt;/p&gt;

&lt;p&gt;When decisions got messy, like how to handle nulls in tables or whether indentation levels should equal structure depth, MCP agents acted like design partners challenging assumptions, connecting dots, forcing clarity. They didn’t generate code. They generated understanding.&lt;/p&gt;

&lt;p&gt;That discipline is what kept KToon around 1000 lines of pure Kotlin instead of spiraling into a maze of abstractions.&lt;/p&gt;

&lt;p&gt;If KToon had been built “vibe-first”, it would have collapsed under its own cleverness.&lt;br&gt;
Built spec-first, it became a skeleton instead of spaghetti.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Skeleton Matters
&lt;/h2&gt;

&lt;p&gt;KToon doesn’t try to replace JSON everywhere.&lt;br&gt;
It exists to solve a very real problem: redundant field names in structured collections.&lt;/p&gt;

&lt;p&gt;The foundation is intentionally small:&lt;/p&gt;

&lt;p&gt;pure Kotlin, no platform code,&lt;br&gt;
single-pass O(N) performance,&lt;br&gt;
works with any existing @Serializable classes,&lt;br&gt;
one-line integration with Ktor,&lt;br&gt;
and a clean surface for future extensions (streaming, binary mode, Retrofit, etc.).&lt;br&gt;
Most importantly, it proves something worth remembering:&lt;/p&gt;

&lt;p&gt;See Project here: &lt;a href="https://github.com/JosephSanjaya/ktoon" rel="noopener noreferrer"&gt;JosephSanjaya/ktoon&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The techniques that make AI faster and cheaper don’t need to stay inside LLM labs.&lt;br&gt;
They can make our everyday apps better too.&lt;/p&gt;

</description>
      <category>kiro</category>
      <category>kotlin</category>
      <category>android</category>
      <category>development</category>
    </item>
    <item>
      <title>The Day Mentoring Felt Like Parenting: Lessons from The Explosive Child</title>
      <dc:creator>Joseph Sanjaya</dc:creator>
      <pubDate>Sun, 09 Nov 2025 17:41:27 +0000</pubDate>
      <link>https://forem.com/sanjayajoseph/the-day-mentoring-felt-like-parenting-lessons-from-the-explosive-child-59cm</link>
      <guid>https://forem.com/sanjayajoseph/the-day-mentoring-felt-like-parenting-lessons-from-the-explosive-child-59cm</guid>
      <description>&lt;p&gt;I didn’t plan to spend my weekend herding two middle schoolers through a game jam, but fate had other ideas.&lt;/p&gt;

&lt;p&gt;“Just mentor them a bit,” they said. “It’ll be fun!” they said.&lt;/p&gt;

&lt;p&gt;I’d been asked, last-minute, to mentor two middle schoolers eighth and ninth graders during a weekend “Build-a-thon.”&lt;/p&gt;

&lt;p&gt;I’m a senior Android engineer and occasional game development mentor, so I expected to spend 48 caffeine-fueled hours teaching arrays, tilesets, and maybe some basic game logic. Instead, I found myself mediating creative explosions, emotional whiplash, and a tug-of-war between two brilliant but combustible imaginations.&lt;/p&gt;

&lt;p&gt;Their project, which they proudly titled &lt;strong&gt;Eco-nomy&lt;/strong&gt;, was a digital board game about saving or exploiting the planet. One team played as &lt;strong&gt;Forest&lt;/strong&gt;, planting trees and cleaning pollution. The other, &lt;strong&gt;Industry&lt;/strong&gt;, mined resources and built factories. It was conceptually sharp an eco-capitalism duel. But the execution? Chaotic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hackathon Meltdown
&lt;/h2&gt;

&lt;p&gt;It started with pure energy whiteboard sketches, quick Godot prototypes, laughter, and instant noodle wrappers multiplying like power-ups. Then, as the caffeine wore off and the logic grew tangled, the temperature in the room rose.&lt;/p&gt;

&lt;p&gt;They fought about &lt;em&gt;everything&lt;/em&gt;. One wanted to build a full-blown &lt;strong&gt;card system&lt;/strong&gt;, complete with randomized draws, resource costs, and special abilities. The other just wanted a &lt;strong&gt;simple drag-and-drop mechanic&lt;/strong&gt; place tiles, take turns, keep it fast and visual.&lt;/p&gt;

&lt;p&gt;I tried to intervene like an engineer. I pointed at the backlog we’d written in Notion and said, “Let’s stick to the MVP.” That only made it worse. They accused each other of “ruining the vision.”&lt;/p&gt;

&lt;p&gt;By the second meltdown, I was googling phrases like &lt;em&gt;how to mentor teenagers without losing your sanity&lt;/em&gt;. That’s when I stumbled back on a book I’d read years ago but never &lt;em&gt;really&lt;/em&gt; applied outside of family life: Ross W. Greene’s &lt;em&gt;The Explosive Child&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It’s a parenting book. But in that chaotic moment, it felt like a software manual for emotional systems under load.&lt;/p&gt;

&lt;h2&gt;
  
  
  “Kids Do Well If They Can”
&lt;/h2&gt;

&lt;p&gt;Greene’s first and most famous principle is deceptively simple: &lt;em&gt;Kids do well if they can.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;He argues that kids aren’t difficult because they &lt;em&gt;want&lt;/em&gt; to be they’re difficult because they’re missing the skills to handle frustration, flexibility, or problem-solving in that moment.&lt;/p&gt;

&lt;p&gt;That line reframed everything. My mentees weren’t being defiant they were &lt;em&gt;overwhelmed&lt;/em&gt;. The scope was huge, the time was short, and their emotional bandwidth was thinner than their FPS counter.&lt;/p&gt;

&lt;p&gt;Once I looked through that lens, their arguments stopped feeling like personal resistance and started looking like &lt;em&gt;lagging skills&lt;/em&gt;. They weren’t refusing to collaborate they just hadn’t learned how to regulate disagreement yet.&lt;/p&gt;

&lt;p&gt;So instead of enforcing rules (“We’re sticking to this plan!”), I took a breath and tried empathy first. “Looks like both of you have strong ideas for the economy system,” I said. “What’s making it tricky to agree right now?”&lt;/p&gt;

&lt;p&gt;That single question changed the tone. They went from attacking each other to explaining themselves to me. One admitted, “I just don’t want my team to be boring.” The other confessed, “I’m scared we won’t finish if we keep adding stuff.”&lt;/p&gt;

&lt;p&gt;Beneath the shouting, there were valid emotions creativity and fear wrestling for control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Regulate Before Reasoning
&lt;/h2&gt;

&lt;p&gt;Greene emphasizes that logic is useless when someone’s brain is in &lt;em&gt;fight-or-flight&lt;/em&gt; mode. You can’t reason with a mind on fire.&lt;/p&gt;

&lt;p&gt;I learned this the hard way.&lt;/p&gt;

&lt;p&gt;At one point, one student was supposed to design the board tiles forests, factories, rivers while his teammate handled the game logic. But after nearly an hour, his screen was still blank except for a single half-drawn tree. His partner exploded: “We’re never going to finish at this pace!”&lt;/p&gt;

&lt;p&gt;The air thickened. The designer froze, eyes fixed on his tablet, shoulders tight. My instinct was to jump straight into “fix mode”: &lt;em&gt;Let’s review your workflow, optimize your sprite pipeline, speed this up.&lt;/em&gt; But the tension in the room told me logic would bounce off a wall of stress.&lt;/p&gt;

&lt;p&gt;So I changed tactics. “Let’s take a break,” I said. “Grab water. Stretch.” I turned the conversation away from the task asked about their favorite games instead. Ten minutes later, laughter returned. When we sat back down, the artist quietly opened his canvas and started sketching again. By the next hour, the board had color and personality.&lt;/p&gt;

&lt;p&gt;That was &lt;em&gt;regulate before reasoning&lt;/em&gt; in action. I learned that when creativity jams, pressure only tightens the knot. Sometimes the fastest way forward is stepping away not to escape the work, but to reset the brain behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lagging Skills, Not Bad Behavior
&lt;/h2&gt;

&lt;p&gt;Greene’s framework treats challenging moments as &lt;em&gt;unsolved problems&lt;/em&gt;, not acts of rebellion.&lt;/p&gt;

&lt;p&gt;Midway through the hackathon, I noticed a pattern. Whenever we hit a design disagreement, one student would quietly drift away from the project watching YouTube or sneaking in a few minutes of Minecraft. Meanwhile, his teammate would start panicking about the deadline, muttering, “He’s not even working anymore!”&lt;/p&gt;

&lt;p&gt;Initially, I saw it as procrastination. But when I thought in terms of &lt;em&gt;lagging skills&lt;/em&gt;, I recognized something else: they didn’t know how to handle scope stress. They weren’t lazy they lacked the executive function to break big problems into smaller ones.&lt;/p&gt;

&lt;p&gt;So I shifted my mentorship from “enforcer” to “coach.” I sat down and said, “Let’s split this into milestones. You finish the resource system while your teammate designs the board grid. Once both work, we merge.”&lt;/p&gt;

&lt;p&gt;Suddenly, momentum returned. They felt capable again because the task had been right-sized to their skill level.&lt;/p&gt;

&lt;p&gt;It reminded me of something Greene wrote: “Challenging behavior occurs when the demands being placed on a child exceed the child’s capacity to respond adaptively.” Substitute &lt;em&gt;developer&lt;/em&gt; for &lt;em&gt;child&lt;/em&gt;, and you’ve got a perfect postmortem for half of all failed sprints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collaborative Problem Solving (Plan B)
&lt;/h2&gt;

&lt;p&gt;Greene describes three “plans” adults can use when conflict arises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Plan A:&lt;/strong&gt; Impose your will.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Plan B:&lt;/strong&gt; Solve collaboratively.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Plan C:&lt;/strong&gt; Drop the issue temporarily.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a senior engineer, I realized I defaulted to Plan A far too often “We’ll do it this way because it’s the most efficient.” But hackathons (and children) don’t thrive on efficiency; they thrive on &lt;em&gt;ownership&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So I tried Plan B.&lt;/p&gt;

&lt;p&gt;During a heated debate over whether trees should generate gold or points, I resisted the urge to “decide as the adult.” Instead, I asked, “What would make it fair for both?”&lt;/p&gt;

&lt;p&gt;They thought. One suggested that trees generate gold, but only if forests stay healthy. The other proposed that pollution lowers the forest’s gold yield. Within minutes, they had invented an elegant feedback loop a mechanic I would have over-engineered into oblivion.&lt;/p&gt;

&lt;p&gt;Collaborative problem solving didn’t just fix the argument; it unlocked &lt;em&gt;innovation&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Turnaround
&lt;/h2&gt;

&lt;p&gt;By the final day, the chaos had finally found a shape. The game didn’t &lt;em&gt;look&lt;/em&gt; great the art was clunky, the colors uneven, and the tiles didn’t quite align but it &lt;em&gt;worked&lt;/em&gt;. Players could drag and drop tiles across the board, trying to form patterns that, when connected, triggered special abilities: a forest alignment would sap production points from Team Industry, while a chain of factories could choke the planet faster.&lt;/p&gt;

&lt;p&gt;It wasn’t elegant, but it was alive. The students, once at odds, were now playtesting side by side, laughing as they discovered unintended combos and quick-fixing bugs together.&lt;/p&gt;

&lt;p&gt;They even began resolving disagreements without me. “Let’s just test both versions,” one said. “We’ll keep whichever feels fairer.”&lt;/p&gt;

&lt;p&gt;They submitted &lt;strong&gt;Eco-nomy&lt;/strong&gt; minutes before the deadline. The judges smiled politely at the art but loved the idea a tiny eco-strategy born from two competing visions that somehow fused into one. It’s here if you want to see it:&lt;br&gt;
👉&lt;a href="https://devpost.com/software/eco-nomy-xz09w8" rel="noopener noreferrer"&gt;https://devpost.com/software/eco-nomy-xz09w8&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For me, the real victory wasn’t on the screen. It was in seeing those two explosive minds finally synchronize not because I took control, but because I learned to let go just enough for collaboration to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reflection
&lt;/h2&gt;

&lt;p&gt;In software teams, we often focus on &lt;em&gt;technical mentorship&lt;/em&gt;: code reviews, architecture decisions, performance tuning. But mentoring those two kids reminded me that leadership whether with junior developers or middle schoolers is fundamentally about &lt;em&gt;emotional scaffolding&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Greene’s principles translate almost perfectly into the engineering world:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;“Kids do well if they can”&lt;/strong&gt; becomes &lt;em&gt;“Developers do well if the system allows.”&lt;/em&gt; Struggles aren’t signs of incompetence; they’re signs of unmet skill development or unclear expectations.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;“Regulate before reasoning”&lt;/strong&gt; becomes &lt;em&gt;“Don’t debug while you’re burning out.”&lt;/em&gt; Step away before you escalate.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;“Collaborative problem solving”&lt;/strong&gt; becomes &lt;em&gt;“Pair-program your way out of conflict.”&lt;/em&gt; Shared ownership outperforms imposed hierarchy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mentorship, like parenting, isn’t about control it’s about creating the conditions for competence.&lt;/p&gt;

&lt;p&gt;When I think back to those two students their laughter, their messiness, their boundless conviction I realize &lt;em&gt;The Explosive Child&lt;/em&gt; isn’t just a parenting guide. It’s a philosophy of human development: that people grow best when they feel understood first, instructed second.&lt;/p&gt;

&lt;p&gt;And maybe that’s the secret to mentoring, managing, or even living well with others in high-pressure, creative work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;People do well if they can. And if they can’t yet, our job isn’t to punish it’s to teach.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;The Explosive Child by Dr. Ross Greene&lt;/p&gt;

</description>
      <category>mentorship</category>
      <category>hackathon</category>
      <category>gamedev</category>
      <category>godot</category>
    </item>
    <item>
      <title>The Subtle Art of Making Android Animations Feel Expensive</title>
      <dc:creator>Joseph Sanjaya</dc:creator>
      <pubDate>Thu, 30 Oct 2025 02:15:36 +0000</pubDate>
      <link>https://forem.com/sanjayajoseph/the-subtle-art-of-making-android-animations-feel-expensive-2lk1</link>
      <guid>https://forem.com/sanjayajoseph/the-subtle-art-of-making-android-animations-feel-expensive-2lk1</guid>
      <description>&lt;h2&gt;
  
  
  Designing Animations That Feel Expensive
&lt;/h2&gt;

&lt;p&gt;Most apps &lt;em&gt;move&lt;/em&gt;. Only a few &lt;em&gt;flow.&lt;/em&gt;&lt;br&gt;
You can feel it instantly, one looks like a slideshow, the other like choreography. The difference isn’t in how many lines of animation code it has, but in &lt;em&gt;how intentionally time and motion are used.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That sense of &lt;em&gt;flow&lt;/em&gt; is what separates hobby projects from products that feel “high-end.” When an animation lands just right, easing in, pausing at the right frame, fading naturally, it gives the user subconscious trust. It tells them: &lt;em&gt;this interface was crafted, not assembled.&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.amazonaws.com%2Fuploads%2Farticles%2Ff0s9vifpe0ptbon6872s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0s9vifpe0ptbon6872s.png" alt="captionless image" width="800" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Animation isn’t flair, it’s feedback.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Recently, &lt;strong&gt;I’ve been mentoring a young briliant developer still in Middle school at 10th grade, already building solid Android apps&lt;/strong&gt;. I’ve been teaching her how &lt;em&gt;motion design&lt;/em&gt;, when guided by research and design principles, can elevate an interface from functional to elegant.&lt;/p&gt;

&lt;p&gt;Once she began applying those principles, her apps stopped &lt;em&gt;looking like prototypes&lt;/em&gt; and started &lt;em&gt;feeling like products.&lt;/em&gt; That’s the real power of thoughtful animation: it quietly teaches the user that the experience was made by someone who understands design at a human level.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Animation Quality Matters
&lt;/h2&gt;

&lt;p&gt;Animation isn’t about &lt;em&gt;making things move&lt;/em&gt; it’s about &lt;em&gt;making things make sense.&lt;/em&gt;&lt;br&gt;
When transitions are timed well, users don’t just see movement; they understand continuity. The screen doesn’t “change,” it &lt;em&gt;flows&lt;/em&gt; into the next state. That’s why good motion design is often invisible your brain just accepts it as natural.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Good animation disappears. Bad animation demands attention.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every major design system from &lt;strong&gt;Material You&lt;/strong&gt; to &lt;strong&gt;Apple’s Human Interface Guidelines&lt;/strong&gt; treats animation as part of interaction design, not decoration. They talk about things like &lt;em&gt;spatial continuity&lt;/em&gt;, &lt;em&gt;momentum&lt;/em&gt;, and &lt;em&gt;temporal hierarchy&lt;/em&gt;.&lt;br&gt;
Translated to code, that means your easing curves, durations, and offsets are more than aesthetic choices they’re cognitive tools. They help the user’s eyes and brain track where elements go and what matters most in each moment.&lt;/p&gt;

&lt;p&gt;From a developer’s perspective, we’re effectively managing &lt;em&gt;perceived latency.&lt;/em&gt; A well-timed 400ms transition can feel faster than a 200ms one because it signals continuity instead of abrupt replacement.&lt;/p&gt;

&lt;p&gt;That’s why, when teaching animation design, I start with the fundamentals: rhythm, easing, and depth. Get those three right, and even a simple fade can feel like craftsmanship. Get them wrong, and no amount of particle effects will save it.&lt;/p&gt;
&lt;h2&gt;
  
  
  From Static Screens to Living Navigation
&lt;/h2&gt;

&lt;p&gt;During a mentoring session for my mentee’s Android project a beautifully structured app that just &lt;em&gt;lacked motion personality&lt;/em&gt; I introduced her to &lt;a href="https://github.com/raamcosta/compose-destinations" rel="noopener noreferrer"&gt;&lt;strong&gt;Ramcosta Destinations&lt;/strong&gt;&lt;/a&gt;.&lt;br&gt;
Not because the library itself is magical, but because it provides a clear structure to experiment with transitions without boilerplate. Once navigation felt manageable, I asked her to focus on &lt;em&gt;how&lt;/em&gt; the screen should move, not just &lt;em&gt;what&lt;/em&gt; it should show.&lt;/p&gt;

&lt;p&gt;That’s when we implemented this custom animation setup. It transformed plain navigation into something that &lt;em&gt;felt&lt;/em&gt; crafted each transition now carries a sense of direction and emotion.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Motion Core
&lt;/h2&gt;

&lt;p&gt;We began by defining three duration constants the backbone of rhythm for every animation in the app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private const val DURATION_LONG = 500
private const val DURATION_MEDIUM = 400
private const val DURATION_SHORT = 250
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of these as musical beats.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Long (500ms)&lt;/strong&gt; is the cinematic motion full attention, used for major transitions.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Medium (400ms)&lt;/strong&gt; keeps companion animations (fade, scale) in harmony.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Short (250ms)&lt;/strong&gt; makes the exit feel snappy; users notice arrivals more than departures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping them centralized makes later tuning simple if the whole app feels too “heavy,” you tweak these three values instead of 20 scattered numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Enter Transition “Make it Arrive with Grace”
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;override val enterTransition = {
    slideInHorizontally(
        initialOffsetX = { fullWidth -&amp;gt; fullWidth / 2 },
        animationSpec = tween(DURATION_LONG, easing = EaseOutQuart)
    ) + fadeIn(
        animationSpec = tween(DURATION_MEDIUM, easing = LinearOutSlowInEasing),
        initialAlpha = 0.05f
    ) + scaleIn(
        animationSpec = tween(DURATION_LONG, easing = EaseOutBack),
        initialScale = 0.90f
    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is to &lt;em&gt;guide the eye gently&lt;/em&gt; into the new screen.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Slide:&lt;/strong&gt; Using &lt;code&gt;fullWidth / 2&lt;/code&gt; makes the entry travel half the screen width enough to show motion, not overwhelm.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;EaseOutQuart:&lt;/strong&gt; Slows down sharply near the end that elegant “coast to stop” found in Apple and high-end app transitions.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Fade:&lt;/strong&gt; Starts almost invisible (0.05f), creating a smooth “emerge from depth” effect.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Scale:&lt;/strong&gt; Begins at 0.9 and slightly overshoots with &lt;code&gt;EaseOutBack&lt;/code&gt;, which mimics real-world elasticity a polished bounce that feels natural, not cartoonish.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;💡&lt;/em&gt; &lt;strong&gt;&lt;em&gt;Tips!&lt;/em&gt;&lt;/strong&gt;_&lt;br&gt;
Try testing with fullWidth / 1 instead of /2. You’ll see instantly how shorter distance makes transitions feel tighter and more luxurious._&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. Exit Transition “Drift Away, Don’t Disappear”
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;override val exitTransition = {
    slideOutHorizontally(
        targetOffsetX = { fullWidth -&amp;gt; -(fullWidth / 4) },
        animationSpec = tween(DURATION_MEDIUM, easing = EaseInOutCubic)
    ) + fadeOut(
        animationSpec = tween(DURATION_SHORT, easing = FastOutLinearInEasing)
    ) + scaleOut(
        animationSpec = tween(DURATION_MEDIUM, easing = EaseInOutCubic),
        targetScale = 0.96f
    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leaving should feel light and dismissive not dramatic.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Slide out only a quarter:&lt;/strong&gt; this asymmetry (half in, quarter out) keeps motion fast without visual fatigue.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;EaseInOutCubic:&lt;/strong&gt; smooth acceleration and deceleration, like a steady drift.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;FadeOut:&lt;/strong&gt; quick 250ms fade because once the new screen enters, the old one should vanish decisively.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;ScaleOut:&lt;/strong&gt; Shrinking to 0.96 subtly hints at distance, reinforcing the illusion of depth.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Pop Enter Transition “The Return”
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;override val popEnterTransition = {
    slideInHorizontally(
        initialOffsetX = { fullWidth -&amp;gt; -fullWidth / 3 },
        animationSpec = tween(DURATION_LONG, easing = EaseOutQuart)
    ) + fadeIn(
        animationSpec = tween(DURATION_MEDIUM, easing = LinearOutSlowInEasing),
        initialAlpha = 0.1f
    ) + scaleIn(
        animationSpec = tween(DURATION_MEDIUM, easing = EaseOutBack),
        initialScale = 0.93f
    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When navigating back, the motion should communicate “returning to where you came from.”&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Negative offset:&lt;/strong&gt; slides from the left to visually match back navigation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;EaseOutQuart + Back combo:&lt;/strong&gt; makes the returning screen feel like it “snaps back into place.”&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Slightly smaller scale (0.93f):&lt;/strong&gt; avoids the feeling that content is jumping too fast from offscreen.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Pop Exit Transition “Leaving the Spotlight”
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;override val popExitTransition = {
    slideOutHorizontally(
        targetOffsetX = { fullWidth -&amp;gt; fullWidth / 2 },
        animationSpec = tween(DURATION_LONG, easing = EaseInCubic)
    ) + fadeOut(
        animationSpec = tween(DURATION_SHORT, easing = FastOutLinearInEasing)
    ) + scaleOut(
        animationSpec = tween(DURATION_MEDIUM, easing = EaseInOutCubic),
        targetScale = 1.05f
    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The screen leaving backward should feel like it’s gently pushed away not abruptly removed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;EaseInCubic:&lt;/strong&gt; accelerates quickly, giving a sense of “slide offstage.”&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;ScaleUp (1.05f):&lt;/strong&gt; that tiny zoom outward makes the departing screen feel like it’s stepping away into depth.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;🧠&lt;/em&gt; &lt;strong&gt;&lt;em&gt;Note:&lt;/em&gt;&lt;/strong&gt;&lt;code&gt;_1.05f_&lt;/code&gt; &lt;em&gt;is the visual sweet spot larger values start to feel cartoonish; smaller ones become imperceptible.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fldy8ydolsdcv834m7gof.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fldy8ydolsdcv834m7gof.gif" alt="MIddle Schooler Project Transition “Revibes”" width="431" height="921"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Smooth State Transitions, Teaching Motion That Feels “Alive”
&lt;/h2&gt;

&lt;p&gt;In the same project where my mentee implemented those navigation transitions, I introduced her to the next level:&lt;br&gt;
making composable state transitions that feel like the UI &lt;em&gt;understands what just happened&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is where we used a custom setup built around &lt;code&gt;AnimatedContent&lt;/code&gt;.&lt;br&gt;
It handles the classic tri-state scenario — &lt;strong&gt;Loading → Content → Error&lt;/strong&gt; — but with movement that feels organic and consistent with the app’s overall animation language.&lt;/p&gt;
&lt;h3&gt;
  
  
  Code Setup: StateSwitcherAnimator
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Composable
fun &amp;lt;T&amp;gt; StateSwitcherAnimator(
    targetState: T,
    modifier: Modifier = Modifier,
    transitionType: AnimationTransitionType = AnimationTransitionType.Default,
    content: @Composable (T) -&amp;gt; Unit
) {
    AnimatedContent(
        targetState = targetState,
        modifier = modifier,
        transitionSpec = {
            when (transitionType.resolve(targetState)) {
                AnimationTransitionType.Error -&amp;gt; {
                    slideInVertically(
                        initialOffsetY = { -it },
                        animationSpec = tween(500, easing = FastOutSlowInEasing)
                    ) + fadeIn() togetherWith
                        slideOutVertically(
                            targetOffsetY = { -it },
                            animationSpec = tween(400, easing = FastOutSlowInEasing)
                        ) + fadeOut()
                }
                AnimationTransitionType.Loading -&amp;gt; {
                    scaleIn(
                        initialScale = 0.8f,
                        animationSpec = tween(400, easing = FastOutSlowInEasing)
                    ) + fadeIn(animationSpec = tween(400)) togetherWith
                        scaleOut(
                            targetScale = 0.8f,
                            animationSpec = tween(300, easing = FastOutSlowInEasing)
                        ) + fadeOut()
                }
                else -&amp;gt; {
                    slideInVertically(
                        initialOffsetY = { it / 2 },
                        animationSpec = tween(500, easing = FastOutSlowInEasing)
                    ) + fadeIn() togetherWith
                        slideOutVertically(
                            targetOffsetY = { it / 2 },
                            animationSpec = tween(400, easing = FastOutSlowInEasing)
                        ) + fadeOut()
                }
            }
        },
        label = "PremiumAnimatedContent"
    ) { state -&amp;gt;
        content(state)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  🧩 How It Works
&lt;/h3&gt;

&lt;p&gt;This function is basically a &lt;strong&gt;state-aware animation wrapper&lt;/strong&gt; it listens to changes in a “state machine” and animates between them in a way that visually communicates what’s happening.&lt;/p&gt;

&lt;p&gt;Each state transition type (Error, Loading, Default) is tuned with a specific &lt;strong&gt;motion profile&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Error State Vertical Slide from Top&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;slideInVertically(initialOffsetY = { -it }) + fadeIn()
slideOutVertically(targetOffsetY = { -it }) + fadeOut()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Errors drop in from above like a toast or alert entering the viewport.&lt;br&gt;
The &lt;code&gt;-it&lt;/code&gt; offset signals urgency, paired with a 500ms easing (&lt;code&gt;FastOutSlowInEasing&lt;/code&gt;) for smooth entry that still feels assertive.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;🎛️&lt;/em&gt; Why these values?&lt;em&gt;The 500 → 400 ms duration split ensures the entry feels slightly longer than the exit — attention-grabbing without overstaying.&lt;br&gt;
Using&lt;/em&gt; &lt;code&gt;_FastOutSlowInEasing_&lt;/code&gt; &lt;em&gt;softens the landing critical for keeping even an “error” message from feeling harsh.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2. Loading State Scale In-Out&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scaleIn(initialScale = 0.8f) + fadeIn()
scaleOut(targetScale = 0.8f) + fadeOut()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loading feels “breathing.”&lt;br&gt;
It scales up from 0.8x to full, suggesting preparation like data being assembled.&lt;br&gt;
The scale symmetry (in and out both to 0.8f) gives a rhythmic pulse.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;🧠&lt;/em&gt; Design reasoning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  0.8f keeps it subtle below that feels cartoonish.&lt;/li&gt;
&lt;li&gt;  Durations (400 → 300 ms) make loading snappy, because waiting UIs should never feel sluggish.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;FastOutSlowInEasing&lt;/code&gt; makes the motion ease naturally in both directions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Default / Content State Gentle Slide&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;slideInVertically(initialOffsetY = { it / 2 }) + fadeIn()
slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When switching between content states (say, from one composable to another), the animation should feel neutral neither alerting nor distracting.&lt;br&gt;
This half-distance slide keeps visual continuity while maintaining flow.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;💡&lt;/em&gt; Calculation insight:_&lt;br&gt;
Dividing by 2 keeps the movement within comfortable visual range (human eyes find 25–50% of screen height ideal for transitional motion)._&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  ⚙️ Supporting Enum Logic
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;AnimationTransitionType&lt;/code&gt; enum allows this system to stay &lt;strong&gt;context-aware&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enum class AnimationTransitionType {
    Error, Loading, Content, Default;
    fun &amp;lt;T&amp;gt; resolve(targetState: T): AnimationTransitionType { ... }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This layer lets any screen hook into the same animation language without repeating logic one pattern for every kind of stateful motion.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This abstraction mimics a design system the way Material defines color tokens, we’re defining&lt;/em&gt; &lt;strong&gt;&lt;em&gt;motion tokens.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  🧩 Integrating with Real UI States
&lt;/h3&gt;

&lt;p&gt;Finally, we bind everything using &lt;code&gt;ContentStateSwitcher&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;@Composable
fun ContentStateSwitcher(
    isLoading: Boolean,
    modifier: Modifier = Modifier,
    error: String? = null,
    actionButton: Pair&amp;lt;String, () -&amp;gt; Unit&amp;gt;? = null,
    onContent: @Composable () -&amp;gt; Unit
) {
    val targetState = when {
        !error.isNullOrBlank() -&amp;gt; "ERROR"
        isLoading -&amp;gt; "LOADING"
        else -&amp;gt; "CONTENT"
    }
    StateSwitcherAnimator(targetState, modifier) { state -&amp;gt;
        when (state) {
            "ERROR" -&amp;gt; GeneralError(error.orEmpty(), actionButton = actionButton)
            "LOADING" -&amp;gt; RevibesLoading()
            "CONTENT" -&amp;gt; onContent()
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives a unified place for handling async content instead of branching in multiple composables, you get one reactive motion system.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Every state change tells a story, animation makes sure users don’t miss the plot.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh9mpp95423bc1c0zygtf.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh9mpp95423bc1c0zygtf.gif" alt="MIddle Schooler Project State Change Animation “Revibes”" width="431" height="921"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts: The Feel of Enterprise Elegance
&lt;/h2&gt;

&lt;p&gt;I built these transitions to prove that small motion details can completely change how an app &lt;em&gt;feels&lt;/em&gt;. The goal wasn’t to make it flashy it was to make it believable.&lt;/p&gt;

&lt;p&gt;The duration values 250 ms, 400 ms, 500 ms aren’t random; they follow how humans perceive motion. Short transitions make interactions feel instant. Medium ones feel smooth and intentional. Long ones give breathing room for major context shifts.&lt;/p&gt;

&lt;p&gt;The easing curves &lt;strong&gt;EaseOutQuart&lt;/strong&gt;, &lt;strong&gt;EaseOutBack&lt;/strong&gt;, &lt;strong&gt;FastOutSlowIn&lt;/strong&gt; come from motion studies that model how physical objects move and settle. That’s why the animation feels grounded instead of robotic.&lt;/p&gt;

&lt;p&gt;Even the math sliding in from &lt;code&gt;width / 2&lt;/code&gt;, scaling from &lt;code&gt;0.9f&lt;/code&gt;helps create subtle parallax and depth. You’re not throwing screens around; you’re guiding the user’s focus. It’s cinematic, not theatrical.&lt;/p&gt;

&lt;p&gt;That balance is what gives an animation its &lt;em&gt;enterprise-level polish&lt;/em&gt;: it’s quiet, intentional, and human. The kind that makes people say, “I don’t know why, but this feels right.”&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;🌱&lt;/em&gt; &lt;strong&gt;&lt;em&gt;See It in Action Revibes&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;You can experience this motion system live in&lt;/em&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.carissa.revibes" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Revibes&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;on Google Play&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It’s built by my middle school mentee her first public release and focuses on raising environmental awareness and encouraging sustainable actions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Some rough edges? Sure. But that’s the magic of early creation purpose meeting experimentation. The animations you’ve seen above power its smooth, meaningful flow.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;🎓&lt;/em&gt; &lt;strong&gt;&lt;em&gt;Join My Mentoring Sessions on ADPList&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;I mentor Android developers for free through&lt;/em&gt; &lt;a href="https://adplist.org/mentors/joseph-sanjaya" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;ADPList&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt; &lt;em&gt;from students just starting out to professionals refining their motion design and architecture skills.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>android</category>
      <category>androiddev</category>
      <category>mobile</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>Quick &amp; Easy Glass Effects in Jetpack Compose</title>
      <dc:creator>Joseph Sanjaya</dc:creator>
      <pubDate>Sun, 26 Oct 2025 07:42:16 +0000</pubDate>
      <link>https://forem.com/sanjayajoseph/quick-easy-glass-effects-in-jetpack-compose-43l8</link>
      <guid>https://forem.com/sanjayajoseph/quick-easy-glass-effects-in-jetpack-compose-43l8</guid>
      <description>&lt;h2&gt;
  
  
  When the Design Brief Says “Make It Look Like Glass”
&lt;/h2&gt;

&lt;p&gt;The request came in during a design review:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“We want the new layout to feel&lt;/em&gt; glassy &lt;em&gt;something with depth, light, and a touch of transparency.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On paper, that sounds straightforward. In reality, it’s one of those UI challenges that sit right between &lt;em&gt;design language&lt;/em&gt; and &lt;em&gt;rendering logic&lt;/em&gt;. “Glassy” can mean a dozen different things depending on who says it. For designers, it’s about mood and perception. For engineers, it’s about how light and blur interact with the pixel pipeline.&lt;/p&gt;

&lt;p&gt;That’s where Jetpack Compose gets interesting. It gives us the tools to think in layers surfaces, gradients, and modifiers without breaking composability. So, when the brief landed, our goal wasn’t just to fake transparency; it was to &lt;strong&gt;build a real sense of depth&lt;/strong&gt; that behaves predictably and performs well.&lt;/p&gt;

&lt;p&gt;We quickly learned that depth isn’t about turning down the opacity. It’s about &lt;em&gt;how&lt;/em&gt; the background and foreground interact. The background softens; the foreground stays sharp. And when those two parts works in sync, your brain reads the surface as glass instead of just “see-through.”&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Depth happens when context blurs and focus stay clear.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From that insight, the task became clear: &lt;strong&gt;two layers&lt;/strong&gt; one for the blurred background, one for the crisp content.&lt;br&gt;
That’s the foundation of everything that follows.&lt;/p&gt;
&lt;h2&gt;
  
  
  Understanding What “Glass” Actually Means in UI
&lt;/h2&gt;

&lt;p&gt;Before touching a single line of Compose code, we had to answer one deceptively simple question:&lt;br&gt;
&lt;strong&gt;What does “glass” really mean in digital design?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When designers talk about glass, they’re usually referencing how &lt;em&gt;light&lt;/em&gt; behaves through a translucent surface not the surface itself. It’s not just about seeing what’s behind something; it’s about how the background becomes soft, how edges catch light, and how everything feels subtly three-dimensional.&lt;/p&gt;

&lt;p&gt;So, the challenge isn’t to make a transparent box.&lt;br&gt;
It’s to recreate how &lt;em&gt;glass interacts with its environment.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Real glass isn’t transparent it’s reactive.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That insight guided how we approached the layout in Compose.&lt;br&gt;
If you think about a piece of glass in the real world, it has two distinct layers of perception:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The background layer&lt;/strong&gt; the world seen &lt;em&gt;through&lt;/em&gt; the glass, slightly blurred, colors diffused.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The content layer,&lt;/strong&gt; reflections, borders, or text on top, perfectly sharp and readable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those two layers together are what convince the human eye that the surface has depth.&lt;/p&gt;

&lt;p&gt;To bring that logic into Compose, we needed a system where both layers could coexist independently:&lt;br&gt;
a blurred base for context and a crisp top for interaction.&lt;br&gt;
That separation would let us fine-tune realism adjusting how soft or strong the effect feels without reworking layouts.&lt;/p&gt;

&lt;p&gt;That became our design rule moving forward:&lt;br&gt;
every glass effect is built from at least two coordinated layers nothing more, nothing less.&lt;/p&gt;

&lt;p&gt;In the next section, we’ll turn that concept into code and show how &lt;code&gt;GlassContainer&lt;/code&gt; makes the illusion both reusable and predictable in Compose.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Layered Approach
&lt;/h2&gt;

&lt;p&gt;Once we understood glass as a layered material, the next question was how to express that elegantly in Compose.&lt;br&gt;
We didn’t want another “UI trick.” It had to be &lt;strong&gt;composable&lt;/strong&gt;, easy to theme, and lightweight enough to survive in production.&lt;/p&gt;

&lt;p&gt;That’s where the idea of &lt;code&gt;GlassContainer&lt;/code&gt; came in a layout that manages both the blurred background and the sharp content layer, while keeping its shape and boundaries consistent.&lt;/p&gt;

&lt;p&gt;The implementation turned out to be surprisingly simple once the layering concept was right.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“One layer sets the mood; the other defines the focus.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here’s the core structure that powers it all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Composable
fun GlassContainer(
    modifier: Modifier = Modifier,
    shape: Shape = RoundedCornerShape(16.dp),
    blurRadius: Dp = 12.dp,
    backgroundAlpha: Float = 0.15f,
    borderAlpha: Float = 0.3f,
    borderWidth: Dp = 1.dp,
    content: @Composable () -&amp;gt; Unit
) {
    val surfaceColor = MaterialTheme.colors.surface
    val borderColor = MaterialTheme.colors.border
    Box(modifier = modifier.clip(shape)) {
        // Background layer with blur
        Box(
            modifier = Modifier
                .matchParentSize()
                .blur(radius = blurRadius)
                .background(brush = createGlassGradient(surfaceColor, backgroundAlpha))
                .border(width = borderWidth, brush = createGlassBorderGradient(borderColor, borderAlpha))
        )

        // Content layer stays sharp
        content()
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The composable uses a &lt;code&gt;Box&lt;/code&gt; as the container, clipped to a shape. Inside it, we render two stacked layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The &lt;strong&gt;background layer&lt;/strong&gt; uses &lt;code&gt;Modifier.matchParentSize()&lt;/code&gt; to fill the container exactly.
It applies blur, gradient, and border the ingredients that give it that glassy diffusion.&lt;/li&gt;
&lt;li&gt; The &lt;strong&gt;content layer&lt;/strong&gt; renders normally, untouched by blur, ensuring text and icons stay crisp.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That single line —&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.matchParentSize()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— does most of the heavy lifting.&lt;br&gt;
It locks the blur area to the same dimensions as your content region, preventing misaligned softness or inconsistent edges.&lt;/p&gt;

&lt;p&gt;This approach has two big benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  It &lt;strong&gt;decouples design from effect.&lt;/strong&gt; You can drop any composable inside without worrying about visual integrity.&lt;/li&gt;
&lt;li&gt;  It &lt;strong&gt;stays composable-friendly.&lt;/strong&gt; No need for custom draw logic or external blur views.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Adding Realism with Gradients
&lt;/h2&gt;

&lt;p&gt;Once the blur was working, we noticed something subtle: the surface still looked too clean.&lt;br&gt;
It was transparent, sure but not &lt;em&gt;alive.&lt;/em&gt;&lt;br&gt;
Real glass has micro-variations in brightness and transparency that make it react to its surroundings. Without those, it just feels like a semi-transparent overlay.&lt;/p&gt;

&lt;p&gt;So, we started layering &lt;strong&gt;gradients&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Flat transparency looks artificial because real materials bend light unevenly.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of relying on a single color with reduced opacity, we introduced a vertical gradient.&lt;br&gt;
At the top, it’s slightly brighter mimicking light catching the upper edge.&lt;br&gt;
The middle stays neutral, and the bottom fades softly to maintain depth.&lt;/p&gt;

&lt;p&gt;That tiny adjustment changes how your eye perceives the surface:&lt;br&gt;
your brain reads it as curved, reflective, and &lt;em&gt;physical&lt;/em&gt; rather than flat.&lt;/p&gt;

&lt;p&gt;Here’s the helper we used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private fun createGlassGradient(baseColor: Color, alpha: Float): Brush {
    return Brush.verticalGradient(
        colors = listOf(
            baseColor.copy(alpha = alpha * 1.1f),  // Slightly brighter at top
            baseColor.copy(alpha = alpha),         // Base opacity
            baseColor.copy(alpha = alpha)          // Consistent bottom
        )
    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gradient works because it introduces just enough contrast for the viewer to infer lighting direction.&lt;br&gt;
It’s not something you consciously notice but it anchors the illusion in realism.&lt;/p&gt;

&lt;p&gt;Then we applied a similar idea to the border.&lt;br&gt;
Edges in real glass catch and scatter lighter, which creates that crisp rim glow designers love.&lt;br&gt;
A second vertical gradient for the border gives that highlight near the top and softens it toward the bottom.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private fun createGlassBorderGradient(borderColor: Color, alpha: Float): Brush {
    return Brush.verticalGradient(
        colors = listOf(
            borderColor.copy(alpha = alpha * 1.5f),  // Strong highlight
            borderColor.copy(alpha = alpha * 0.3f),  // Fade in middle  
            borderColor.copy(alpha = alpha * 0.8f)   // Subtle bottom
        )
    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Together, these gradients make a huge perceptual difference.&lt;br&gt;
The component suddenly stops looking like “just transparency” and starts to behave like a &lt;em&gt;material.&lt;/em&gt;&lt;br&gt;
It refracts light, separates itself from the background, and feels consistent across themes. Result after applying gradient:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feou7xys1t3t9izgr4ll0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feou7xys1t3t9izgr4ll0.png" alt="Preview" width="575" height="462"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Reusable Variations That Scale with Your Design
&lt;/h2&gt;

&lt;p&gt;After we got the glass effect looking right, the next challenge was consistency.&lt;br&gt;
We didn’t want each screen or designer to invent their own blur strength or alpha combination.&lt;br&gt;
The real power of Compose is that once a pattern is stable, it should scale effortlessly.&lt;/p&gt;

&lt;p&gt;So, we abstracted the effect into &lt;strong&gt;variations&lt;/strong&gt; light and heavy glass containers each tuned for a different purpose.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Consistency isn’t limitation; it’s leverage.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The lighter version is subtle good for overlays, cards, or backgrounds where content underneath should still peek through.&lt;br&gt;
The heavier version has stronger blur and opacity, ideal for modals or drawers where you need visual separation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Composable
fun LightGlassContainer(/* ... */) {
    GlassContainer(
        blurRadius = 6.dp,
        backgroundAlpha = 0.06f,
        borderAlpha = 0.12f
    ) { content() }
}
@Composable  
fun HeavyGlassContainer(/* ... */) {
    GlassContainer(
        blurRadius = 16.dp,
        backgroundAlpha = 0.12f,
        borderAlpha = 0.25f
    ) { content() }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This small abstraction turned out to be the real win.&lt;br&gt;
By treating &lt;em&gt;glass&lt;/em&gt; as a reusable composable, not a visual trick, we could easily adapt it across our app.&lt;/p&gt;

&lt;p&gt;Here’s a closing statement for your article, written in that same casual and smart style, which incorporates your animated background idea.&lt;/p&gt;
&lt;h2&gt;
  
  
  Taking It Further: Making the Glass Reactive
&lt;/h2&gt;

&lt;p&gt;We’ve successfully built a &lt;code&gt;GlassContainer&lt;/code&gt; that’s reusable, theme-friendly, and scales with our design system. We’ve separated the blurred background from the crisp content and added subtle gradients to mimic how real-world light catches an edge.&lt;/p&gt;

&lt;p&gt;But we can push this illusion one step further.&lt;/p&gt;

&lt;p&gt;Early on, we noted that “real glass isn’t transparent it’s reactive”. Our &lt;code&gt;GlassContainer&lt;/code&gt; looks great on a static background, but to make it feel truly alive, it needs something to &lt;em&gt;react to&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is where a dynamic background comes in. By placing a subtle, animated gradient &lt;em&gt;behind&lt;/em&gt; our glass components, the blur effect suddenly has shifting light to catch and diffuse. The glass no longer feels like a static overlay; it feels like a physical object interacting with a living environment.&lt;/p&gt;

&lt;p&gt;Here’s a quick way to create a “light bleed” animation, simplified to just the dark-mode colors that match our final design:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// --- Define our dark palette ---
// (Assuming these are defined in your theme)
val blue = Color(0xFF3A7DFF)
val midnight = Color(0xFF161A3C)
val grape = Color(0xFF5A4BFF)
val nearBlack = Color(0xFF0D0F21)
// --------------------------------
@Composable
fun AnimatedLightBleedBackground(
    modifier: Modifier = Modifier,
    animationDurationMs: Int = 8000,
    bleedIntensity: Float = 0.15f
) {
    val infiniteTransition = rememberInfiniteTransition(label = "lightBleedTransition")
    val animationProgress by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(
                durationMillis = animationDurationMs,
                easing = LinearEasing
            ),
            repeatMode = RepeatMode.Restart
        ),
        label = "lightBleedProgress"
    )
    val darkColors = remember(bleedIntensity) {
        createDarkModeColors(bleedIntensity)
    }
    Canvas(
        modifier = modifier.fillMaxSize()
    ) {
        // Use the provided custom drawing logic
        drawNaturalLight(
            animationProgress = animationProgress,
            colors = darkColors
        )
    }
}
private fun createDarkModeColors(bleedIntensity: Float): List&amp;lt;Color&amp;gt; {
    return listOf(
        blue.copy(alpha = bleedIntensity * 1.8f),
        midnight.copy(alpha = bleedIntensity * 1.2f),
        grape.copy(alpha = bleedIntensity * 0.8f),
        midnight.copy(alpha = bleedIntensity * 0.6f),
        nearBlack.copy(alpha = bleedIntensity * 0.4f),
        Color.Transparent,
        Color.Transparent
    )
}
/**
 * Draws a natural, multi-source light bleed effect that
 * animates gently.
 */
private fun DrawScope.drawNaturalLight(
    animationProgress: Float,
    colors: List&amp;lt;Color&amp;gt;
) {
    val lightSourceX = size.width * 0.85f
    val lightSourceY = size.height * 0.15f
    // Calculate animated positions for the primary light source
    val animatedX = lightSourceX + cos(animationProgress * 2 * Math.PI).toFloat() * size.width * 0.02f
    val animatedY = lightSourceY + sin(animationProgress * 2 * Math.PI * 0.7f).toFloat() * size.height * 0.015f
    // Define multiple light sources for a more complex, natural feel
    val lightSources = listOf(
        Offset(animatedX, animatedY), // Primary source
        Offset(animatedX - size.width * 0.05f, animatedY + size.height * 0.03f), // Secondary
        Offset(animatedX + size.width * 0.03f, animatedY - size.height * 0.02f)  // Tertiary
    )
    lightSources.forEachIndexed { index, lightSource -&amp;gt;
        // Vary the radius for each light source
        val radiusMultiplier = when (index) {
            0 -&amp;gt; 1.2f // Main light is largest
            1 -&amp;gt; 0.8f
            else -&amp;gt; 0.6f
        }
        // Vary the intensity (alpha) for each light source
        val intensityMultiplier = when (index) {
            0 -&amp;gt; 1.0f // Main light is brightest
            1 -&amp;gt; 0.6f
            else -&amp;gt; 0.4f
        }
        val radius = size.width * radiusMultiplier
        // Apply the intensity multiplier to the base colors
        val naturalColors = colors.map { color -&amp;gt;
            color.copy(alpha = color.alpha * intensityMultiplier)
        }
        val brush = Brush.radialGradient(
            colors = naturalColors,
            center = lightSource,
            radius = radius
        )
        drawRect(
            brush = brush,
            size = size
        )
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Final Combination
&lt;/h2&gt;

&lt;p&gt;Now, we put it all together.&lt;/p&gt;

&lt;p&gt;In our main screen, we use a &lt;code&gt;Box&lt;/code&gt; to create our layers. The &lt;code&gt;AnimatedLightBleedBackground&lt;/code&gt; goes at the very bottom. On top of that, we stack our &lt;code&gt;GlassContainer&lt;/code&gt; elements (like the "Account Information" and "Settings" cards).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiif192qikkwa66oro01b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiif192qikkwa66oro01b.png" alt="Final Combination" width="424" height="793"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The result is a UI that feels both deep and dynamic. The animated background provides just enough shifting light for the blurred layer of our glass cards to catch and diffuse. The content on top, like “Settings,” stays perfectly sharp and readable.&lt;/p&gt;

&lt;p&gt;This combination is what finally delivers on that initial brief. We didn’t just make a transparent box; we built a small, layered system that creates a believable and truly “glassy” effect.&lt;/p&gt;

</description>
      <category>jetpackcompose</category>
      <category>kotlin</category>
      <category>android</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Mastering Engineering Habits for Growth</title>
      <dc:creator>Joseph Sanjaya</dc:creator>
      <pubDate>Sun, 19 Oct 2025 05:58:48 +0000</pubDate>
      <link>https://forem.com/sanjayajoseph/mastering-engineering-habits-for-growth-22dh</link>
      <guid>https://forem.com/sanjayajoseph/mastering-engineering-habits-for-growth-22dh</guid>
      <description>&lt;p&gt;It’s my turn for our team’s mini tech talk this week. You know the drill someone’s gotta bring a topic that’s interesting enough to keep people from checking social media halfway through.&lt;/p&gt;

&lt;p&gt;I wanted to talk about something deeper than just tools or frameworks. Something that actually sticks.&lt;br&gt;
Coincidentally, I’m in the middle of finishing &lt;em&gt;Better Than Before&lt;/em&gt; by Gretchen Rubin, a book about mastering habits in everyday life.&lt;/p&gt;

&lt;p&gt;And as I was reading it, something clicked: everything Rubin writes about habits how we form them, why we break them, how tiny changes compound applies frighteningly well to how we grow as engineers.&lt;/p&gt;

&lt;p&gt;Think about it.&lt;br&gt;
The way we review pull requests, plan features, pick up new frameworks, or avoid touching that legacy module it’s all habit.&lt;br&gt;
We often mistake skill for experience, but the truth is, what really defines a great engineer is &lt;em&gt;how they build repeatable patterns of improvement.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What we do every day matters more than what we do once in a while.” — Gretchen Rubin&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, instead of another deep dive into architecture patterns or the latest AI framework, I thought why not talk about the habits that actually make us better engineers in this harsh, demanding tech world?&lt;/p&gt;

&lt;p&gt;This article isn’t about “hustling harder.” The industry already worships at that altar.&lt;br&gt;
It’s about designing better &lt;em&gt;engineering habits&lt;/em&gt; using Rubin’s five strategies:&lt;br&gt;
&lt;strong&gt;Scheduling&lt;/strong&gt;, &lt;strong&gt;First Steps&lt;/strong&gt;, &lt;strong&gt;The Lightning Bolt&lt;/strong&gt;, &lt;strong&gt;Monitoring&lt;/strong&gt;, and &lt;strong&gt;The One Coin Loophole&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;These are ideas from psychology and behavior science, reframed for the engineer who wants to build long-term mastery not burnout.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Engineering Grind Loop
&lt;/h2&gt;

&lt;p&gt;Most engineers don’t burn out because they’re lazy.&lt;br&gt;
They burn out because they’re &lt;em&gt;always on&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Every sprint bleeds into the next.&lt;br&gt;
Meetings stretch into “just one more task.”&lt;br&gt;
Even after shipping, our brains won’t stop benchmarking themselves against someone else’s Git activity.&lt;/p&gt;

&lt;p&gt;We live inside the machine and the machine never sleeps.&lt;/p&gt;

&lt;p&gt;The modern engineering world quietly worships &lt;strong&gt;hustle culture&lt;/strong&gt;.&lt;br&gt;
It’s the unspoken rule that your value equals your output.&lt;br&gt;
We glorify the developer who fixes bugs at midnight, the architect who “lives and breathes code,” the founder who hasn’t taken a weekend in years.&lt;/p&gt;

&lt;p&gt;It sounds noble until it becomes noise.&lt;br&gt;
Every podcast, every tweet, every LinkedIn post tells us to &lt;em&gt;optimize ourselves more.&lt;/em&gt;&lt;br&gt;
Ship faster. Learn Rust. Write threads. Publish side projects.&lt;br&gt;
It’s an infinite feedback loop of performance disguised as growth.&lt;/p&gt;

&lt;p&gt;But like any overloaded system, it eventually collapses.&lt;/p&gt;

&lt;p&gt;We try to fix it the way we fix everything else: with optimization.&lt;br&gt;
We buy new apps, reorganize Notion boards, experiment with productivity frameworks like they’re new libraries to import.&lt;br&gt;
For a while, it works. Then entropy takes over.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Habits are the invisible architecture of daily life.” — Gretchen Rubin&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That line hit me hard.&lt;br&gt;
Because in engineering, &lt;em&gt;architecture&lt;/em&gt; is everything and most of us are building our days on fragile, undocumented systems.&lt;/p&gt;

&lt;p&gt;We keep pushing hotfixes into our routines instead of designing them properly.&lt;br&gt;
We don’t test. We don’t monitor. And eventually, the system crashes.&lt;/p&gt;

&lt;p&gt;Hustle culture tells us the fix is to run faster.&lt;br&gt;
But the real fix is to &lt;strong&gt;redesign the system itself&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The world won’t slow down frameworks will keep updating, expectations will keep rising.&lt;/p&gt;

&lt;p&gt;So the question isn’t &lt;em&gt;how do I keep up?&lt;/em&gt;&lt;br&gt;
It’s &lt;em&gt;how do I build habits that keep me stable while everything else moves fast?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That’s where Rubin’s strategies come in not as motivational fluff, but as genuine design patterns for the self.&lt;br&gt;
Because if you can design reliable systems for distributed microservices, you can design one for your own well-being too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 1: The Power of Scheduling, Build Rituals Like Cron Jobs
&lt;/h2&gt;

&lt;p&gt;The first strategy Rubin talks about is &lt;em&gt;scheduling,&lt;/em&gt; the simple act of deciding &lt;em&gt;when&lt;/em&gt; you’ll do something, instead of endlessly debating &lt;em&gt;whether&lt;/em&gt; to do it.&lt;/p&gt;

&lt;p&gt;Sounds basic, right?&lt;br&gt;
But that tiny shift changes everything.&lt;/p&gt;

&lt;p&gt;In code, once you automate a process say a nightly build or a deployment you stop thinking about it. It just happens.&lt;br&gt;
Scheduling habits works the same way.&lt;br&gt;
Once something is on the calendar, your brain stops negotiating with itself.&lt;/p&gt;

&lt;p&gt;No more internal debates like &lt;em&gt;“Should I learn that new framework tonight?”&lt;/em&gt; or &lt;em&gt;“Maybe I’ll refactor later.”&lt;/em&gt;&lt;br&gt;
You’ve already made the decision once the system will execute it&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We can build habits by deciding not to decide.” — Gretchen Rubin&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s why the best engineers I know don’t rely on bursts of motivation.&lt;br&gt;
They rely on &lt;em&gt;rituals.&lt;/em&gt;&lt;br&gt;
A fixed morning slot for learning.&lt;br&gt;
A Friday hour blocked for cleaning up tech debt.&lt;br&gt;
A recurring calendar reminder for journaling ideas or documenting architecture.&lt;/p&gt;

&lt;p&gt;At first, these feel rigid. But in reality, they create freedom the same kind you get when automation removes repetitive tasks.&lt;/p&gt;

&lt;p&gt;Because if every decision costs energy, scheduling is how you write a &lt;em&gt;scheduler&lt;/em&gt; for your life.&lt;/p&gt;

&lt;p&gt;⚡ Practical Takeaway for Engineers&lt;/p&gt;

&lt;p&gt;Start small. Don’t build a new system overnight.&lt;br&gt;
Just pick &lt;em&gt;one&lt;/em&gt; recurring thing that matters and automate it.&lt;br&gt;
Maybe it’s a 15-minute slot to review your pull requests, or to read docs before daily stand-up.&lt;/p&gt;

&lt;p&gt;Make it non-negotiable.&lt;br&gt;
Make it boring.&lt;br&gt;
That’s the point.&lt;/p&gt;

&lt;p&gt;Once it runs smoothly, it becomes part of your mental infrastructure reliable, predictable, and beautifully unremarkable.&lt;/p&gt;

&lt;p&gt;That’s what a real habit looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 2: The First Step Start Small, Ship Something
&lt;/h2&gt;

&lt;p&gt;One of Gretchen Rubin’s most deceptively simple ideas is &lt;em&gt;The Strategy of the First Step&lt;/em&gt;.&lt;br&gt;
To begin a habit, you just need to begin. Not plan, not research tools for three days, not color-code a Notion board just &lt;em&gt;start.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As engineers, we’re world-class at avoiding first steps.&lt;br&gt;
We tell ourselves we’re “setting up the architecture” or “exploring options,” but half the time, we’re just over-preparing. Our brains crave the illusion of progress instead of the discomfort of actually building.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“The first step is small but once taken, it creates its own energy.” —&lt;/em&gt; Better Than Before&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s exactly what pushed me to create my own build logic repository:&lt;/p&gt;

&lt;p&gt;👉🏻 &lt;code&gt;[sjy-build-logic](https://github.com/Sanjaya-Inc/sjy-build-logic)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For years, I noticed how much friction existed every time I started a new Android project. Reconfiguring Gradle files. Re-adding dependencies I always use. Re-tweaking project structures. Each time, the &lt;em&gt;real&lt;/em&gt; first step actually writing code got delayed behind repetitive setup.&lt;/p&gt;

&lt;p&gt;So I built my own starting point.&lt;br&gt;
The repo now contains everything I usually need: dependency catalogs, Android Studio templates, project configurations from compile SDK to test frameworks all the boring parts automated.&lt;br&gt;
Now, spinning up a new project isn’t a mental mountain. It’s one command, and I’m straight into &lt;em&gt;momentum mode&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But here’s the best part: it &lt;strong&gt;updates itself&lt;/strong&gt;.&lt;br&gt;
Whenever the frameworks evolve or new stable versions drop, the setup refreshes automatically using &lt;code&gt;dependabot&lt;/code&gt;. That means I don’t waste energy keeping up I just start coding, always on the latest stack.&lt;/p&gt;

&lt;p&gt;That’s what Rubin means by mastering the first step: make it so easy, you can’t not start.&lt;br&gt;
It’s not about removing effort it’s about removing &lt;em&gt;resistance&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In an industry that glorifies “10x speed” and “hustle harder,” it’s easy to overlook the quiet power of small beginnings.&lt;br&gt;
But the truth is, mastery rarely starts with a grand launch. It starts with a repo, a file, a function and the courage to make it exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 3: The Lightning Bolt When Change Hits You Hard
&lt;/h2&gt;

&lt;p&gt;Every once in a while, something hits you so clearly that it changes how you work forever. Gretchen Rubin calls this &lt;em&gt;The Strategy of the Lightning Bolt&lt;/em&gt; a sudden moment of insight that makes a new habit not just appealing, but &lt;em&gt;inevitable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We’ve all felt it as engineers.&lt;br&gt;
&lt;strong&gt;That late-night realization that the architecture you’ve been defending is actually holding the team back.&lt;br&gt;
That one tech talk that makes you rethink how you’ve been testing.&lt;br&gt;
Or that single line in a pull request comment that rewires your entire approach to clean code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For me, one of those lightning-bolt moments was realizing how much friction was silently draining my creativity. I’d spend more time fighting boilerplate and build errors than building ideas.&lt;br&gt;
That’s what led me to systemize my own tooling, automate setup, and ultimately create &lt;code&gt;sjy-build-logic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But this time, the motivation didn’t come from planning it came from &lt;em&gt;frustration&lt;/em&gt;. And frustration, when channeled right, can be lightning in a bottle.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Some changes are gradual, but others happen in a flash when we’re struck by a powerful insight that alters everything.” —&lt;/em&gt; Better Than Before&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Lightning Bolt is rare, but when it strikes, it rewrites the rules of your game. It’s the spark behind every great open-source project, every bold refactor, every side project that suddenly consumes your weekend in the best way.&lt;/p&gt;

&lt;p&gt;In a culture that romanticizes &lt;em&gt;grind&lt;/em&gt;, we sometimes forget how valuable these moments of emotional clarity are.&lt;br&gt;
You can’t schedule them. You can’t automate them. But you can &lt;em&gt;recognize&lt;/em&gt; them and when they come, act fast before the feeling fades.&lt;/p&gt;

&lt;p&gt;Because the truth is, change doesn’t always require discipline. Sometimes it just requires electricity and the courage to follow it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 4: Monitoring Debug Yourself Like Production Code
&lt;/h2&gt;

&lt;p&gt;After a Lightning Bolt moment, motivation surges but it never lasts forever. That’s where Gretchen Rubin’s &lt;em&gt;Strategy of Monitoring&lt;/em&gt; steps in. She writes that “we manage what we monitor.”&lt;br&gt;
And honestly, no group understands that better than engineers.&lt;/p&gt;

&lt;p&gt;When something’s in production, we don’t just &lt;em&gt;hope&lt;/em&gt; it behaves well we instrument it, observe it, and set up alerts. We track uptime, latency, user engagement, crash rates.&lt;br&gt;
But when it comes to &lt;em&gt;ourselves&lt;/em&gt; our learning pace, our focus, our burnout risk we often fly blind.&lt;/p&gt;

&lt;p&gt;We forget that we &lt;em&gt;are&lt;/em&gt; the system.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Self-knowledge is the foundation of habit change.” —&lt;/em&gt; Better Than Before&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That realization hit me when I noticed I was constantly jumping between projects, convinced I was being “productive.” In truth, I was context-switching myself into exhaustion.&lt;br&gt;
So, I started debugging my own patterns. I tracked when I wrote my best code (mornings), when I got distracted (afternoons), and how often I overcommitted (always).&lt;/p&gt;

&lt;p&gt;Eventually, I began treating my workflow like an observability problem.&lt;br&gt;
Just like I’d analyze logs or performance metrics, I’d note down how long tasks &lt;em&gt;actually&lt;/em&gt; took compared to how long I &lt;em&gt;thought&lt;/em&gt; they would.&lt;br&gt;
Turns out, my mental estimates were off by a consistent 30–40%. Once I saw the data, it stopped being personal it became &lt;em&gt;debuggable&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The beautiful thing about monitoring is that it removes judgment.&lt;br&gt;
You don’t need to guilt-trip yourself for being tired or slow you just read the logs and optimize.&lt;br&gt;
That mindset shift alone changed everything. My calendar became a dashboard, not a to-do list. My progress stopped being about emotion and started being about evidence.&lt;/p&gt;

&lt;p&gt;In engineering, we love to say “if you can’t measure it, you can’t improve it.”&lt;br&gt;
That’s true for code and even truer for ourselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 5: The One Coin Loophole When “Just Once” Becomes a Bug
&lt;/h2&gt;

&lt;p&gt;Gretchen Rubin tells a story about a man who kept adding coins to a jar every day.&lt;br&gt;
One coin alone meant nothing but over time, it made him rich.&lt;br&gt;
The flip side is just as true: skipping “just one coin” doesn’t seem like a big deal… until it becomes a habit of exceptions.&lt;br&gt;
She calls this &lt;em&gt;The One Coin Loophole&lt;/em&gt; our tendency to tell ourselves that one small deviation doesn’t matter.&lt;/p&gt;

&lt;p&gt;Engineers know how that story ends.&lt;br&gt;
Because “just once” is how production bugs are born.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“What we do ‘just this once,’ we often do again and again.” —&lt;/em&gt; Better Than Before&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We see it all the time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  “I’ll skip writing this test, just this once.”&lt;/li&gt;
&lt;li&gt;  “I’ll push directly to main, just this once.”&lt;/li&gt;
&lt;li&gt;  “I’ll skip the code review it’s urgent.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s innocent, even logical in the moment. But the cost compounds quietly, like technical debt disguised as efficiency.&lt;br&gt;
The danger isn’t the exception it’s the normalization of it.&lt;/p&gt;

&lt;p&gt;I’ve seen this play out not just in code, but in habits too.&lt;br&gt;
Skip one personal retrospective. Miss one refactor sprint. Delay one cleanup task. Suddenly, the system you built to keep yourself sharp starts drifting out of sync.&lt;br&gt;
And like any complex system, once entropy sets in, recovery takes far more effort than maintenance ever did.&lt;/p&gt;

&lt;p&gt;That’s why I treat my own process like a living codebase.&lt;br&gt;
When I catch myself saying &lt;em&gt;“just this once,”&lt;/em&gt; I pause and ask:&lt;br&gt;
Would I accept this logic in production?&lt;/p&gt;

&lt;p&gt;Most of the time, the answer’s obvious.&lt;/p&gt;

&lt;p&gt;The One Coin Loophole isn’t about guilt it’s about &lt;em&gt;awareness&lt;/em&gt;. It’s the reminder that consistency, not intensity, defines growth.&lt;br&gt;
Habits, like systems, thrive on small correctnesses tiny acts of integrity repeated long enough to become invisible.&lt;/p&gt;

&lt;p&gt;And that’s how good engineers become great ones.&lt;br&gt;
Not by doing everything perfectly, but by refusing to let “just once” become “every time.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing: Building Momentum, Not Just Code
&lt;/h2&gt;

&lt;p&gt;At the end of the day, growth isn’t about doing &lt;em&gt;more&lt;/em&gt; things it’s about removing the friction between you and momentum. For me, that’s exactly why I built &lt;code&gt;[**sjy-build-logic**](https://github.com/Sanjaya-Inc/sjy-build-logic)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It’s not just another Gradle setup. It’s a &lt;strong&gt;living foundation&lt;/strong&gt; a repository that evolves automatically, preloaded with every dependency, Android Studio template, and project configuration I rely on daily. It means when an idea strikes, I don’t waste energy wiring things up I just &lt;strong&gt;create, run, and iterate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every engineer needs their own version of this: a launchpad that eliminates hesitation and keeps you focused on building things that matter. Because once you remove the friction, consistency follows naturally and that’s where mastery begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  References:
&lt;/h2&gt;

&lt;p&gt;Better Than Before: Mastering the Habits of Our Everyday Lives — Gretchen Rubin&lt;/p&gt;




&lt;p&gt;📚 &lt;em&gt;Originally published on &lt;a href="https://medium.com/vidio/editing-systems-not-sprints-a-guide-to-sustainable-engineering-habits-ce4c401a3867" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
💬 &lt;em&gt;I also share free mentoring sessions on &lt;a href="https://adplist.org/mentors/joseph-sanjaya" rel="noopener noreferrer"&gt;ADPList&lt;/a&gt;.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
🚀 &lt;em&gt;Let’s connect and talk about Android, Kotlin, and building great systems together!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>habit</category>
      <category>productivity</category>
      <category>softwaredevelopment</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Offline-First Challenge: Making CSV &amp; PDF Reports Right on Android</title>
      <dc:creator>Joseph Sanjaya</dc:creator>
      <pubDate>Sat, 18 Oct 2025 14:42:05 +0000</pubDate>
      <link>https://forem.com/sanjayajoseph/offline-first-challenge-making-csv-pdf-reports-right-on-android-3p4c</link>
      <guid>https://forem.com/sanjayajoseph/offline-first-challenge-making-csv-pdf-reports-right-on-android-3p4c</guid>
      <description>&lt;p&gt;Usually, when someone asks for a “report feature,” we Android devs breathe easy that’s backend territory. We send the data, wait for a neat PDF or CSV, and move on with our day.&lt;/p&gt;

&lt;p&gt;Not this time.&lt;/p&gt;

&lt;p&gt;The requirement came with one deceptively simple line: &lt;strong&gt;“It has to work fully offline.”&lt;/strong&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.amazonaws.com%2Fuploads%2Farticles%2F1n5oc8yuc4ky15ul7grh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1n5oc8yuc4ky15ul7grh.png" alt="captionless image" width="800" height="650"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No APIs. No internet. No backend wizardry. Everything had to happen locally, right inside the Android app.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Sometimes the hardest part of an offline app isn’t the lack of network it’s realizing the phone is your backend now.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What started as a “simple export button” turned into a deep dive through file permissions, document contracts, and Compose rendering quirks. Somewhere between debugging URI access and watching Compose draw on an invisible canvas, I realized this wasn’t just about exporting data it was about &lt;strong&gt;rethinking what ‘frontend’ really means&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: When Servers Step Aside
&lt;/h2&gt;

&lt;p&gt;Most Android apps live in a comfortable ecosystem data comes from APIs, heavy work happens on the server, and we just display the results beautifully.&lt;/p&gt;

&lt;p&gt;But take away the server, and everything changes. Suddenly, you’re not just designing UI you’re handling the entire workflow: data collection, structuring, rendering, and writing files to storage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“The moment your app goes offline, you stop being a client developer you become a system architect.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Offline-first development forces you to think differently. It’s not just about making things &lt;em&gt;work&lt;/em&gt; without the internet it’s about designing an experience that feels complete, even in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSV Generation: The Warmup
&lt;/h2&gt;

&lt;p&gt;Before diving into the PDF rabbit hole, there was the warm-up round generating CSV files. Compared to Compose and pagination math, this felt like a vacation.&lt;/p&gt;

&lt;p&gt;A CSV is basically structured text with commas. You don’t need fancy rendering or layout logic, just clean data and careful escaping.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“CSV generation is that rare Android task that behaves exactly how you expect it to.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Compose, you can kick off file creation with the &lt;strong&gt;Storage Access Framework&lt;/strong&gt;.&lt;br&gt;
This ensures users choose where the CSV goes, while your app writes safely through a provided URI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val csvLauncher = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.CreateDocument("text/csv"),
    onResult = { uri -&amp;gt;
        if (uri != null) {
            csvWriter.writeCsv(uri, transactions)
        }
    }
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When triggered (say, from a button click), this opens Android’s file picker so the user can pick the save location.&lt;/p&gt;

&lt;p&gt;Next, the writer itself lightweight, dependency-free, and easy to plug into any project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class CsvWriter(
    private val transactionCsvMapper: TransactionCsvMapper,
    private val context: Context
) {
    fun writeCsv(uri: Uri, transactions: List&amp;lt;ExportedTransaction&amp;gt;) {
        context.contentResolver.openOutputStream(uri)?.use { output -&amp;gt;
            val headers = TransactionCsvMapper.HEADERS
            val rows = transactions.map { transactionCsvMapper.toCsvRow(it) }            BufferedWriter(OutputStreamWriter(output)).use { writer -&amp;gt;
                writer.appendLine(headers.joinToString(","))
                for (row in rows) {
                    writer.appendLine(row.joinToString(",") { escapeCsv(it) })
                }
            }
        }
    }    
private fun escapeCsv(value: String): String {
        val needsQuotes = value.contains(",") || value.contains("\"") || value.contains("\n")
        return if (needsQuotes) "\"${value.replace("\"", "\"\"")}\"" else value
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s all you need. No external libraries, no fragile permissions hacks just clean Kotlin I/O.&lt;br&gt;
When you trigger the launcher, the system handles file creation and your writer takes care of the content.&lt;/p&gt;

&lt;p&gt;There’s a certain joy in that simplicity. No layouts, no rendering, no UI thread drama. Just data in, file out.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Built-in PDF Way
&lt;/h2&gt;

&lt;p&gt;Before we jumped into custom layouts, life was simple.&lt;br&gt;
Android already gives you a handy class called &lt;code&gt;PdfDocument&lt;/code&gt;you just draw directly onto a &lt;code&gt;Canvas&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here’s what a minimal version looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val pdfDocument = PdfDocument()
val pageInfo = PdfDocument.PageInfo.Builder(595, 842, 1).create()
val page = pdfDocument.startPage(pageInfo)
val canvas = page.canvas
val paint = Paint().apply { textSize = 16f }
canvas.drawText("Hello, PDF World!", 50f, 50f, paint)
pdfDocument.finishPage(page)
val file = File(context.cacheDir, "report.pdf")
pdfDocument.writeTo(FileOutputStream(file))
pdfDocument.close()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ &lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Runs fully offline.&lt;/li&gt;
&lt;li&gt;  Works fine for static text or simple receipts.&lt;/li&gt;
&lt;li&gt;  Lightweight and dependency-free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;❌ &lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  No layout engine you calculate everything manually.&lt;/li&gt;
&lt;li&gt;  Forget about Compose, dynamic grids, or pretty formatting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Custom-Designed PDFs with Compose
&lt;/h2&gt;

&lt;p&gt;Okay, now we’re getting into the fun (and slightly painful) part.&lt;/p&gt;

&lt;p&gt;When the requirement changed from &lt;em&gt;“just export a PDF”&lt;/em&gt; to &lt;em&gt;“make it look beautiful, like a real report”&lt;/em&gt;, the built-in &lt;code&gt;PdfDocument&lt;/code&gt; wasn’t enough anymore. You can draw text and rectangles but not full layouts, grids, or dynamic tables like Compose gives us.&lt;/p&gt;

&lt;p&gt;The dream:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“What if we could use the same Jetpack Compose UI that powers our app screen, and print that as a PDF?”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s exactly what we did.&lt;br&gt;
But to make it work, we first needed to define a precise &lt;strong&gt;page specification&lt;/strong&gt; because print layouts aren’t like pixels on a phone screen.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Defining the Page Specs
&lt;/h2&gt;

&lt;p&gt;Printing uses &lt;strong&gt;typographic points&lt;/strong&gt; instead of pixels, and your layout must scale correctly for real-world paper sizes like A4 or Letter.&lt;br&gt;
So we start by defining a &lt;code&gt;PageSpec&lt;/code&gt; and a &lt;code&gt;PdfPageConfig&lt;/code&gt; to handle this conversion neatly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enum class PageSpec(val widthPoints: Double, val heightPoints: Double) {
    A4(595.2755905511812, 841.8897637795277),
    LETTER(612.0, 792.0);
    fun toPx(dpi: Int): Pair&amp;lt;Int, Int&amp;gt; {
        val scale = dpi.toDouble() / 72.0
        val widthPx = round(widthPoints * scale).toInt()
        val heightPx = round(heightPoints * scale).toInt()
        return Pair(widthPx, heightPx)
    }
    companion object {
        fun fromLocale(locale: Locale): PageSpec {
            return when (locale.country.uppercase(Locale.ROOT)) {
                "US", "CA", "MX" -&amp;gt; LETTER
                else -&amp;gt; A4
            }
        }
    }
}
data class PdfPageConfig(
    val pageSpec: PageSpec = PageSpec.fromLocale(Locale.getDefault()),
    val dpi: Int = 300
) {
    fun getSizePx() = pageSpec.toPx(dpi)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets us dynamically adapt to &lt;strong&gt;paper type, locale, and print DPI&lt;/strong&gt; without hardcoding anything.&lt;br&gt;
It also ensures that when Compose renders off-screen, our elements will line up perfectly when printed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: Building the Composable Page
&lt;/h2&gt;

&lt;p&gt;The key trick here is when rendering to PDF, your Composable must match &lt;strong&gt;exactly&lt;/strong&gt; the size of a printed page no scrolling, no “infinite height”.&lt;/p&gt;

&lt;p&gt;That’s where &lt;code&gt;Modifier.requiredSize()&lt;/code&gt; comes in.&lt;br&gt;
Here’s how our export composable looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Composable
fun ExportTxPdf(
    fileName: String,
    transactions: PersistentList&amp;lt;ExportedTransaction&amp;gt;,
    pageIndex: Int,
    totalPages: Int,
    pdfPageConfig: PdfPageConfig
    modifier: Modifier = Modifier
) {
    val tableSpec = remember { ExportTxTableSpec.default() }
    val (pageWidthPx, pageHeightPx) = pdfPageConfig.getSizePx()
    val density = LocalDensity.current
    val pageWidthDp = with(density) { pageWidthPx.toDp() }
    val pageHeightDp = with(density) { pageHeightPx.toDp() }    
    Column(
        modifier = modifier
            .requiredSize(pageWidthDp, pageHeightDp)
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        ExportTxHeader(tableSpec)
        transactions.forEachIndexed { index, transaction -&amp;gt;
            val color = if (index % 2 == 0) Color(0xFFE9E6F7) else Color.White
            ExportTxTableItem(tableSpec, transaction, color = color)
        }
        Spacer(modifier = Modifier.weight(1f))
        ExportTxFooter(fileName, pageIndex, totalPages)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each page is rendered like a real paper same width, same height, all inside a Compose layout.&lt;/p&gt;

&lt;p&gt;No guesswork, no scaling errors.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;💡&lt;/em&gt; &lt;strong&gt;&lt;em&gt;Pro tip (quote block idea):&lt;/em&gt;&lt;/strong&gt;_&lt;br&gt;
“When you treat your PDF page as a Compose layout, you stop thinking in pixels you start designing paper.”_&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: Printing the Composables to PDF
&lt;/h2&gt;

&lt;p&gt;Once each Composable page is ready, we still need to &lt;em&gt;print&lt;/em&gt; it into a real PDF document.&lt;br&gt;
That means rendering the Compose view to a &lt;code&gt;Bitmap&lt;/code&gt;, then drawing it onto the &lt;code&gt;PdfDocument&lt;/code&gt; page canvas.&lt;/p&gt;

&lt;p&gt;Here’s the helper extension we use for that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private fun List&amp;lt;View&amp;gt;.saveAsPdf(outputStream: OutputStream) {
    if (isEmpty()) return
    val dpi = 300
    val pageSpec = PageSpec.A4
    val (pageWidthPx, pageHeightPx) = pageSpec.toPx(dpi)
    val document = PdfDocument()
    forEachIndexed { index, view -&amp;gt;
        val widthSpec = View.MeasureSpec.makeMeasureSpec(pageWidthPx, View.MeasureSpec.EXACTLY)
        val heightSpec = View.MeasureSpec.makeMeasureSpec(pageHeightPx, View.MeasureSpec.EXACTLY)
        view.measure(widthSpec, heightSpec)
        view.layout(0, 0, pageWidthPx, pageHeightPx)
        val bitmap = createBitmap(pageWidthPx, pageHeightPx)
        val canvas = Canvas(bitmap)
        view.draw(canvas)
        val pageInfo = PdfDocument.PageInfo.Builder(pageWidthPx, pageHeightPx, index + 1).create()
        val page = document.startPage(pageInfo)
        page.canvas.drawBitmap(bitmap, 0f, 0f, null)
        document.finishPage(page)
        bitmap.recycle()
    }
    document.writeTo(outputStream)
    document.close()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method converts a list of &lt;strong&gt;offscreen-rendered Compose views&lt;/strong&gt; into a print-ready, paginated PDF all offline, all on device.&lt;/p&gt;

&lt;p&gt;Now here’s the catch: Compose rendering &lt;strong&gt;must&lt;/strong&gt; happen on the main thread. You can’t just call it inside a background coroutine like your CSV export it’ll crash or hang.&lt;br&gt;
So, you need to offload heavy work like bitmap compression or file writing to &lt;code&gt;Dispatchers.IO&lt;/code&gt;, but keep the Compose snapshot creation on the UI thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏁 Wrapping It All Up
&lt;/h2&gt;

&lt;p&gt;All this work landed as a new feature in open-source app (Brain Wallet) that I actively maintained, now you can export transactions directly to CSV or beautifully styled PDFs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check it out here:&lt;/strong&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/josephsanjaya/YourAppName" rel="noopener noreferrer"&gt;&lt;em&gt;GitHub —&lt;/em&gt;&lt;/a&gt;&lt;a href="https://github.com/gruntsoftware/android" rel="noopener noreferrer"&gt;gruntsoftware/android: The open source code of Brainwallet Android&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Download Here:&lt;/strong&gt;&lt;br&gt;
👉&lt;a href="https://play.google.com/store/apps/details?id=ltd.grunt.brainwallet&amp;amp;hl=en-US&amp;amp;pli=1" rel="noopener noreferrer"&gt;Brainwallet®: Buy Litecoin — Apps on Google Play&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🌱 Takeaways
&lt;/h2&gt;

&lt;p&gt;To wrap it up neatly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  ✅ Compose can render beautiful PDFs but it must run on the &lt;strong&gt;UI thread&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  📐 Accurate page specs (points → pixels → Dp) are key for consistent layouts.&lt;/li&gt;
&lt;li&gt;  🧮 Handling pagination and page size manually is unavoidable but rewarding.&lt;/li&gt;
&lt;li&gt;  💾 CSV export is the easy win but custom PDFs are where the real learning begins.&lt;/li&gt;
&lt;li&gt;  🚀 And yes, you can now build full offline report systems &lt;em&gt;entirely on Android.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;📚 &lt;em&gt;Originally published on &lt;a href="https://medium.com/@sanjayajosep/offline-first-challenge-making-csv-pdf-reports-right-on-android-faf2ee7946dc" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
💬 &lt;em&gt;I also share free mentoring sessions on &lt;a href="https://adplist.org/mentors/joseph-sanjaya" rel="noopener noreferrer"&gt;ADPList&lt;/a&gt;.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
🚀 &lt;em&gt;Let’s connect and talk about Android, Kotlin, and building great systems together!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>androiddev</category>
      <category>kotlin</category>
      <category>mobile</category>
    </item>
    <item>
      <title>When Smart People Clash: What Engineering Taught Me About NVC</title>
      <dc:creator>Joseph Sanjaya</dc:creator>
      <pubDate>Fri, 17 Oct 2025 07:18:33 +0000</pubDate>
      <link>https://forem.com/sanjayajoseph/when-smart-people-clash-what-engineering-taught-me-about-nvc-4c7d</link>
      <guid>https://forem.com/sanjayajoseph/when-smart-people-clash-what-engineering-taught-me-about-nvc-4c7d</guid>
      <description>&lt;p&gt;My coworker gave a short tech talk today not about Kotlin or CI pipelines, but about &lt;strong&gt;Nonviolent Communication&lt;/strong&gt;. And it hit me. Hard.&lt;/p&gt;

&lt;p&gt;Because conflict in teams is one of those things we all quietly struggle with. No matter how good the tech stack is, no matter how clean the architecture once communication breaks down, everything else starts to rot.&lt;/p&gt;

&lt;p&gt;You’ve seen it before: a small disagreement turns into a cold silence. A code review starts feeling personal. A team that used to brainstorm freely suddenly just plays it safe.&lt;/p&gt;

&lt;p&gt;That talk made me realize something uncomfortable but true:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We spend our days debugging systems, yet we rarely stop to debug how we talk to each other.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Real Cost of Conflict
&lt;/h2&gt;

&lt;p&gt;Conflict doesn’t start loud it starts quiet. It starts with a sigh on workspace chats. A “fine, I’ll do it myself.” A meeting where someone clearly wants to say something but doesn’t.&lt;/p&gt;

&lt;p&gt;In engineering teams, these tiny moments stack up like unhandled exceptions. One unspoken frustration becomes two, then five, and before you know it, your high-performing team starts feeling like a collection of isolated freelancers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“When engineers stop talking honestly, teams don’t just lose velocity, they lose safety.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the scary thing? You can still ship features while your team is quietly falling apart. That’s what makes communication breakdowns so dangerous they don’t show up in metrics. You’ll still hit sprint goals, still merge PRs, still deliver builds. But underneath, trust is decaying.&lt;/p&gt;

&lt;p&gt;Soon, feedback feels like an attack. Architecture discussions turn into turf wars. The best people stop speaking up because it’s easier to stay quiet than to be misunderstood.&lt;/p&gt;

&lt;p&gt;And once that happens, even the cleanest codebase can’t save the culture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovering Nonviolent Communication
&lt;/h2&gt;

&lt;p&gt;At its core, NVC is simple: it’s a way of talking that separates &lt;em&gt;what happened&lt;/em&gt; from &lt;em&gt;how we feel about it&lt;/em&gt;, and &lt;em&gt;what we need&lt;/em&gt; from &lt;em&gt;what we demand.&lt;/em&gt;&lt;br&gt;
It’s built on four small but surprisingly powerful steps:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8xmhk9ry05fu5du8suus.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8xmhk9ry05fu5du8suus.png" alt="captionless image" width="800" height="695"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Observation&lt;/strong&gt;: describe what you see without judgment.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Feeling:&lt;/strong&gt; name how it makes you feel.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Need:&lt;/strong&gt; express the value or need behind that feeling.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Request:&lt;/strong&gt; ask for a concrete action, not a threat or demand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sounds easy, right? It’s not. Because our brains are wired to judge before we understand.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We’re quick to debug code, but slow to debug assumptions.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine a typical code review comment:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“You didn’t follow the architecture guidelines.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now, apply a bit of NVC thinking:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“I noticed the implementation doesn’t follow our shared architecture guideline. I feel concerned because consistency helps us maintain long-term readability. Could we revisit this approach together?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The difference isn’t just tone it’s intent. One message points a finger; the other opens a door.&lt;/p&gt;

&lt;p&gt;NVC doesn’t mean avoiding hard feedback or sugarcoating problems. It’s about &lt;em&gt;removing unnecessary violence&lt;/em&gt;, the subtle edge in our words that turns collaboration into conflict.&lt;/p&gt;

&lt;p&gt;And just like writing clean code, clarity here takes effort. The cleaner you express what you observe and need, the easier it is for others to understand your intent without defensiveness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don’t Debug People in Public
&lt;/h2&gt;

&lt;p&gt;One of the hardest lessons I’ve learned in engineering teams is that &lt;em&gt;how&lt;/em&gt; you deliver feedback matters as much as &lt;em&gt;what&lt;/em&gt; you’re saying.&lt;/p&gt;

&lt;p&gt;When feedback turns into a group event a thread, a meeting callout, or a “let’s all discuss this one person’s decision” moment it stops being about learning. It starts feeling like exposure.&lt;/p&gt;

&lt;p&gt;Public criticism, even if technically correct, lands like a personal attack.&lt;br&gt;
The person on the receiving end doesn’t hear your logic they hear the message beneath it: &lt;em&gt;“You don’t belong here.”&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.amazonaws.com%2Fuploads%2Farticles%2Fc7z21sjkeje9tpu4956v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7z21sjkeje9tpu4956v.png" alt="captionless image" width="800" height="642"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Once feedback feels like comparison, the goal shifts from improvement to survival.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nonviolent Communication teaches the opposite approach: empathy scales best one-on-one.&lt;/p&gt;

&lt;p&gt;Pull someone aside. Ask questions instead of assigning blame. Listen for intent before judging execution.&lt;/p&gt;

&lt;p&gt;Private space turns feedback from &lt;strong&gt;humiliation&lt;/strong&gt; into &lt;strong&gt;dialogue&lt;/strong&gt;. &lt;strong&gt;That’s where understanding lives and understanding is what keeps teams together.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Consequences of Ignoring This
&lt;/h2&gt;

&lt;p&gt;The thing about communication bugs is that they don’t crash immediately. They degrade slowly, like memory leaks in culture.&lt;/p&gt;

&lt;p&gt;Teams that don’t talk stop trusting.&lt;br&gt;
Teams that don’t trust stop collaborating.&lt;br&gt;
And teams that stop collaborating start quietly dying, even while the sprint board looks perfectly healthy.&lt;/p&gt;

&lt;p&gt;You start hearing phrases like, “Let’s just do it this way,” instead of, “What if we tried this?” Decisions become political. Feedback becomes personal. And brilliant engineers start looking for new jobs, not because of the tech, but because of the tone.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“You can fix bad code. You can’t fix a team that’s stopped talking.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Debugging Ourselves
&lt;/h2&gt;

&lt;p&gt;The more I think about it, the more it’s clear: most “technical” problems aren’t about code, they’re about people.&lt;/p&gt;

&lt;p&gt;Nonviolent Communication is basically &lt;strong&gt;clean code for conversations&lt;/strong&gt;. When words get messy, intent gets lost.&lt;/p&gt;

&lt;p&gt;Small rewrites go a long way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  “You should’ve known” → “I wish we’d talked earlier.”&lt;/li&gt;
&lt;li&gt;  “That’s wrong” → “Can you walk me through your thinking?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same message, totally different vibe.&lt;/p&gt;

&lt;p&gt;Conflict isn’t bad. Avoiding it is.&lt;br&gt;
Good teams argue they just do it safely.&lt;/p&gt;

&lt;p&gt;So next time things get tense, pause and ask yourself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What do I really feel, and how can I say it without blame?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That little pause? It’s like linting your words before pushing them to prod.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;Marshall Rosenberg’s &lt;em&gt;Nonviolent Communication&lt;/em&gt; book&lt;/p&gt;




&lt;p&gt;📚 &lt;em&gt;Originally published on &lt;a href="https://medium.com/vidio/when-smart-people-clash-what-engineering-taught-me-about-nvc-7333bbb80331" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
💬 &lt;em&gt;I also share free mentoring sessions on &lt;a href="https://adplist.org/mentors/joseph-sanjaya" rel="noopener noreferrer"&gt;ADPList&lt;/a&gt;.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
🚀 &lt;em&gt;Let’s connect and talk about Android, Kotlin, and building great systems together!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>teamwork</category>
      <category>communication</category>
      <category>codereview</category>
    </item>
  </channel>
</rss>
