<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Bunyamin's Tech & Mobile Lab]]></title><description><![CDATA[Bunyamin's Tech & Mobile Lab]]></description><link>https://bunyamintamar.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Bunyamin&apos;s Tech &amp; Mobile Lab</title><link>https://bunyamintamar.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 13 Oct 2026 18:22:36 GMT</lastBuildDate><atom:link href="https://bunyamintamar.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I Built an Offline-First Pomodoro & Eisenhower Matrix App in Flutter (With 87 Languages and Native PiP)]]></title><description><![CDATA[By Bünyamin Tamar (@bunyamintamar) — Solo Developer of PomoPlan

If you look at the productivity section of the Google Play Store or App Store, you'll notice a frustrating pattern: app fragmentation.
]]></description><link>https://bunyamintamar.hashnode.dev/how-i-built-an-offline-first-pomodoro-eisenhower-matrix-app-in-flutter-with-87-languages-and-native-pip</link><guid isPermaLink="true">https://bunyamintamar.hashnode.dev/how-i-built-an-offline-first-pomodoro-eisenhower-matrix-app-in-flutter-with-87-languages-and-native-pip</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Mobile apps]]></category><category><![CDATA[software architecture]]></category><category><![CDATA[Android]]></category><dc:creator><![CDATA[Bunyamin Tamar]]></dc:creator><pubDate>Fri, 09 Oct 2026 14:09:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac8f4e0a6679b5089afea72/e8f6a876-877f-40fe-9661-b69682ec8df8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>By Bünyamin Tamar (@bunyamintamar) — Solo Developer of PomoPlan</em></p>
<hr />
<p>If you look at the productivity section of the Google Play Store or App Store, you'll notice a frustrating pattern: <strong>app fragmentation</strong>.</p>
<p>To have an effective daily focus workflow, you usually have to juggle at least three different tools:</p>
<ol>
<li>A <strong>Pomodoro timer</strong> (like <em>Forest</em> or <em>Focus To-Do</em>).</li>
<li>A <strong>task manager</strong> (like <em>Todoist</em> or <em>TickTick</em>).</li>
<li>A <strong>habit tracker</strong> (like <em>Loop</em>).</li>
</ol>
<p>None of them talk to each other. Even worse, many of them require a mandatory cloud account just to count down 25 minutes, and lock basic features like custom interval sounds or tags behind a $40–$60/year subscription.</p>
<p>Frustrated by this, I spent the last year building <strong><a href="https://play.google.com/store/apps/details?id=com.pomoplan.pomoplan">PomoPlan</a></strong> as a solo developer using Flutter. In this post, I want to share the architectural decisions, native Android platform hurdles, and lessons learned building a heavy, production-ready local-first app.</p>
<hr />
<h2>1. Why We Chose Hive Over SQLite for Local-First Storage</h2>
<p>A productivity app lives and dies by its <strong>latency</strong>. If a user unlocks their phone to check off a task or pause a timer, they shouldn't stare at a splash spinner or wait for a cloud sync handshake.</p>
<p>We chose <strong>Hive</strong> (a lightweight, blazing-fast key-value database written in pure Dart) instead of SQLite or Room for several reasons:</p>
<ul>
<li><strong>Zero Native Overhead:</strong> Hive reads and writes directly to binary boxes on disk without SQLite bridge serialization delays.</li>
<li><strong>Instant Startup:</strong> Loading the entire day's tasks, active timer state, and habit streaks takes <strong>under 15 milliseconds</strong> on a cold start.</li>
<li><strong>Custom TypeAdapters:</strong> We designed 6 core TypeAdapters:<ul>
<li><code>Task</code> (typeId: 0)</li>
<li><code>Session</code> (typeId: 1)</li>
<li><code>SubTask</code> (typeId: 2)</li>
<li><code>Tag</code> (typeId: 3)</li>
<li><code>TaskList</code> (typeId: 4)</li>
<li><code>Habit</code> (typeId: 5)</li>
</ul>
</li>
</ul>
<p>Here is a simplified example of our reactive repository pattern with <strong>Riverpod</strong>:</p>
<pre><code class="language-dart">@riverpod
class TasksNotifier extends _$TasksNotifier {
  late final Box&lt;Task&gt; _tasksBox;

  @override
  List&lt;Task&gt; build() {
    _tasksBox = Hive.box&lt;Task&gt;(AppConstants.tasksBox);
    // Listen to box events for zero-overhead UI updates
    _tasksBox.listenable().addListener(_onBoxChanged);
    return _tasksBox.values.toList();
  }

  void _onBoxChanged() {
    state = _tasksBox.values.toList();
  }

