<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hamber</title>
    <description>The latest articles on DEV Community by Hamber (@hamberluo).</description>
    <link>https://dev.to/hamberluo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3505361%2Faa670900-da4d-43f0-98ed-c86485758de1.png</url>
      <title>DEV Community: Hamber</title>
      <link>https://dev.to/hamberluo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hamberluo"/>
    <language>en</language>
    <item>
      <title>Modern Flutter Best Practices for 2026</title>
      <dc:creator>Hamber</dc:creator>
      <pubDate>Sat, 04 Jul 2026 14:35:55 +0000</pubDate>
      <link>https://dev.to/hamberluo/modern-flutter-best-practices-for-2026-56o3</link>
      <guid>https://dev.to/hamberluo/modern-flutter-best-practices-for-2026-56o3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;If you're starting a new Flutter project in 2026, this article covers two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What a modern Flutter project should look like (tech stack, architecture, engineering);&lt;/li&gt;
&lt;li&gt;The trade-offs between an OPC (One Person Company) project and an enterprise-grade production project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything below is grounded in two real projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;go_gba&lt;/strong&gt; — a GBA emulator App built by a single developer (single package, shipped on the App Store / Google Play, now at v3.12). A textbook OPC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AppX&lt;/strong&gt; (pseudonym) — a high-traffic consumer App maintained by a large cross-platform team (melos monorepo, multiple flavors, multiple environments). A textbook enterprise-grade project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both projects share the same modern Flutter foundation, but make completely opposite bets on "complexity investment." Comparing them tells you far more about &lt;em&gt;when to reach for the heavy machinery&lt;/em&gt; than looking at either one alone.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  0. The Bottom Line (TL;DR)
&lt;/h2&gt;

&lt;p&gt;In 2026, several choices have moved from "optional" to "default":&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;2026 default&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;State management&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Riverpod 3&lt;/strong&gt; (+ code generation)&lt;/td&gt;
&lt;td&gt;Compile-time safety, testable, no BuildContext dependency — the de facto community standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routing&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;go_router&lt;/strong&gt; (declarative + type-safe routes)&lt;/td&gt;
&lt;td&gt;Officially recommended; deep links / nested navigation are first-class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data models&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;freezed 3&lt;/strong&gt; + json_serializable&lt;/td&gt;
&lt;td&gt;Immutable, pattern matching, &lt;code&gt;copyWith&lt;/code&gt; — kills hand-written boilerplate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i18n&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;slang&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Type-safe, compile-time checked, an order of magnitude nicer than the official &lt;code&gt;intl&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version pinning&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;fvm&lt;/strong&gt; (pin the Flutter SDK version)&lt;/td&gt;
&lt;td&gt;The end of "works on my machine"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code generation&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;build_runner&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;freezed / riverpod / slang / routes / assets all depend on it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lint&lt;/td&gt;
&lt;td&gt;Strict lint + &lt;strong&gt;custom lint rules&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Turns team conventions into machine-enforceable rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crash / analytics&lt;/td&gt;
&lt;td&gt;Firebase Crashlytics + Analytics (or equivalent)&lt;/td&gt;
&lt;td&gt;A production App without observability is flying blind&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Details below.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Tech Stack: The 2026 "Standard Recipe"
&lt;/h2&gt;

&lt;p&gt;The two projects agree closely on their core libraries, which itself is evidence that consensus has formed.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 State Management — Riverpod 3
&lt;/h3&gt;

