<?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: Guillermo</title>
    <description>The latest articles on Forem by Guillermo (@gcaguilar).</description>
    <link>https://forem.com/gcaguilar</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%2F3821022%2Fa8c67aca-6d48-4c15-9f8f-5c1318499f24.gif</url>
      <title>Forem: Guillermo</title>
      <link>https://forem.com/gcaguilar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/gcaguilar"/>
    <language>en</language>
    <item>
      <title>How to Automate Google Play Releases Without Losing Your Mind</title>
      <dc:creator>Guillermo</dc:creator>
      <pubDate>Fri, 20 Mar 2026 10:35:25 +0000</pubDate>
      <link>https://forem.com/gcaguilar/how-to-automate-google-play-releases-without-losing-your-mind-139a</link>
      <guid>https://forem.com/gcaguilar/how-to-automate-google-play-releases-without-losing-your-mind-139a</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;Let’s be honest.&lt;/p&gt;

&lt;p&gt;Publishing an Android app to Google Play manually is already annoying…&lt;br&gt;&lt;br&gt;
but automating it? That’s where things usually get messy.&lt;/p&gt;

&lt;p&gt;If you’ve ever tried, you probably ended up with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a random JSON key sitting in your repo 😬
&lt;/li&gt;
&lt;li&gt;a CI pipeline you don’t fully trust
&lt;/li&gt;
&lt;li&gt;or a setup you’re afraid to touch because “it works… somehow”
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ve been there.&lt;/p&gt;

&lt;p&gt;So this post is the version I wish I had earlier:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;a clean, secure way to automate Google Play releases without hacks, without leaking secrets, and without overcomplicating everything.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&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%2F5giq1vuvj7exgspirazk.jpg" alt="meme" width="554" height="451"&gt;
&lt;/h2&gt;
&lt;h2&gt;
  
  
  The Key Idea
&lt;/h2&gt;

&lt;p&gt;Instead of storing a Google service account key in your repo (please don’t), we use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workload Identity Federation (WIF)
&lt;/li&gt;
&lt;li&gt;Service account impersonation
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In plain English:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your CI logs into Google using short-lived tokens — no permanent secrets needed.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  What You’ll Get
&lt;/h2&gt;