  Future&lt;void&gt; toggleTask(String id) async {
    final task = _tasksBox.get(id);
    if (task != null) {
      task.isCompleted = !task.isCompleted;
      task.completedAt = task.isCompleted ? DateTime.now() : null;
      await task.save();
    }
  }
}
</code></pre>
<hr />
<h2>2. Solving Android Picture-in-Picture (PiP) and Floating Window</h2>
<p>One of the biggest flaws with existing mobile Pomodoro apps is that they <strong>lock you into their screen</strong>. If you're reading a PDF, taking notes in Notion, or browsing code on your phone/tablet, you can't see how much focus time remains.</p>
<p>On Android, we implemented a dual approach:</p>
<ol>
<li><strong>Picture-in-Picture (PiP Mode):</strong> When the user presses the home button during an active focus session, the Flutter Activity transitions into a native Android PiP window showing the radial countdown.</li>
<li><strong>Floating Pill Overlay (<code>SystemAlertWindow</code>):</strong> A draggable, compact floating timer bubble that sits on top of any app.</li>
</ol>
<p>Here is the Kotlin hook in our <code>MainActivity.kt</code>:</p>
<pre><code class="language-kotlin">private fun updatePictureInPictureParams(timeLeftFormatted: String, isRunning: Boolean) {
    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.O) {
        val aspectRatio = Rational(1, 1)
        val params = PictureInPictureParams.Builder()
            .setAspectRatio(aspectRatio)
            .build()
        setPictureInPictureParams(params)
    }
}
</code></pre>
<p>This single feature completely transformed user retention — users no longer have to constantly switch back to the timer app to check their remaining focus time.</p>
<hr />
<h2>3. The 3:00 AM Day Boundary (For Night Owls)</h2>
<p>Most productivity apps reset daily streaks, "Today" smart lists, and habit trackers strictly at <strong>00:00 midnight</strong>.</p>
<p>If you've ever studied for an exam or coded until 1:30 AM, you know how demoralizing this is: your timer resets, your daily task list empties out, and your streak registers as broken before you've even gone to sleep.</p>
<p>To fix this, we created a custom <code>PomoDateExtension</code>:</p>
<pre><code class="language-dart">extension PomoDateExtension on DateTime {
  /// Shifts day boundary back by 3 hours.
  /// 02:30 AM on Oct 10th belongs to Oct 9th productivity day.
  DateTime get effectiveDate {
    return subtract(const Duration(hours: 3));
  }

  DateTime get effectiveStartOfDay {
    final d = effectiveDate;
    return DateTime(d.year, d.month, d.day, 3, 0, 0);
  }
}
</code></pre>
<p>Every streak calculation, daily goal check, and habit counter uses <code>effectiveDate</code> rather than <code>DateTime.now()</code>.</p>
<hr />
<h2>4. Scaling Localization to 87 Languages</h2>
<p>We wanted PomoPlan to be truly global. Instead of relying on heavy third-party translation cloud services at runtime, we implemented a static JSON-based localization asset pipeline:</p>
<ul>
<li>87 language files stored under <code>assets/translations/</code> (<code>en-US.json</code>, <code>tr-TR.json</code>, <code>de-DE.json</code>, <code>ja-JP.json</code>, etc.).</li>
<li>Dynamic fallback system: if a newly added key is missing in a minor dialect, it transparently falls back to <code>en-US</code> without crashing or showing blank labels.</li>
<li>A custom benchmark test checks all 87 translation files in <strong>under 30 milliseconds</strong> before every release build.</li>
</ul>
<hr />
<h2>5. Summary &amp; Key Takeaways</h2>
<p>Building a local-first mobile app in 2026 has been an incredible journey:</p>
<ol>
<li><strong>Privacy is a competitive advantage:</strong> Users are tired of creating an account just to use basic utilities. By keeping all data strictly on the device (with optional encrypted Google Drive backup), trust is built instantly.</li>
<li><strong>Offline-first means 60 FPS:</strong> Because there are no HTTP waterfalls or network loaders on the main screens, animations and transitions remain buttery smooth even on low-end hardware.</li>
<li><strong>Solve real workflows, not isolated gimmicks:</strong> Unifying Pomodoro with Kanban, Eisenhower Priority Matrix, and habit streaks solves the "app sprawl" problem that millions of students and professionals struggle with.</li>
</ol>
<hr />
<h3>📲 Check It Out:</h3>
<ul>
<li><strong>Google Play:</strong> <a href="https://play.google.com/store/apps/details?id=com.pomoplan.pomoplan">PomoPlan on Google Play</a></li>
<li><strong>Uptodown (Direct APK):</strong> <a href="https://pomoplan-pomodoro-focus-timer.en.uptodown.com/android">PomoPlan on Uptodown</a></li>
<li><strong>APKPure:</strong> <a href="https://apkpure.com/p/com.pomoplan.pomoplan">PomoPlan on APKPure</a></li>
<li><strong>Official Website:</strong> <a href="https://bunyamintamar.github.io/pomoplan-web/">pomoplan.app</a></li>
</ul>
<p>I'd love to hear your thoughts on local-first Flutter architecture, Hive vs. Isar/Drift, or native platform overlays. Feel free to leave a comment or ask questions below!</p>
]]></content:encoded></item></channel></rss>