&lt;p&gt;Stop agonizing over Provider / Bloc / GetX. &lt;strong&gt;Riverpod 3 is already the default answer for new projects.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;go_gba: &lt;code&gt;flutter_riverpod: 3.3.2&lt;/code&gt;, hand-written providers.&lt;/li&gt;
&lt;li&gt;AppX: &lt;code&gt;riverpod_generator&lt;/code&gt; + &lt;code&gt;riverpod_annotation&lt;/code&gt;, full code generation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference is &lt;strong&gt;whether you adopt code generation&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Hand-written (good enough for OPC, directly readable)&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;gameLibraryProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FutureProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;((&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gameRepositoryProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Generated (recommended for enterprise; stricter types, safer refactors)&lt;/span&gt;
&lt;span class="nd"&gt;@riverpod&lt;/span&gt;
&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gameLibrary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GameLibraryRef&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gameRepositoryProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Recommendation&lt;/strong&gt;: Go straight to Riverpod 3 on new projects. Team projects should use the generator (with &lt;code&gt;riverpod_lint&lt;/code&gt; enforcing conventions); solo projects are perfectly fine hand-writing — don't add the mental overhead of code generation just to look "advanced."&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 Routing — go_router
&lt;/h3&gt;

&lt;p&gt;Both projects use &lt;code&gt;go_router: 17.x&lt;/code&gt;. AppX additionally adopts &lt;code&gt;go_router_builder&lt;/code&gt; (type-safe routes: route parameters go from &lt;code&gt;String&lt;/code&gt; to compile-time-checked objects).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// go_router_builder: routes ARE types, no more string concatenation for navigation&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GameDetailRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;gameId:&lt;/span&gt; &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Recommendation&lt;/strong&gt;: go_router is the only no-brainer. Deep links, Web support, and nested Shell navigation all rely on it. The builder is worth it for team projects; optional for solo.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.3 Data Models — freezed 3
&lt;/h3&gt;

&lt;p&gt;AppX uses freezed across the board to define immutable models, paired with json_serializable for automatic serialization. This is the default way to handle data classes in 2026: immutability, &lt;code&gt;copyWith&lt;/code&gt;, &lt;code&gt;when&lt;/code&gt;/&lt;code&gt;map&lt;/code&gt; pattern matching, and value equality — all generated for you.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note the naming convention for generated files (worth copying from AppX):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;*.f.freezed.dart&lt;/code&gt; — freezed-generated copyWith/when/map&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*.f.g.dart&lt;/code&gt; — freezed + JSON serialization&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*.g.dart&lt;/code&gt; — riverpod / standalone json_serializable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Never hand-edit generated files&lt;/strong&gt;, and &lt;code&gt;exclude&lt;/code&gt; them in &lt;code&gt;analysis_options.yaml&lt;/code&gt; to avoid lint noise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1.4 i18n — slang
&lt;/h3&gt;

&lt;p&gt;Both projects abandoned the official &lt;code&gt;intl&lt;/code&gt; arb workflow in favor of &lt;strong&gt;slang&lt;/strong&gt;. Reasons: type safety (&lt;code&gt;t.home.title&lt;/code&gt; instead of &lt;code&gt;AppLocalizations.of(context)!.homeTitle&lt;/code&gt;), compile-time missing-key checks, and support for plurals / parameters / namespaces.&lt;/p&gt;

&lt;p&gt;An enterprise-grade detail: AppX's slang output is &lt;strong&gt;not committed&lt;/strong&gt; (&lt;code&gt;strings*.g.dart&lt;/code&gt; stays out of the repo) and is generated on demand locally / in CI. This reduces merge conflicts but requires CI to run i18n generation before the build. OPC projects can just commit the output for simplicity.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.5 Networking
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;go_gba: plain &lt;code&gt;dio&lt;/code&gt;, simple and direct.&lt;/li&gt;
&lt;li&gt;AppX: &lt;code&gt;dio&lt;/code&gt; + &lt;code&gt;dio_http2_adapter&lt;/code&gt; (HTTP/2) + &lt;code&gt;native_dio_adapter&lt;/code&gt; (uses the native network stack) + in-house networking / WebSocket infrastructure packages (unified team wrappers).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This is the first classic fork between OPC and enterprise&lt;/strong&gt;: solo projects use bare dio; teams extract the network layer into standalone packages to unify interceptors, retries, auth, telemetry, and certificate pinning.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Architecture: Different "Investment Budgets" for Layering
&lt;/h2&gt;

&lt;p&gt;This is where the two projects diverge the most, and where "complexity is an investment" shows most clearly.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 OPC: feature-by-layer (go_gba)
&lt;/h3&gt;

&lt;p&gt;go_gba uses the classic &lt;strong&gt;slice-by-technical-layer&lt;/strong&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lib/
├── core/          # emulator / services / analytics / errors / theme
├── data/          # datasources / repositories / adapters / platform
├── domain/        # business entities and use cases
├── providers/     # Riverpod providers
├── pages/         # organized by feature page: home / play / settings / toolkit ...
├── widgets/       # shared components
├── router/        # go_router config
└── i18n/          # slang
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Characteristics: &lt;strong&gt;one package does it all&lt;/strong&gt;. &lt;code&gt;pages/&lt;/code&gt; has sub-directories per feature; &lt;code&gt;data&lt;/code&gt;/&lt;code&gt;domain&lt;/code&gt; are shared globally. For a project maintained by one person, this structure carries the lowest mental load — you can fit the whole dependency graph in your head and don't need package boundaries to force decoupling.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 Enterprise: feature-first + Clean Architecture inside each feature (AppX)
&lt;/h3&gt;

&lt;p&gt;AppX is a &lt;strong&gt;melos monorepo&lt;/strong&gt;, split at the top level into three kinds of packages by responsibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/
└── app/                     # app shell: main_dev / main_prod / main_test (multiple entrypoints)
packages/
├── features/                # business feature packages, each self-contained:
│   ├── home/  feature_a/  feature_b/  auth/  ai/ ...
├── design_system/           # design system: colors/typography/spacing/components/chart
├── data_hub/  router/  routes/  env/  foundation/  push/  flavor_config/
└── lints/                   # team's custom lint rules package
plugins/                     # native plugins: various platform-capability wrappers
third_party/                 # third-party wrappers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;strong&gt;each feature package internally&lt;/strong&gt; is a full Clean Architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/features/home/lib/src/
├── application/    # coordinators / services (use-case orchestration)
├── domain/         # entities / repositories (interfaces)
├── data/           # datasources (local/remote) / dtos / mappers / repositories_impl
└── presentation/   # pages / widgets / controllers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why go this heavy? Because on a team of dozens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Package boundaries = enforced decoupling.&lt;/strong&gt; Feature A cannot directly import Feature B's internal implementation — only its public API. You can't achieve this with directory conventions; you need packages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compilation isolation = faster incremental builds.&lt;/strong&gt; Changing one feature doesn't recompile the world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No collisions in parallel development.&lt;/strong&gt; Different squads own different packages, so the conflict surface is small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An independent design system&lt;/strong&gt; = a single source of truth for UI consistency (the &lt;code&gt;design_system&lt;/code&gt; package), instead of every page hard-coding its own colors.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2.3 The Key Judgment: When Should You Upgrade From an OPC Structure to a Monorepo?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Don't start with a monorepo.&lt;/strong&gt; This is a hotspot for over-engineering. Upgrade only when the signals appear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The codebase grows beyond what one person can "hold in their head";&lt;/li&gt;
&lt;li&gt;2+ developers need to work on different modules in parallel;&lt;/li&gt;
&lt;li&gt;Incremental compilation starts getting noticeably slow;&lt;/li&gt;
&lt;li&gt;Arguments over "which module does this logic belong to" start happening;&lt;/li&gt;
&lt;li&gt;You need to reuse a single design system / network layer across multiple Apps or flavor outputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Until then, go_gba's single-package-with-clear-layering is the &lt;strong&gt;most cost-effective&lt;/strong&gt; structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Engineering: Turning Conventions Into Machine Enforcement
&lt;/h2&gt;

&lt;p&gt;The maturity of a modern Flutter project is largely reflected in &lt;em&gt;how many conventions are machine-enforced rather than left to human discipline&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Strict Lint (both projects do it, at different intensities)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;go_gba: based on &lt;code&gt;leancode_lint&lt;/code&gt;, and it &lt;strong&gt;built its own &lt;code&gt;gogba_custom_lint&lt;/code&gt; package&lt;/strong&gt; — encoding project-specific conventions (e.g. "must go through a certain service instead of calling the API directly") as custom lint rules. Worth doing even solo, because it's a guardrail for "future you."&lt;/li&gt;
&lt;li&gt;AppX: an extremely strict &lt;code&gt;analysis_options.yaml&lt;/code&gt;, promoting many rules from &lt;code&gt;warning&lt;/code&gt; to &lt;strong&gt;&lt;code&gt;error&lt;/code&gt; level&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;analyzer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;strict-casts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;       &lt;span class="c1"&gt;# forbid implicit type casts&lt;/span&gt;
    &lt;span class="na"&gt;strict-inference&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# forbid inference falling back to dynamic&lt;/span&gt;
  &lt;span class="na"&gt;errors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;always_declare_return_types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt;
    &lt;span class="na"&gt;avoid_void_async&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt;
    &lt;span class="na"&gt;prefer_single_quotes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt;
    &lt;span class="na"&gt;require_trailing_commas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt;   &lt;span class="c1"&gt;# enforce trailing commas → better diffs and formatting&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Recommendation&lt;/strong&gt;: Max out lint on day one of a new project. &lt;code&gt;strict-casts&lt;/code&gt; + &lt;code&gt;strict-inference&lt;/code&gt; are two low-cost, high-reward switches. Team projects must have a shared &lt;code&gt;lints&lt;/code&gt; package to unify rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Custom Lint — the 2026 Power Move
&lt;/h3&gt;

&lt;p&gt;Both projects do this, and many people overlook it: &lt;strong&gt;use &lt;code&gt;custom_lint&lt;/code&gt; to compile your team's/project's verbal conventions into static checks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A convention written in a doc goes unread; written as a lint rule, a violation lights up red. go_gba did this even as a one-person project — proof that the ROI is high enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 Version Pinning — fvm Is a Necessity
&lt;/h3&gt;

&lt;p&gt;AppX uses &lt;code&gt;fvm&lt;/code&gt; to pin the Flutter SDK to an exact version, committed in &lt;code&gt;.fvmrc&lt;/code&gt;. Everyone on the team, CI, and local runs all use the same SDK.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team projects without fvm periodically bleed time to "it won't compile on my end."&lt;/strong&gt; Solo projects should use it too, mainly to prevent a global upgrade from breaking the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.4 The Code-Generation Pipeline Must Be "Ordered"
&lt;/h3&gt;

&lt;p&gt;A hard-won lesson from AppX (written in its AGENTS.md): &lt;strong&gt;codegen cannot run in parallel.&lt;/strong&gt; Because packages have dependencies (design_system's generated output is consumed by features), running in parallel causes &lt;code&gt;AssetNotFoundException&lt;/code&gt;. So it wrote &lt;code&gt;codegen.sh&lt;/code&gt; to generate in dependency-graph order, leaf packages first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Correct: in dependency order&lt;/span&gt;
melos run codegen   &lt;span class="c"&gt;# → bash script/codegen.sh&lt;/span&gt;

&lt;span class="c"&gt;# Wrong: parallel melos exec build_runner → random failures&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway&lt;/strong&gt;: monorepo codegen must explicitly manage ordering. Single-package projects don't have this problem — one &lt;code&gt;dart run build_runner build -d&lt;/code&gt; does it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.5 Git Hooks — Move Discipline Left, Before Push
&lt;/h3&gt;

&lt;p&gt;After &lt;code&gt;melos bootstrap&lt;/code&gt;, AppX &lt;strong&gt;automatically installs versioned git hooks&lt;/strong&gt; (&lt;code&gt;pre-push&lt;/code&gt; runs a lint attribution check). This way "you must pass validation before committing" isn't something people have to remember — it's intercepted automatically at push time.&lt;/p&gt;

&lt;p&gt;Paired with this is a unified &lt;code&gt;verify.sh&lt;/code&gt; (format check + per-package analyze across the whole workspace), and &lt;strong&gt;local and CI run the exact same script&lt;/strong&gt; — this is crucial: passing locally = passing CI, eliminating the "green locally, red in CI" finger-pointing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.6 Multi-Environment / Multi-Entrypoint (an enterprise trait)
&lt;/h3&gt;

&lt;p&gt;AppX has three entrypoints: &lt;code&gt;main_dev.dart&lt;/code&gt; / &lt;code&gt;main_prod.dart&lt;/code&gt; / &lt;code&gt;main_test.dart&lt;/code&gt;, paired with generated environment variables (&lt;code&gt;env.impl.dart&lt;/code&gt;) and per-flavor config (&lt;code&gt;flavor_env.impl.dart&lt;/code&gt;). One codebase produces dev/prod, plus different flavor outputs, via build parameters.&lt;/p&gt;

&lt;p&gt;go_gba, as a single product, doesn't need any of this. &lt;strong&gt;Multi-flavor / multi-channel packaging is a textbook "tax you only pay at enterprise scale."&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Observability: The Eyes of a Production App
&lt;/h2&gt;

&lt;p&gt;go_gba, despite being a one-person project, does observability thoroughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;firebase_crashlytics&lt;/code&gt; — crash collection&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;firebase_analytics&lt;/code&gt; — user behavior&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;firebase_remote_config&lt;/code&gt; — remote config / gradual rollout switches&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;firebase_ai&lt;/code&gt; — AI capabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This is worth emphasizing: observability isn't an enterprise privilege — an OPC needs it more.&lt;/strong&gt; A solo developer has no QA team and no support agents relaying issues; when something breaks in production, all you have is the Crashlytics stack trace and the Analytics funnel to diagnose it yourself. remote_config lets you kill a broken feature without shipping a release — a lifesaver for indie developers.&lt;/p&gt;

&lt;p&gt;Enterprise projects typically use an in-house or heavier APM (AppX uses &lt;code&gt;sentry_flutter&lt;/code&gt;), but the &lt;strong&gt;core idea is identical&lt;/strong&gt;: crashes, performance, and behavior — three data streams, none optional.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. OPC vs Enterprise: A Decision Matrix
&lt;/h2&gt;

&lt;p&gt;Condensing the differences into one table. On the left, "the baseline both should do"; on the right, where they fork.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 The Shared Baseline (do this regardless of scale)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Riverpod 3 + go_router + freezed + slang&lt;/td&gt;
&lt;td&gt;Modern foundation, no compromise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strict lint (strict-casts / strict-inference)&lt;/td&gt;
&lt;td&gt;Max it out on day one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;build_runner code generation&lt;/td&gt;
&lt;td&gt;Kill boilerplate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability (crash + analytics + remote config)&lt;/td&gt;
&lt;td&gt;Mandatory in production; OPC needs it especially&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clear layering (data / domain / presentation)&lt;/td&gt;
&lt;td&gt;Layer even in a single package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom lint rules&lt;/td&gt;
&lt;td&gt;High ROI, worth it even solo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  5.2 The Fork Points
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;OPC (e.g. go_gba)&lt;/th&gt;
&lt;th&gt;Enterprise (e.g. AppX)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repo structure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single package, layered directories&lt;/td&gt;
&lt;td&gt;melos monorepo, packages as boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Architecture depth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;feature-by-layer, globally shared domain&lt;/td&gt;
&lt;td&gt;Independent Clean Architecture per feature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;State management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hand-written Riverpod is enough&lt;/td&gt;
&lt;td&gt;Riverpod generator + riverpod_lint enforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bare dio&lt;/td&gt;
&lt;td&gt;Extracted into standalone packages; unified interceptors/auth/HTTP2/WS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Design system&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A single theme directory&lt;/td&gt;
&lt;td&gt;Standalone &lt;code&gt;design_system&lt;/code&gt; package (single UI source)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-environment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single entrypoint&lt;/td&gt;
&lt;td&gt;Multiple flavor entrypoints + multi-channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;codegen&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One command&lt;/td&gt;
&lt;td&gt;Ordered generation script by dependency graph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CI / discipline&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local scripts + store release&lt;/td&gt;
&lt;td&gt;git hook + verify script (local = CI) + full pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependency versions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Exact pinning is enough&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dependency_overrides&lt;/code&gt; unifies versions across the workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Optimization goal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Iteration speed, lowest mental load&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Maintainability, parallelism, consistency&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  5.3 The One-Line Principle
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;OPC optimizes for "speed of change"; enterprise optimizes for "safety of change."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On a solo project, your biggest cost is your own attention, so cut every bit of complexity that needs "maintaining" (monorepo, extra abstraction layers, multi-channel packaging).&lt;br&gt;
On a team project, your biggest cost is communication and incidents, so pay for "boundaries" and "enforcement" (package isolation, git hooks, unified versions).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6. The Two Most Common Mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OPC over-engineering&lt;/strong&gt;: one person puts a to-do App on a melos monorepo with four-layer Clean Architecture per feature. The result: 80% of the time spent maintaining scaffolding instead of building features. &lt;strong&gt;Complexity is an investment; below the scale threshold, it's pure loss.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enterprise under-investment&lt;/strong&gt;: five people cram into one &lt;code&gt;lib/&lt;/code&gt;, with no package boundaries, no unified lint, codegen run by hand, and local vs CI scripts out of sync. The result: every merge is a disaster and "it works" is down to luck. &lt;strong&gt;Skip the tax you owe, and you'll pay it back later with interest.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To judge which tier you're in, don't look at "how professional I want to seem" — look at your &lt;strong&gt;actual current headcount and codebase size.&lt;/strong&gt; Upgrade when the scale arrives. That's the only correct sequence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Appendix: 2026 Starter Checklist
&lt;/h2&gt;

&lt;p&gt;Day one of a new project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;fvm use &amp;lt;version&amp;gt;&lt;/code&gt; to pin the Flutter SDK, commit &lt;code&gt;.fvmrc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Riverpod 3 + go_router + freezed + slang as the foundation&lt;/li&gt;
&lt;li&gt;[ ] Max out strict lint in &lt;code&gt;analysis_options.yaml&lt;/code&gt; (&lt;code&gt;strict-casts&lt;/code&gt;, &lt;code&gt;strict-inference&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Wire up crash + analytics + remote config (Firebase or equivalent)&lt;/li&gt;
&lt;li&gt;[ ] Layered directories (even in a single package): &lt;code&gt;data&lt;/code&gt; / &lt;code&gt;domain&lt;/code&gt; / &lt;code&gt;presentation&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Generated-file naming convention + &lt;code&gt;.gitignore&lt;/code&gt;/&lt;code&gt;exclude&lt;/code&gt; handling&lt;/li&gt;
&lt;li&gt;[ ] A single &lt;code&gt;verify&lt;/code&gt; script, run identically locally and in CI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally for team projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] melos workspace + package boundaries (features / design_system / foundation)&lt;/li&gt;
&lt;li&gt;[ ] A &lt;code&gt;lints&lt;/code&gt; package to unify rules; consider custom lint&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;dependency_overrides&lt;/code&gt; to unify versions across the workspace&lt;/li&gt;
&lt;li&gt;[ ] git hook (pre-push validation)&lt;/li&gt;
&lt;li&gt;[ ] Multiple flavor entrypoints, ordered codegen script&lt;/li&gt;
&lt;li&gt;[ ] AGENTS.md / CONTRIBUTING spelling out conventions (especially traps like codegen ordering)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>flutter</category>
      <category>opc</category>
    </item>
    <item>
      <title>Building a Handheld Console with Flutter</title>
      <dc:creator>Hamber</dc:creator>
      <pubDate>Tue, 28 Apr 2026 13:11:16 +0000</pubDate>
      <link>https://dev.to/hamberluo/building-a-handheld-console-with-flutter-3idh</link>
      <guid>https://dev.to/hamberluo/building-a-handheld-console-with-flutter-3idh</guid>
      <description>&lt;h2&gt;
  
  
  The Afternoon I Got Stuck on a Japanese Dialogue
&lt;/h2&gt;

&lt;p&gt;One weekend last year, I was playing a classic GBA RPG and hit an NPC conversation I couldn't read — all in Japanese. I screenshotted it, switched to a translation app, looked it up, switched back. The game had already moved on.&lt;/p&gt;

&lt;p&gt;That context switch was deeply annoying.&lt;/p&gt;

&lt;p&gt;I'm a Flutter GDE, and I happened to have a GBA emulator project called &lt;strong&gt;GoGBA&lt;/strong&gt; sitting on my machine. I thought: what if I could press one button, without ever leaving the game, and have AI read the screen and translate it for me?&lt;/p&gt;

&lt;p&gt;This article is the complete story of going from that idea to a shipped feature. The stack: &lt;strong&gt;Flutter + mGBA + Firebase AI (Gemini) + Riverpod + Clean Architecture&lt;/strong&gt;. All real production code.&lt;/p&gt;

&lt;p&gt;GoGBA is live on the App Store and Google Play — search &lt;strong&gt;GoGBA&lt;/strong&gt; to download it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: Architecture — Can Flutter Actually Run an Emulator?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Not Go Native
&lt;/h3&gt;

&lt;p&gt;Emulators are performance-sensitive, so the instinct is "Flutter isn't fast enough." But GoGBA's emulation core is &lt;strong&gt;libretro/mGBA&lt;/strong&gt; — a battle-tested C/C++ engine. Flutter only handles UI and event dispatch; it never touches the emulation logic.&lt;/p&gt;

&lt;p&gt;That's what makes cross-platform viable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flutter UI (Dart)
      ↓  MethodChannel / EventChannel
Kotlin (Android) / Swift (iOS)
      ↓  JNI / C FFI
libretro mGBA (C/C++)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flutter renders the game screen using the &lt;strong&gt;Texture widget&lt;/strong&gt; — the native layer writes mGBA's framebuffer into a &lt;code&gt;SurfaceTexture&lt;/code&gt; (Android) or &lt;code&gt;CVPixelBuffer&lt;/code&gt; (iOS), and Flutter composites it directly. Zero-copy. 60fps with no issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  Designing the Channel Boundaries
&lt;/h3&gt;

&lt;p&gt;GoGBA uses three channels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MethodChannel&lt;/span&gt; &lt;span class="n"&gt;_channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;MethodChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'go_gba/emulator'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MethodChannel&lt;/span&gt; &lt;span class="n"&gt;_audioChannel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;MethodChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'go_gba/audio'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EventChannel&lt;/span&gt; &lt;span class="n"&gt;_eventChannel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;EventChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'go_gba/emulator_events'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;_channel&lt;/code&gt;: command traffic — load ROM, save state, cheats&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_audioChannel&lt;/code&gt;: separated to prevent audio calls from blocking the game loop&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_eventChannel&lt;/code&gt;: native-initiated events — RetroAchievements unlocks, leaderboard updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;EventChannel&lt;/code&gt; is the design decision most people miss.&lt;/strong&gt; Emulator events happen asynchronously on the native side. Polling with MethodChannel is wasteful. Surfacing them as a Dart &lt;code&gt;Stream&lt;/code&gt; via EventChannel means a Riverpod provider can just &lt;code&gt;watch&lt;/code&gt; it — fully reactive, no polling, no glue code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Can Clean Architecture Actually Work in Flutter?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Bother With Layers
&lt;/h3&gt;

&lt;p&gt;GoGBA's early code was all crammed into &lt;code&gt;PlayPage&lt;/code&gt; — emulator calls, save logic, and UI state tangled together. When cloud saves, cheats, and AI translation needed to be added, every change rippled unpredictably.&lt;/p&gt;

&lt;p&gt;Clean Architecture's real value isn't aesthetics. It's &lt;strong&gt;letting features evolve independently&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GoGBA's layer structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pages / widgets / providers   ← Presentation
        ↓
domain/usecases               ← Application (business rules)
        ↓
domain/entities, ports,       ← Domain (pure Dart, no Flutter/dart:io)
repositories (interfaces)
        ↑ implements
data/repositories, core/emulator  ← Data / Infra
        ↓ MethodChannel
Kotlin / Swift / mGBA (native)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The hard rule: &lt;code&gt;domain/&lt;/code&gt; cannot import `package:flutter/&lt;/strong&gt;&lt;code&gt;, &lt;/code&gt;dart:io&lt;code&gt;, or anything from &lt;/code&gt;data/`.** Not a suggestion — a rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforcing Architecture with custom_lint
&lt;/h3&gt;

&lt;p&gt;Code review alone will eventually miss things. GoGBA uses &lt;strong&gt;custom_lint&lt;/strong&gt; to turn these constraints into compile-time errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# analysis_options.yaml&lt;/span&gt;
&lt;span class="na"&gt;analyzer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;custom_lint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two custom rules enforce the boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gogba_domain_layer_dependencies&lt;/code&gt;: blocks flutter / dart:io / data imports in domain&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gogba_presentation_no_data_imports&lt;/code&gt;: blocks presentation from reaching into data directly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now if anyone writes &lt;code&gt;import 'package:flutter/material.dart'&lt;/code&gt; inside &lt;code&gt;domain/&lt;/code&gt;, &lt;code&gt;flutter analyze&lt;/code&gt; fails and CI catches it. &lt;strong&gt;The rule lives in the toolchain, not in someone's memory.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the single most effective architecture enforcement technique I've used in a real Flutter project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Port/Adapter for Cross-Layer Dependencies
&lt;/h3&gt;

&lt;p&gt;Riverpod providers need to read and write app config — but they shouldn't import &lt;code&gt;ConfigDatasource&lt;/code&gt; directly (that's a data-layer type). GoGBA's solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// domain/ports/app_config_storage_port.dart (interface, pure Dart)&lt;/span&gt;
&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppConfigStoragePort&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppConfig&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;updateConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppConfig&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// data/adapters/ (implementation)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConfigDatasourceAppConfigStorageAdapter&lt;/span&gt;
    &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;AppConfigStoragePort&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// providers/ (composition root)&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;appConfigStoragePortProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppConfigStoragePort&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;((&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ConfigDatasourceAppConfigStorageAdapter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Presentation depends only on the port interface. Tests swap in a fake. Widget tests don't need to touch the filesystem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: AI Real-Time Translation — One Button, Three Technical Layers
&lt;/h2&gt;

&lt;p&gt;This is my favorite feature in GoGBA, and the most interesting engineering problem in the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Capturing the Game Screen
&lt;/h3&gt;

&lt;p&gt;The GBA screen is a native texture — not a regular Flutter widget. You can't screenshot it the normal way.&lt;/p&gt;

&lt;p&gt;GoGBA wraps the game view in a &lt;code&gt;RepaintBoundary&lt;/code&gt;, then uses &lt;code&gt;RenderRepaintBoundary.toImage()&lt;/code&gt; to capture the current frame. The expensive encoding work runs in a separate isolate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/core/utils/game_texture_capture.dart&lt;/span&gt;
&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Uint8List&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;captureGameTextureAsJpeg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;GlobalKey&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;targetWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;targetHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;boundary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentContext&lt;/span&gt;
      &lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;findRenderObject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;RenderRepaintBoundary&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;boundary&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;boundary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;pixelRatio:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;byteData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toByteData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;format:&lt;/span&gt; &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ImageByteFormat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;png&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;byteData&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;pngBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;byteData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asUint8List&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// PNG → resize → JPEG runs in an isolate — main thread stays unblocked&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_encodePngBytesToJpeg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;pngBytes:&lt;/span&gt; &lt;span class="n"&gt;pngBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;targetWidth:&lt;/span&gt; &lt;span class="n"&gt;targetWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;targetHeight:&lt;/span&gt; &lt;span class="n"&gt;targetHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;compute()&lt;/code&gt; call is the key detail. Image encoding and resizing happen in a dedicated isolate — the main thread stays responsive and the game keeps running without a hitch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Gemini Multimodal Translation
&lt;/h3&gt;

&lt;p&gt;GoGBA uses &lt;strong&gt;Firebase AI Logic&lt;/strong&gt; (Vertex AI on Firebase) with the &lt;code&gt;firebase_ai&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/core/services/game_screen_translation_service.dart&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;GenerativeModel&lt;/span&gt; &lt;span class="n"&gt;_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FirebaseAI&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vertexAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;location:&lt;/span&gt; &lt;span class="s"&gt;'global'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;generativeModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;model:&lt;/span&gt; &lt;span class="s"&gt;'gemini-3.1-flash-lite-preview'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;generationConfig:&lt;/span&gt; &lt;span class="n"&gt;GenerationConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;maxOutputTokens:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;temperature:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;topP:&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// Translation doesn't need reasoning — disable to save latency and tokens&lt;/span&gt;
        &lt;span class="nl"&gt;thinkingConfig:&lt;/span&gt; &lt;span class="n"&gt;ThinkingConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withThinkingBudget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;translateJpeg&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;jpegBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;targetLanguageTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="s"&gt;'GBA screenshot: pixel UI. Transcribe all visible on-screen text, '&lt;/span&gt;
      &lt;span class="s"&gt;'then translate it into "&lt;/span&gt;&lt;span class="si"&gt;$targetLanguageTag&lt;/span&gt;&lt;span class="s"&gt;". '&lt;/span&gt;
      &lt;span class="s"&gt;'Use natural RPG/menu phrasing. '&lt;/span&gt;
      &lt;span class="s"&gt;'Output only the translation text, no scene summary or extra commentary. '&lt;/span&gt;
      &lt;span class="s"&gt;'If no readable text, reply exactly: No text detected.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;multi&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="n"&gt;InlineDataPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'image/jpeg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Uint8List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jpegBytes&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="n"&gt;TextPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt engineering choices are deliberate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Use natural RPG/menu phrasing&lt;/code&gt;&lt;/strong&gt;: keeps translations in-genre — "HP" won't become "Health Points"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Output only the translation text&lt;/code&gt;&lt;/strong&gt;: strips the model's boilerplate preamble&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;If no readable text, reply exactly: No text detected.&lt;/code&gt;&lt;/strong&gt;: structured fallback the client can match on&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;temperature: 0.1&lt;/code&gt;&lt;/strong&gt;: translation is a deterministic task; higher temperature just adds noise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ThinkingConfig.withThinkingBudget(0)&lt;/code&gt;&lt;/strong&gt;: Gemini 2.x enables thinking by default; for translation it adds latency and tokens with no benefit — explicitly disable it&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Layer 3: Monthly Quota Management
&lt;/h3&gt;

&lt;p&gt;AI calls have real costs. GoGBA's AI translation is a separate subscription. Monthly usage limits are served from Firebase Remote Config, so they can be adjusted without a release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/domain/services/game_screen_translation_quota_service.dart&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GameScreenTranslationQuotaService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;_currentUtcYm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toUtc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${u.year.toString().padLeft(4, '0')}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
        &lt;span class="s"&gt;'-&lt;/span&gt;&lt;span class="si"&gt;${u.month.toString().padLeft(2, '0')}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;isExhausted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;monthlyLimit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monthlyLimit&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;uses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;getUsesThisMonth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;uses&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;monthlyLimit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;recordSuccessfulTranslation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;prefs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SharedPreferences&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_usesForCurrentUtcMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;prefs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_keyCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;UTC month, not local time.&lt;/strong&gt; Users span time zones. Local time means the quota resets at different moments for different people. UTC is the only fair counting window.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Wiring It Together: the UI Layer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/pages/play/widgets/game_translation_bottom_sheet.dart&lt;/span&gt;
&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Capture the frame&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;jpeg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;captureGameTextureAsJpeg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;targetWidth:&lt;/span&gt; &lt;span class="n"&gt;vw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;targetHeight:&lt;/span&gt; &lt;span class="n"&gt;vh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Translate with Gemini (follows system language)&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;LocaleSettings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentLocale&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flutterLocale&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toLanguageTag&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;GameScreenTranslationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;translateJpeg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;jpegBytes:&lt;/span&gt; &lt;span class="n"&gt;jpeg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;targetLanguageTag:&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Show result and record quota&lt;/span&gt;
  &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_phase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_TranslationPhase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_resultText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;recordUsageOnSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;GameScreenTranslationQuotaService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;recordSuccessfulTranslation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the user experiences: press the translate button → bottom sheet slides up → spinner for a second or two → translation appears. Behind that: frame capture, isolate encoding, multimodal AI call, quota write. All async. Game never pauses.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: AI-Assisted Development — What It Actually Feels Like
&lt;/h2&gt;

&lt;p&gt;GoGBA's development workflow is deeply integrated with &lt;strong&gt;Claude Code&lt;/strong&gt; (Anthropic's AI coding assistant). As a solo developer, it lets me maintain the kind of engineering discipline that normally takes a team.&lt;/p&gt;

&lt;p&gt;A few real examples:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture enforcement&lt;/strong&gt;: The project's &lt;code&gt;SKILL.md&lt;/code&gt; documents the domain layer rules and forbidden patterns. Claude Code reads this before every change and won't suggest code that violates the layering — the constraints stay consistent without manual review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;i18n automation&lt;/strong&gt;: GoGBA ships in 24 languages. When a new feature adds UI strings, Claude Code fills in all language files in &lt;code&gt;l10n/*.i18n.json&lt;/code&gt;, then triggers &lt;code&gt;dart run slang&lt;/code&gt; to regenerate. What used to take 20 minutes of copy-paste takes seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fastlane releases&lt;/strong&gt;: Build number bumps, changelog generation, App Store submission — all scripted. Claude Code runs the sequence and catches problems.&lt;/p&gt;

&lt;p&gt;This isn't "AI replacing the developer." It's &lt;strong&gt;AI reducing the cost of following your own rules to near zero&lt;/strong&gt;. Write the standards once; the tool enforces them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: Bugs That Taught Me Things
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bug 1: &lt;code&gt;ref.read()&lt;/code&gt; inside &lt;code&gt;dispose()&lt;/code&gt; crashes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Riverpod's &lt;code&gt;ref.read()&lt;/code&gt; and &lt;code&gt;ref.watch()&lt;/code&gt; cannot be called after &lt;code&gt;dispose()&lt;/code&gt;. The widget is gone; the provider may have already been released. GoGBA had a handful of early crashes from this. The rule is now in &lt;code&gt;SKILL.md&lt;/code&gt; and detected by custom_lint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2: &lt;code&gt;invalidate(provider)&lt;/code&gt; causes an AsyncLoading flash&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Calling &lt;code&gt;invalidate&lt;/code&gt; after updating config forces the provider to rebuild from scratch, briefly putting the UI into a loading state. Users see a flicker. The fix: update state directly with &lt;code&gt;state = newValue&lt;/code&gt; inside the notifier and let Riverpod diff it. No invalidation needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 3: Gemini's thinking mode is on by default&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;firebase_ai&lt;/code&gt; Gemini 2.x models enable extended thinking by default. For translation — a deterministic task — this means longer latency, more tokens, and less predictable output. You have to explicitly disable it with &lt;code&gt;ThinkingConfig.withThinkingBudget(0)&lt;/code&gt;. The default bit me in early testing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;GoGBA is my testbed for engineering ideas: where Flutter's cross-platform ceiling actually sits, whether Clean Architecture can hold up in a real project without becoming an interview-question abstraction, how to design AI features that are genuinely useful rather than just impressive in a demo.&lt;/p&gt;

&lt;p&gt;My conclusions: &lt;strong&gt;Flutter is mature enough for this. AI tooling is raising the ceiling for individual developers in ways that weren't possible two years ago.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every specific choice in this codebase — custom_lint guarding domain boundaries, &lt;code&gt;compute()&lt;/code&gt; keeping the main thread clean, UTC-based quota windows — is a scar from a real mistake. I hope some of it saves you the same trouble.&lt;/p&gt;

&lt;p&gt;Search &lt;strong&gt;GoGBA&lt;/strong&gt; on the App Store or Google Play. If you play GBA games in Japanese or English and hit a text wall, the AI translation feature is there for exactly that.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions about Flutter cross-platform development, Firebase AI Logic integration, or shipping a solo app with an AI workflow? Drop them in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>ai</category>
      <category>vibecoding</category>
    </item>
  </channel>
</rss>