&lt;p&gt;By the end of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your app builds automatically
&lt;/li&gt;
&lt;li&gt;Your AAB gets uploaded to Google Play
&lt;/li&gt;
&lt;li&gt;No credentials stored in your repo
&lt;/li&gt;
&lt;li&gt;A setup you won’t be scared to touch later
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Quick Setup (Copy &amp;amp; Paste)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Define variables
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-project-id"&lt;/span&gt;
&lt;span class="nv"&gt;PROJECT_NUMBER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-project-number"&lt;/span&gt;
&lt;span class="nv"&gt;POOL_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt;
&lt;span class="nv"&gt;PROVIDER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ci-provider"&lt;/span&gt;
&lt;span class="nv"&gt;SERVICE_ACCOUNT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ci-publisher"&lt;/span&gt;
&lt;span class="nv"&gt;SERVICE_ACCOUNT_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ci-publisher@your-project-id.iam.gserviceaccount.com"&lt;/span&gt;
&lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-org/your-repo"&lt;/span&gt;
&lt;span class="nv"&gt;PACKAGE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"com.your.app"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Create service account
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam service-accounts create &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_ACCOUNT_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Create identity pool
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam workload-identity-pools create &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POOL_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"global"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Connect your CI (OIDC)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam workload-identity-pools providers create-oidc &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROVIDER_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"global"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workload-identity-pool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POOL_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--issuer-uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://token.actions.githubusercontent.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attribute-mapping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.ref=assertion.ref"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attribute-condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"assertion.repository == '&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Allow impersonation
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam service-accounts add-iam-policy-binding &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_ACCOUNT_EMAIL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"roles/iam.workloadIdentityUser"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"principalSet://iam.googleapis.com/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_NUMBER&lt;/span&gt;&lt;span class="s2"&gt;/locations/global/workloadIdentityPools/&lt;/span&gt;&lt;span class="nv"&gt;$POOL_ID&lt;/span&gt;&lt;span class="s2"&gt;/attribute.repository/&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Enable Play API
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud services &lt;span class="nb"&gt;enable &lt;/span&gt;androidpublisher.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. Configure Play Console (manual)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;Users and permissions&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add the service account email&lt;/li&gt;
&lt;li&gt;Give access to your app&lt;/li&gt;
&lt;li&gt;Grant publish permissions (internal track is enough)&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  8. Add CI secrets
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Workload Identity Provider&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;projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL/providers/PROVIDER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Service Account&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;ci-publisher@your-project-id.iam.gserviceaccount.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  9. Add CI workflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish Android to Play&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v*"&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@vX&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Authenticate&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/auth@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;workload_identity_provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.WIF_PROVIDER }}&lt;/span&gt;
          &lt;span class="na"&gt;service_account&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVICE_ACCOUNT }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew bundleRelease&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;r0adkll/upload-google-play@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;serviceAccountJson&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.auth.outputs.credentials_file_path }}&lt;/span&gt;
          &lt;span class="na"&gt;packageName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.your.app&lt;/span&gt;
          &lt;span class="na"&gt;releaseFiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app/build/outputs/bundle/release/*.aab&lt;/span&gt;
          &lt;span class="na"&gt;track&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  10. Trigger a release
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git tag v1.0.0
git push origin v1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Debug Checklist
&lt;/h2&gt;

&lt;p&gt;If something breaks, check this first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the provider pointing to the correct repo?&lt;/li&gt;
&lt;li&gt;Does your secret include &lt;code&gt;/providers/...&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Did you add &lt;code&gt;id-token: write&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Is the service account added in Play Console?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Auth works but upload fails
&lt;/h3&gt;

&lt;p&gt;Play Console permissions are wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auth fails completely
&lt;/h3&gt;

&lt;p&gt;Repository or branch does not match the provider condition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Token errors
&lt;/h3&gt;

&lt;p&gt;Missing &lt;code&gt;id-token: write&lt;/code&gt; permission.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This setup might look complex at first…&lt;br&gt;&lt;br&gt;
but once it’s in place, it just works.&lt;/p&gt;

&lt;p&gt;And more importantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no leaked keys
&lt;/li&gt;
&lt;li&gt;no fragile hacks
&lt;/li&gt;
&lt;li&gt;no future headaches
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just a clean pipeline doing its job.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Don’t store Google service account keys
&lt;/li&gt;
&lt;li&gt;Use Workload Identity Federation
&lt;/li&gt;
&lt;li&gt;Let CI impersonate a service account
&lt;/li&gt;
&lt;li&gt;Upload with a GitHub Action
&lt;/li&gt;
&lt;li&gt;Sleep better at night 😄&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>automation</category>
      <category>cicd</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Why `bizimobile` Feels Like a Real-World Kotlin Multiplatform Project, Not Just a Demo</title>
      <dc:creator>Guillermo</dc:creator>
      <pubDate>Sat, 14 Mar 2026 12:26:57 +0000</pubDate>
      <link>https://forem.com/gcaguilar/why-bizimobile-feels-like-a-real-world-kotlin-multiplatform-project-not-just-a-demo-hhf</link>
      <guid>https://forem.com/gcaguilar/why-bizimobile-feels-like-a-real-world-kotlin-multiplatform-project-not-just-a-demo-hhf</guid>
      <description>&lt;p&gt;When "just build the app" turns into four apps overnight&lt;/p&gt;

&lt;p&gt;We all know the feeling. The Android version is finally coming together, and then the inevitable request drops: &lt;em&gt;“Can we get this on iPhone too?”&lt;/em&gt; A week later, it’s &lt;em&gt;“What about Wear OS?”&lt;/em&gt; Then Apple Watch joins the chat. And because the universe has a sense of humor, someone asks, &lt;em&gt;“Could Siri or Assistant just answer quick questions?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Suddenly, you aren't building a single app anymore. You’re managing a whole ecosystem of apps, devices, APIs, permissions, sync rules, and UX expectations that desperately want to drift apart the second you look away.&lt;/p&gt;

&lt;p&gt;That’s exactly why the &lt;a href="https://github.com/gcaguilar/bizimobile" rel="noopener noreferrer"&gt;gcaguilar/bizimobile&lt;/a&gt; repository is so interesting. It’s not just another simple bike-station app for Zaragoza. It reads like a practical, battle-tested reference for shipping a cohesive product across Android, Wear OS, iOS, and Apple Watch—all while keeping the messy logic exactly where it belongs: shared. (GitHub)&lt;/p&gt;

&lt;h3&gt;
  
  
  The Pain: The headaches this repo actually solves
&lt;/h3&gt;

&lt;p&gt;The real challenge this project tackles isn't showing bike stations on a map. That’s the easy part. The hard part is everything that surrounds it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duplicated logic:&lt;/strong&gt; Writing the exact same business rules for Android and iOS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different UI stacks:&lt;/strong&gt; Managing entirely different interfaces for phones and watches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location limbo:&lt;/strong&gt; Dealing with users denying permissions or GPS taking forever to lock on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flaky APIs:&lt;/strong&gt; Relying on public civic data sources that aren't always in a great mood.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-device sync:&lt;/strong&gt; Making sure a "favorite" on your phone actually shows up as a "favorite" on your watch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice integrations:&lt;/strong&gt; Trying to add Siri or Assistant without accidentally building a second, parallel version of your app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;bizimobile&lt;/code&gt; tackles all of these head-on. By splitting responsibilities into &lt;code&gt;shared:core&lt;/code&gt;, &lt;code&gt;shared:mobile-ui&lt;/code&gt;, &lt;code&gt;androidApp&lt;/code&gt;, &lt;code&gt;wearApp&lt;/code&gt;, and &lt;code&gt;apple&lt;/code&gt;, it sends a strong architectural message: &lt;strong&gt;share the logic and as much UI as possible, but keep native shells for the parts that truly need native APIs.&lt;/strong&gt; The shared core uses Kotlin Multiplatform (KMP) with Ktor, serialization, Okio, and Metro DI. Meanwhile, the shared mobile UI is built with Compose Multiplatform and exported to iOS as static frameworks. (GitHub) The practical value here is huge: it stops the classic “same feature, four different implementations” trap before it even starts.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: Shared where it matters, native where it pays off
&lt;/h3&gt;

&lt;p&gt;What makes this repo so elegant is that it doesn’t force everything into a single, massive abstraction blender. Instead, it uses a highly pragmatic split:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Shared core for domain and data
&lt;/h4&gt;

&lt;p&gt;The shared core owns the models, repositories, configuration, platform contracts, API clients, station matching, settings, and assistant-resolution logic. In plain English: the rules of the app are written exactly once. (GitHub)&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Shared mobile UI for Android and iOS
&lt;/h4&gt;

&lt;p&gt;There’s a dedicated &lt;code&gt;shared:mobile-ui&lt;/code&gt; module that relies on the core, uses Compose Multiplatform, and gets exported as an iOS framework. Android loads that UI directly, while the Apple side imports &lt;code&gt;BiziMobileUi&lt;/code&gt; and wraps it with SwiftUI/App Intents glue. It’s the perfect middle ground—one product experience, without pretending iOS and Android are identical under the hood. (GitHub)&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Native shells for platform superpowers
&lt;/h4&gt;

&lt;p&gt;On Android, &lt;code&gt;MainActivity&lt;/code&gt; creates platform bindings, feeds them into the shared UI, parses shortcut payloads, and handles location permissions. Over on Apple, Swift files wrap the shared graph for Siri, App Intents, and WatchConnectivity. For Wear OS, the watch app builds the same shared graph but renders a watch-specific Compose UI. (GitHub)&lt;/p&gt;

&lt;p&gt;It’s the kind of architecture that feels grown-up: &lt;strong&gt;shared brain, native hands.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works under the hood
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1) The data layer is built for reality, not the happy path&lt;/strong&gt;&lt;br&gt;
One of the smartest details in the repo is how it fetches station data. It tries the official Zaragoza City Council feed first. If that fails, it quietly falls back to CityBikes. It handles pagination, merges results, filters out out-of-service stations, calculates your distance, and sorts by proximity. (GitHub)&lt;/p&gt;

&lt;p&gt;It’s an excellent example of defensive product engineering. It’s like carrying an umbrella in a city where weather apps lie to you—not glamorous, but deeply practical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) GPS gets a timeout (because users hate waiting)&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;StationsRepositoryImpl&lt;/code&gt; does something every app should do: it refuses to block the whole UI while waiting for a perfect GPS fix. It tries to get your location, but only gives it a short timeout. If it fails (or if the user says "nope" to permissions), it falls back to a default location in the city center. The user immediately gets useful data instead of staring at an endless loading spinner. (GitHub)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3) Favorites are actually synchronized, not just stored&lt;/strong&gt;&lt;br&gt;
Favorites are treated as shared user state. The repository saves a JSON snapshot locally, pulls the latest from the watch sync bridge, merges them, deduplicates, writes back to disk, and pushes the update back to the watch. On Apple platforms, it uses &lt;code&gt;WatchConnectivity&lt;/code&gt; to turn the watch-and-phone relationship into a real, two-way sync channel, not just a fake "companion app." (GitHub)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4) The Dependency Injection is intentionally boring&lt;/strong&gt;&lt;br&gt;
The repo uses Metro DI for compile-time dependency injection. If you haven't used compile-time DI, think of it like wiring a house before you move the furniture in: everything is securely connected ahead of time, rather than relying on runtime magic to find the right outlet. This keeps the shared layer completely portable without blinding it to native capabilities. (GitHub)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5) Voice shortcuts aren't just bolted on&lt;/strong&gt;&lt;br&gt;
Instead of duplicating logic for voice assistants, the shared core defines the actions ("nearest station", "station status", etc.) and handles the query matching. On Apple platforms, the &lt;code&gt;AppleShortcutRunner&lt;/code&gt; simply asks the shared KMP graph for the answers. This is exactly how you avoid the dreaded "Siri says one thing, but the app shows another" bug. (GitHub)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6) Wear OS is a first-class citizen&lt;/strong&gt;&lt;br&gt;
The Wear app isn't an afterthought. It builds the shared graph, refreshes data, and lets users browse stations and launch routes using a UI actually built for a watch screen. It proves a great point: shared logic doesn't mean shared screens; it means shared decisions. (GitHub)&lt;/p&gt;

&lt;h3&gt;
  
  
  Why you should study this (even if you don't care about bikes)
&lt;/h3&gt;

&lt;p&gt;The bike-network use case is just the vehicle. What you’re really looking at is a highly reusable blueprint for apps that need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-platform domain logic&lt;/li&gt;
&lt;li&gt;Shared mobile UI&lt;/li&gt;
&lt;li&gt;Native integrations at the edges&lt;/li&gt;
&lt;li&gt;Resilient API consumption&lt;/li&gt;
&lt;li&gt;Real watch/mobile state sync&lt;/li&gt;
&lt;li&gt;Assistant and shortcut support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even the CI/CD pipeline is mature, building Android, iOS, and watchOS jobs in parallel and distributing them through Firebase. That’s release-minded engineering, not toy-project behavior. (GitHub)&lt;/p&gt;

&lt;h3&gt;
  
  
  The Takeaway
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;bizimobile&lt;/code&gt; is worth your time because it showcases a version of Kotlin Multiplatform that is practical, not ideological. It proves you can share the heavy lifting, keep the native UX where it counts, and build something that actually behaves coherently across phones, watches, and voice assistants. (GitHub)&lt;/p&gt;

&lt;p&gt;If you get even one &lt;em&gt;"ah, that's a smart way to do it"&lt;/em&gt; moment out of it, go drop a star on the repo. The best technical examples aren't the ones shouting the loudest—they're the ones quietly solving the messes we deal with every day.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>mobile</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I got fed up with my city’s bike-share, so I built BiziData</title>
      <dc:creator>Guillermo</dc:creator>
      <pubDate>Thu, 12 Mar 2026 20:37:08 +0000</pubDate>
      <link>https://forem.com/gcaguilar/i-got-fed-up-with-my-citys-bike-share-so-i-built-bizidata-2mgj</link>
      <guid>https://forem.com/gcaguilar/i-got-fed-up-with-my-citys-bike-share-so-i-built-bizidata-2mgj</guid>
      <description>&lt;p&gt;Like many of you, I use bike-share system pretty regularly. And, honestly, it drives me nuts: either there’s not a single bike when I need one, or the station is packed and I can’t return mine. It feels inefficient—but is it really, or is it just bad luck?&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%2Fnpcymuwvau82bhbpfn3t.jpg" 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%2Fnpcymuwvau82bhbpfn3t.jpg" alt=" " width="550" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That nagging question led me to start tinkering with BiziData, a little side project to dig into the data behind the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters&lt;/strong&gt;&lt;br&gt;
Cities release mobility data all the time, but it’s usually buried in APIs or spreadsheets that are a pain to make sense of. So, I thought: What if we could actually see what’s going on?&lt;/p&gt;

&lt;p&gt;So I started exploring things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;How does bike availability change throughout the day?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Where do the biggest imbalances happen?&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What patterns emerge in how people move around?&lt;br&gt;
I’m also curious about how outside factors play into usage:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Do big events in the city spike demand?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How does weather mess with usage patterns?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How do other transport options (like buses or scooters) interact with bike-share?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Could changes in bike lanes or infrastructure make a difference?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The big picture? Using data to understand—and maybe even improve—how urban mobility works.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What’s built so far&lt;/strong&gt;&lt;br&gt;
Right now, BiziData pulls data from Zaragoza’s bike-share and turns it into a simple dashboard. It shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time station availability&lt;/li&gt;
&lt;li&gt;Historical usage trends&lt;/li&gt;
&lt;li&gt;Visualizations of system activity
Tech stack: Next.js, TypeScript, Prisma, Redis&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%2Fgvfkgp3toyp5imtagqfl.jpeg" 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%2Fgvfkgp3toyp5imtagqfl.jpeg" alt=" " width="800" height="762"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Could this work in other cities?&lt;/strong&gt;&lt;br&gt;
For now, it’s just Zaragoza—but here’s the cool part: a lot of bike-share systems (especially those run by Lyft) use similar tech. That means the same approach could work elsewhere, and even let us compare how different cities handle mobility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tough parts&lt;/strong&gt;&lt;br&gt;
Some things were harder than I expected:&lt;/p&gt;

&lt;p&gt;Building a reliable data pipeline from open APIs&lt;br&gt;
Figuring out which metrics actually matter (and not drowning in noise)&lt;br&gt;
Resisting the urge to scope-creep—there are so many directions to take this!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monetization? Still a mystery&lt;/strong&gt;&lt;br&gt;
&lt;del&gt;Honestly, I haven’t figured this out yet.&lt;/del&gt; I've been battling AI with the role of an investor who doesn't want to part with a penny, and it's won..But I do think there’s potential: if data can help cities manage bike-share better—like improving bike distribution, guiding infrastructure decisions, or cutting inefficiencies—everyone wins.&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%2Fyo9mb4wjhjs0uy5escxt.jpg" 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%2Fyo9mb4wjhjs0uy5escxt.jpg" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I’d love your thoughts!&lt;/strong&gt;&lt;br&gt;
If you’ve got experience with mobility data, bike-share systems, or just ideas, I’d love to hear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What metrics would you want to see in a mobility dashboard?&lt;/li&gt;
&lt;li&gt;Would cross-city comparisons be useful?&lt;/li&gt;
&lt;li&gt;Have you worked with similar data before?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out the repo if you’re curious: &lt;a href="https://github.com/gcaguilar/bizidashboard" rel="noopener noreferrer"&gt;https://github.com/gcaguilar/bizidashboard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spoiler: There’s more coming…&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;(Shhh… I’m already working on a mobile app to help you find the nearest station with available bikes or free slots. Because, let’s be real, nobody wants to walk five blocks just to find out they can’t park their bike.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading—and let me know what you think! 🚴‍♂️&lt;/p&gt;

</description>
      <category>data</category>
      <category>datascience</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
