<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Code With Andrea</title><description>Learn how to become a Flutter Pro and build production-ready apps on mobile and beyond.</description><link>https://codewithandrea.com</link><language>en</language><lastBuildDate>Thu, 26 Feb 2026 13:11:30 +0100</lastBuildDate><pubDate>Thu, 26 Feb 2026 13:11:30 +0100</pubDate><ttl>250</ttl><atom:link href="https://codewithandrea.com/rss.xml/" rel="self" type="application/rss+xml"/><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/february-2026/</guid><title>February 2026: Flutter 3.41, 16 Claudes Build a C Compiler, the OpenClaw Security Crisis, and GPT-5.3 Codex</title><description>Also included: why the SDLC is dead, the importance of documentation artifacts for AI coding, and why code is cheap but software isn't.</description><link>https://codewithandrea.com/newsletter/february-2026/</link><pubDate>Thu, 26 Feb 2026 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>What a month for AI coding.</p><p>Anthropic got 16 Claude instances to build a C compiler (with some caveats). OpenAI shipped GPT-5.3-Codex, and it's good! And then OpenClaw — a vibe-coded AI agent with 230K GitHub stars — became a full-blown security crisis, with malicious packages showing up on pub.dev too.</p><p>There's a new Flutter release as well, so let's start there. 👇</p><h2><a id="flutter-341-+-dart-311" href="#flutter-341-+-dart-311">Flutter 3.41 + Dart 3.11</a></h2><p>Flutter 3.41 landed earlier this month with 868 commits from 145 contributors. The ongoing work to <a href="https://github.com/flutter/flutter/issues/101479">decouple Material and Cupertino into standalone packages</a> continues, and this release brings several other improvements:</p><ul><li><strong>Fragment shader improvements</strong>: Synchronous image decoding (<code>decodeImageFromPixelsSync</code>) eliminates frame lag, and 128-bit float texture support unlocks GPU-accelerated photo filters.</li><li><strong><a href="https://docs.flutter.dev/perf/impeller">Impeller</a> bounded blur</strong>: Fixes color bleeding on translucent widgets using <code>BackdropFilter</code>.</li><li><strong><a href="https://docs.flutter.dev/tools/widget-previewer">Widget Previewer</a></strong>: Better VS Code and IntelliJ integration, plus support for dependencies on platform-specific libraries like <code>dart:ffi</code> and <code>dart:io</code>.</li><li><strong>New Getting Started Experience</strong>: The redesigned <a href="https://docs.flutter.dev/get-started">Learn section</a> on the Flutter website, powered by <a href="https://pub.dev/packages/jaspr">Jaspr</a>.</li><li><strong>2026 Roadmap</strong>: Four stable releases planned. <a href="https://docs.flutter.dev/platform-integration/web/wasm">WebAssembly</a> is on track to become the default for Flutter Web (40% faster load times, 30% less runtime memory).</li></ul><p>For the full details, read the official blog post:</p><ul><li><a href="https://blog.flutter.dev/whats-new-in-flutter-3-41-302ec140e632">What's New in Flutter 3.41</a></li></ul><p>As for <strong>Dart 3.11</strong>, this release focuses on tooling improvements rather than new language features. The highlights include faster analyzer plugins (~10s saved on startup), a new <code>dart pub cache gc</code> command to reclaim disk space, enhanced <a href="https://dart.dev/language/dot-shorthands">dot shorthand</a> tooling, and <a href="https://dart.dev/tools/pub/workspaces">pub workspace</a> glob support. The <a href="https://github.com/dart-lang/ai">Dart &amp; Flutter MCP Server</a> was also improved. 🤖</p><p>For all the details:</p><ul><li><a href="https://blog.dart.dev/announcing-dart-3-11-b6529be4203a">Announcing Dart 3.11</a></li></ul><blockquote><p>For more context about Flutter's position in 2026 (including a full timeline of significant milestones over the last year), check out this <a href="https://devnewsletter.com/p/state-of-flutter-2026/">State of Flutter 2026</a> article.</p></blockquote><h2><a id="ai-news" href="#ai-news">AI News</a></h2><p>This was a big month for agentic AI coding. Two stories in particular stood out to me.</p><h3><a id="📝-building-a-c-compiler-with-a-team-of-parallel-claudes" href="#📝-building-a-c-compiler-with-a-team-of-parallel-claudes">📝 Building a C Compiler with a Team of Parallel Claudes</a></h3><p>Anthropic researcher Nicholas Carlini orchestrated <strong>16 Claude instances</strong> to build a C compiler in Rust over nearly 2,000 Claude Code sessions and ~$20K in API costs. The result: a 100,000-line compiler with a 99% pass rate on standard compiler test suites.</p><p>The multi-agent engineering appears to be genuinely impressive:</p><ul><li>Agents claimed work from a shared task queue via lock files, with git handling merges and conflict resolution.</li><li>Each agent had specialized roles: core compilation, code deduplication, performance optimization, documentation, and architecture.</li><li>Tests served as the primary feedback mechanism, with comprehensive suites using SQLite, Redis, libjpeg, and the <a href="https://gcc.gnu.org/onlinedocs/gccint/Torture-Tests.html">GCC torture tests</a>.</li></ul><p>Here's the full write-up:</p><ul><li><a href="https://www.anthropic.com/engineering/building-c-compiler">Building a C compiler with a team of parallel Claudes</a></li></ul><p><strong>However</strong>, Anthropic's marketing of the project attracted legitimate criticism. ThePrimeagen <a href="https://www.youtube.com/watch?v=6QryFk4RYaM">published a detailed response</a> pointing out that the framing of "from scratch, no human intervention" was misleading:</p><ul><li><strong>Not "from scratch"</strong>: Claude was trained on <a href="https://gcc.gnu.org/">GCC</a> (open source), and used its 37-year-old torture test suite for validation, plus GCC itself as an "online Oracle" to verify output. Starting "from scratch" with the perfect test suite and a reference implementation to test against is not quite the same thing as starting from a blank slate.</li><li><strong>Not "no human intervention"</strong>: agents crashed and required restarting during the two weeks.</li><li><strong>Can't actually boot Linux</strong>: the compiler lacks a 16-bit x86 code generator small enough to meet Linux's 32KB real mode limit.</li><li><strong>Hello World didn't compile</strong>: the README example failed because the project lacks a linker.</li></ul><p>So what's the <strong>real takeaway</strong>? As ThePrimeagen himself acknowledges: getting 16 AI agents to run mostly autonomously for two weeks and produce a substantial, functional piece of software is genuinely cool. It shows that models are improving and can handle projects at this scale with the right orchestration. That's the real story — no hype needed.</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="6QryFk4RYaM"></div></div><blockquote><p><a href="https://en.wikipedia.org/wiki/Chris_Lattner">Chris Lattner</a> (creator of <a href="https://llvm.org/">LLVM</a>, Swift, and <a href="https://www.modular.com/mojo">Mojo</a>) also <a href="https://www.modular.com/blog/the-claude-c-compiler-what-it-reveals-about-the-future-of-software">published a thoughtful analysis</a>, calling it "real progress, a milestone for the industry" while noting that the compiler reproduces decades of established engineering rather than inventing novel abstractions. His core thesis: as coding becomes cheaper, the real challenge becomes <strong>choosing the right problems and managing the resulting complexity</strong>.</p></blockquote><h3><a id="📝-gpt-53-codex-full-autonomy-has-arrived?" href="#📝-gpt-53-codex-full-autonomy-has-arrived?">📝 GPT-5.3-Codex: Full Autonomy Has Arrived?</a></h3><p>Earlier this month, OpenAI released <a href="https://openai.com/index/introducing-gpt-5-3-codex/">GPT-5.3-Codex</a>, and the early reviews are eye-opening.</p><p>Matt Shumer's <a href="https://shumer.dev/gpt53-codex-review">hands-on review</a> describes it as the first model where "full autonomy starts feeling operationally real." In practice, this means you can specify an outcome, set up validation criteria, press go, and come back hours later to find the work done — including code changes, GitHub pushes, deployments, and log monitoring.</p><p>Key highlights:</p><ul><li>Can run tasks for <strong>8+ hours</strong> without degradation.</li><li>25% faster than GPT-5.2-Codex, and tops <a href="https://www.swebench.com/">SWE-Bench</a> Pro and Terminal-Bench 2.0.</li><li>Self-improvement capabilities: the model debugged its own training run and scaled its own GPU clusters.</li></ul><blockquote><p>I've been testing GPT-5.3-Codex for my own Flutter work over the past few weeks, and I have to say: I'm finding it <a href="https://x.com/biz84/status/2022313477474222308">faster, cheaper, and sharper than Opus 4.6</a> — often <a href="https://x.com/biz84/status/2022239244941758629">surfacing edge cases and insights</a> that Opus misses. With that said, while "8+ hours without degradation" may be possible, my workflows require frequent human oversight and I care more about output quality than "how long it can run".</p></blockquote><h2><a id="the-openclaw-security-crisis" href="#the-openclaw-security-crisis">The OpenClaw Security Crisis</a></h2><p>Now for the other side of the coin. If the Claude compiler pushes the boundaries of AI autonomy, the OpenClaw saga shows what happens when the worst practices collide with rapid adoption.</p><p><a href="https://en.wikipedia.org/wiki/OpenClaw">OpenClaw</a> (formerly Clawdbot/Moltbot) is an open-source AI agent that exploded to 150K+ GitHub stars. It connects to LLMs and can autonomously execute tasks through messaging platforms like WhatsApp, Telegram, and Slack. Sounds cool, right?</p><p>The problem: OpenClaw requires broad permissions to function (email, calendars, messaging platforms, file system), and misconfigured or exposed instances quickly became a magnet for attackers. One of OpenClaw's own maintainers warned on Discord: "if you can't understand how to run a command line, this is far too dangerous of a project for you to use safely." Within weeks, the security issues piled up:</p><ul><li><strong>40,000+ instances exposed</strong> on the public internet because the gateway binds to <code>0.0.0.0</code> by default.</li><li><strong>API keys stored in plaintext</strong> markdown and JSON files.</li><li><strong>12-20% of ClawHub marketplace skills were malicious</strong>, with the <a href="https://www.pixee.ai/weekly-briefings/openclaw-malware-ai-agent-trust-2026-02-11">ClawHavoc campaign</a> distributing Atomic Stealer to harvest crypto keys, SSH credentials, and browser passwords.</li><li><strong><a href="https://adversa.ai/blog/openclaw-security-101-vulnerabilities-hardening-2026/">CVE-2026-25253</a></strong> (CVSS 8.8): a one-click remote code execution exploit where visiting a single malicious webpage is enough.</li><li><strong>Prompt injection attacks</strong> already seen in the wild, including crypto wallet drain attempts.</li></ul><p>For the full details, here's CrowdStrike's breakdown:</p><ul><li><a href="https://www.crowdstrike.com/en-us/blog/what-security-teams-need-to-know-about-openclaw-ai-super-agent/">What Security Teams Need to Know About OpenClaw</a></li></ul><p>And it gets worse. In a case of meta-irony, <strong><a href="https://thehackernews.com/2026/02/cline-cli-230-supply-chain-attack.html">Cline CLI was also compromised</a></strong> via a supply chain attack that silently installed OpenClaw on ~4,000 developer machines. The root cause? Prompt injection exploiting AI-assisted GitHub workflows to steal npm publish credentials. An AI coding tool, compromised via an AI-specific attack vector. 😬</p><p>For entertainment value, here's a video of OpenClaw deleting an entire inbox:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="JiA4fvoeUfI"></div></div><h3><a id="why-flutter-devs-should-care" href="#why-flutter-devs-should-care">Why Flutter Devs Should Care</a></h3><p>This isn't just a general security story. The r/FlutterDev community has been <a href="https://www.reddit.com/r/FlutterDev/comments/1qyjbwr/flutter_devs_avoid_the_openclaw_vibe_coding">flagging OpenClaw-generated packages appearing on pub.dev</a> — vibe-coded packages that lack proper testing, security review, and sometimes contain hidden dependencies or malicious code.</p><p>The broader lesson: AI agents can ship code at unprecedented speed, but that speed makes proper security practices <strong>more important, not less</strong>. Treat community-generated skills and packages with the same skepticism you'd give a random npm package from a stranger.</p><h2><a id="ai-articles" href="#ai-articles">AI Articles</a></h2><p>Beyond the headline stories, I bookmarked some excellent articles this month that are good food for thought.</p><h3><a id="📝-the-software-development-lifecycle-is-dead" href="#📝-the-software-development-lifecycle-is-dead">📝 The Software Development Lifecycle Is Dead</a></h3><p>Boris Tane argues that AI agents haven't just accelerated the SDLC — they've <strong>dismantled</strong> it. The traditional sequential stages (requirements → design → implementation → testing → review → deployment → monitoring) didn't get faster. They merged into a single, tight feedback loop.</p><figure><picture><source srcset="images/sdlc.webp 2x" type="image/webp"/><img class="bottom-12px" alt="What actually happens when an engineer works with a coding agent" srcset="images/sdlc.png 2x"/></picture><figcaption><center><i>What actually happens when an engineer works with a coding agent</i></center></figcaption></figure><p>His key insights:</p><ul><li>Requirements are now fluid and iterative rather than frozen specifications.</li><li>Code review via PRs becomes a bottleneck when agents generate hundreds of changes daily. Self-verification and second-agent reviews are replacing human code review for routine changes.</li><li>Observability becomes the primary safety mechanism, with monitoring feeding back to agents for automatic fixes.</li><li>"<a href="https://x.com/tobi/status/1935533422589399127">Context engineering</a>" replaces process management as the new critical skill.</li></ul><p>Read the full article:</p><ul><li><a href="https://boristane.com/blog/the-software-development-lifecycle-is-dead/">The Software Development Lifecycle Is Dead</a></li></ul><blockquote><p>I've been using a traditional "spec → plan → implement → review → ship" cycle in my own work. But I'm starting to notice that if the spec is solid and the agent has its own verification loop (<a href="https://www.aihero.dev/skill-test-driven-development-claude-code">TDD</a> helps greatly here), manual code reviews become less important. The discipline shifts upstream — getting the requirements right matters more than ever.</p></blockquote><h3><a id="📝-the-importance-of-artifacts-in-ai-assisted-programming" href="#📝-the-importance-of-artifacts-in-ai-assisted-programming">📝 The Importance of Artifacts in AI-Assisted Programming</a></h3><p>Nicholas Zakas makes a compelling case for why documentation isn't optional when coding with AI. His core point: <strong>AI has no memory beyond its context window</strong>. It can't tell you why it made a decision six months ago that brought down your server today.</p><p>His recommended artifacts:</p><ul><li><strong>Product Requirements Documents (PRDs)</strong>: Capture the <em>what</em> and <em>why</em>.</li><li><strong>Architectural Decision Records (<a href="https://adr.github.io/">ADRs</a>)</strong>: Immutable records of technical choices and their rationale.</li><li><strong>Technical Design Documents (TDDs)</strong>: The <em>how</em> of implementation.</li><li><strong>Task Lists</strong>: Granular work items with dependencies and acceptance criteria.</li></ul><p>If you're using AI coding tools and <strong>not</strong> maintaining these artifacts, this article will change how you think about documentation:</p><ul><li><a href="https://humanwhocodes.com/blog/2026/02/artifacts-ai-assisted-programming/">The Importance of Artifacts in AI-Assisted Programming</a></li></ul><blockquote><p>These two articles pair well together: as the SDLC collapses into tight feedback loops, documentation artifacts become the new source of truth that compensates for what both AI and humans forget over time.</p></blockquote><p>I also recommend <a href="https://www.chrisgregori.dev/opinion/code-is-cheap-now-software-isnt">Code Is Cheap Now. Software Isn't</a> — a thoughtful piece arguing that while LLMs have made code generation nearly free, the barrier to building <em>meaningful</em> software remains unchanged. Engineering value is shifting from syntax mastery toward architectural thinking, taste, and knowing <em>where not to cut corners</em>. This echoes Chris Lattner's thesis perfectly.</p><h2><a id="latest-from-code-with-andrea" href="#latest-from-code-with-andrea">Latest from Code with Andrea</a></h2><p>I've been quiet on the content front lately, and for good reason: I've been heads-down building an <strong>agentic coding toolkit</strong> for Flutter — and dogfooding it heavily.</p><p>Here are a few Flutter web apps I built entirely with this spec-driven workflow (no manual coding):</p><ul><li><a href="https://currency-converter-ab.web.app/">Currency Converter</a></li><li><a href="https://folio-tracker-ab.web.app/">Portfolio Tracker</a></li><li><a href="https://italian-tax-calculator-ab.web.app/">Italian Tax Calculator</a></li></ul><p>If you're curious what the generated code looks like, I've open sourced the Currency Converter on GitHub:</p><ul><li><a href="https://github.com/bizz84/currency_converter/">Currency Converter (GitHub)</a></li></ul><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>The toolkit is shaping up well and I'm hoping to launch it soon. I want to get it right — something you can actually use to write quality Flutter code, faster.</p><p>Thanks for reading, and happy coding! 🎉</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/january-2026/</guid><title>January 2026: AI Agents Take Over, Claude Code Workflows, Multi-Agent Orchestration, and OpenCode</title><description>Also included: Gas Town multi-agent orchestration, Ralph loops, Anthropic's ToS crackdown, and my 2025 retrospective.</description><link>https://codewithandrea.com/newsletter/january-2026/</link><pubDate>Wed, 28 Jan 2026 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Welcome to 2026! This is my 42rd monthly newsletter, which means I've been writing this for <strong>3.5 years</strong> now (here's the <a href="https://codewithandrea.com/newsletter/archive/">full archive</a>).</p><p>Over the past year, AI coding agents have completely reshaped how we work — from <a href="https://x.com/karpathy/status/1886192184808149383">vibe coding</a>, to agentic coding with human review, all the way to fully automated <a href="https://github.com/steveyegge/gastown">multi-agent orchestration</a>. We're all at different stages of this evolution, and I put this edition together to help you navigate it. 👇</p><h2><a id="andrej-karpathy-on-the-state-of-ai" href="#andrej-karpathy-on-the-state-of-ai">Andrej Karpathy on the State of AI</a></h2><p><a href="https://x.com/karpathy">Andrej Karpathy</a> has been one of the most insightful voices in AI. I featured his <a href="https://karpathy.bearblog.dev/year-in-review-2025/">2025 LLM Year in Review</a> in my <a href="https://codewithandrea.com/newsletter/december-2025/">previous newsletter</a>, and since then he's shared two more posts that are well worth your attention.</p><h3><a id="📝-ive-never-felt-this-much-behind-as-a-programmer" href="#📝-ive-never-felt-this-much-behind-as-a-programmer">📝 "I've never felt this much behind as a programmer"</a></h3><p>Earlier this month, Karpathy <a href="https://x.com/karpathy/status/2004607146781278521">posted on X</a> that he'd "never felt this much behind as a programmer." The post went viral and resonated deeply across the developer community.</p><p>He describes a new "programmable layer of abstraction" involving agents, subagents, prompts, contexts, memory, modes, permissions, tools, plugins, skills, hooks, MCP, and more. In his words:</p><blockquote><p>"Clearly some powerful alien tool was handed around except it comes with no manual and everyone has to figure out how to hold it and operate it, while the resulting magnitude 9 earthquake is rocking the profession."</p></blockquote><p>If even Karpathy feels behind, the rest of us can feel a bit less guilty about struggling to keep up 😅</p><p>For a counterpoint, Maximilian Schwarzmüller recorded a <a href="https://www.youtube.com/watch?v=C39Vm-iYThI">reaction video</a> with a reassuring "Relax" message. I think the truth is somewhere in the middle — things are moving fast, but there's no need to panic 🙂</p><h3><a id="📝-random-notes-from-claude-coding" href="#📝-random-notes-from-claude-coding">📝 Random notes from Claude coding</a></h3><p>Most recently, Karpathy shared a <a href="https://x.com/karpathy/status/2015883857489522876">detailed thread</a> with practical observations from weeks of heavy Claude Code usage. This is the most actionable of the two, and I found myself nodding along to many of his points.</p><p>Here are the highlights:</p><ul><li><strong>Workflow shift</strong>: he went from 80% manual coding to 80% agent coding in just a few weeks. "I really am mostly programming in English now."</li><li><strong>Agent pitfalls</strong>: models make wrong assumptions, don't seek clarifications, don't push back when they should, and are "still a little too sycophantic." They'll implement 1,000 lines of bloated code, and when challenged, immediately cut it to 100.</li><li><strong>Agent swarm hype</strong>: "too much for right now" — watch models "like a hawk" if you care about your code.</li><li><strong>Tenacity</strong>: agents never get tired or demoralized. Watching one struggle for 30 minutes and come out victorious is a "feel the AGI" moment.</li><li><strong>Speedup vs. expansion</strong>: the main effect isn't just speed — it's doing things that "wouldn't have been worth coding before."</li><li><strong>Key tip</strong>: "Don't tell it what to do, give it success criteria and watch it go." Shift from imperative to declarative.</li><li><strong>Fun factor</strong>: programming feels <em>more</em> fun — drudgery removed, creative part remains, less blocked/stuck.</li><li><strong>Atrophy warning</strong>: he's already noticing a decline in his ability to write code manually.</li></ul><p>His TLDR: "LLM agent capabilities have crossed some kind of threshold of coherence around December 2025 and caused a phase shift in software engineering."</p><h2><a id="ai-workflows-and-tools" href="#ai-workflows-and-tools">AI Workflows and Tools</a></h2><p>So, with the big picture in mind, let's look at what builders are actually doing with these tools.</p><h3><a id="📝-how-the-creator-of-claude-code-uses-claude-code" href="#📝-how-the-creator-of-claude-code-uses-claude-code">📝 How the Creator of Claude Code Uses Claude Code</a></h3><p><a href="https://x.com/bcherny">Boris Cherny</a>, the creator of Claude Code, shared his workflow in a <a href="https://x.com/bcherny/status/2007179832300581177">viral thread</a> that was covered by <a href="https://venturebeat.com/technology/the-creator-of-claude-code-just-revealed-his-workflow-and-developers-are">VentureBeat</a>, <a href="https://www.infoq.com/news/2026/01/claude-code-creator-workflow/">InfoQ</a>, and <a href="https://fortune.com/2026/01/24/anthropic-boris-cherny-claude-code-non-coders-software-engineers/">Fortune</a>.</p><p>Here are the key takeaways from his workflow:</p><ul><li>Runs <strong>5 parallel Claude sessions</strong> locally, plus 5-10 on claude.ai</li><li>Each session uses its own git checkout (not branches or worktrees)</li><li>Starts in <strong>Plan Mode</strong>, iterates until the plan is good, then switches to auto-accept mode</li><li>Maintains a <strong>CLAUDE.md</strong> file per team to document mistakes and best practices (~2.5K tokens)</li><li>Uses <strong>PostToolUse hooks</strong> for auto-formatting</li><li>Most important tip: "<strong>Give Claude a way to verify its work</strong>" — tests, browser, simulators</li></ul><p>No exotic customization. No clever hacks. He also accepts that 10-20% of sessions simply get abandoned.</p><blockquote><p>For more details, there's even a dedicated site: <a href="https://howborisusesclaudecode.com/">howborisusesclaudecode.com</a>. Definitely worth a read if you use Claude Code.</p></blockquote><h3><a id="📝-shipping-at-inference-speed" href="#📝-shipping-at-inference-speed">📝 Shipping at Inference-Speed</a></h3><p><a href="https://steipete.me/posts/2025/shipping-at-inference-speed">Peter Steinberger</a>, the prominent iOS developer and PSPDFKit founder, takes things even further. His claim: <strong>he no longer reads the code</strong> his AI agents generate.</p><p>Instead, he manages 3-8 simultaneous projects, commits directly to main, and relies on AI agents to iterate and validate. Software development, he argues, is no longer limited by coding ability — but by "inference time and hard thinking."</p><blockquote><p>I always find it fascinating when very experienced devs like Peter claim they no longer read or review the AI generated code on their solo-projects. Yet, many others still advocate that human-in-the-loop is necessary. Who's right? 🤔</p></blockquote><p>This tension — between "ship without reading" and "human validation is essential" — is one of the most interesting debates heading into this year.</p><h2><a id="ai-agent-orchestration" href="#ai-agent-orchestration">AI Agent Orchestration</a></h2><p>Beyond single-agent workflows, some developers are pushing into multi-agent territory. Here are two approaches at opposite ends of the spectrum — and both are fascinating.</p><h3><a id="📝-gas-town-multi-agent-orchestration" href="#📝-gas-town-multi-agent-orchestration">📝 Gas Town: Multi-Agent Orchestration</a></h3><p><a href="https://steve-yegge.medium.com/welcome-to-gas-town-4f25ee16dd04">Steve Yegge's Gas Town</a> is a Go-based orchestration system that lets you coordinate <strong>20-30 parallel Claude Code agents</strong> using <a href="https://github.com/tmux/tmux/wiki">tmux</a>. It features 7 distinct worker roles and runs on a git-based work tracking system called <a href="https://github.com/steveyegge/beads">Beads</a>.</p><p>What I find most useful is Yegge's <strong>8-stage maturity model</strong> for AI-assisted coding:</p><figure><picture><img class="bottom-12px" alt="Gas Town orchestration system" srcset="images/8-stages-dev-evolution-ai.webp 2x"/></picture><figcaption><center><i>Gas Town orchestration system</i></center></figcaption></figure><ul><li><strong>Stages 1-2</strong>: No or minimal AI (autocomplete, sidebar chat)</li><li><strong>Stages 3-5</strong>: Single agent with increasing trust and automation</li><li><strong>Stages 6-7</strong>: CLI, multi-agent, hand-managed (3-10+ parallel instances)</li><li><strong>Stage 8</strong>: Building your own orchestrator</li></ul><p>Most of us (myself included) are somewhere between stages 3-6. Gas Town is for stages 7-8. Fair warning: running heavy sessions can cost $100-200/hour in API fees 😅</p><ul><li><a href="https://github.com/steveyegge/gastown">Gas Town on GitHub</a></li></ul><h3><a id="📝-the-ralph-wiggum-technique-simple-agent-loops" href="#📝-the-ralph-wiggum-technique-simple-agent-loops">📝 The Ralph Wiggum Technique: Simple Agent Loops</a></h3><p>At the other end of the spectrum, <a href="https://ghuntley.com/ralph/">Geoffrey Huntley's Ralph technique</a> is brilliantly simple — just a bash loop that repeatedly feeds Claude a prompt file:</p><pre><code><div class="highlight"><span></span><span class="k">while</span><span class="w"> </span>:<span class="p">;</span><span class="w"> </span><span class="k">do</span><span class="w"> </span>cat<span class="w"> </span>PROMPT.md<span class="w"> </span><span class="p">|</span><span class="w"> </span>claude-code<span class="w"> </span><span class="p">;</span><span class="w"> </span><span class="k">done</span>
</div></code></pre><p>Each iteration gets fresh context, and memory persists via git history and progress files. The results can be remarkable: a team at a Y Combinator hackathon <a href="https://github.com/repomirrorhq/repomirror/blob/main/repomirror.md">produced 1,100+ commits across 6 repos overnight</a> for ~$800 in AI costs 🤯</p><p>Anthropic later built an <a href="https://github.com/anthropics/claude-code/blob/main/plugins/ralph-wiggum/README.md">official ralph-wiggum plugin</a>, though Matt Pocock <a href="https://x.com/mattpocockuk/status/2014627397241282564">argued that the plugin misses the point</a>: a proper Ralph loop gives <em>bash</em> control over the agent, while the plugin inverts this (letting the agent control the loop, leading to context rot). If you're curious, <a href="https://www.aihero.dev/why-the-anthropic-ralph-plugin-sucks">this article</a> argues why the original bash loop might be preferred.</p><h2><a id="anthropics-tos-crackdown" href="#anthropics-tos-crackdown">Anthropic's ToS Crackdown</a></h2><p>Earlier this month, Anthropic deployed safeguards that <strong>blocked Claude Pro/Max subscription tokens from working outside the official Claude Code CLI</strong>. Overnight, third-party tools like <a href="https://github.com/anomalyco/opencode/">OpenCode</a> stopped working — with no warning.</p><p>Anthropic's rationale: third-party tools had been spoofing the Claude Code client identity, generating unusual traffic without telemetry, and making debugging and support impossible. This violates their Terms of Service.</p><p>There's also an economic angle: the $200/month Max plan provides unlimited tokens through Claude Code, while the same usage via API would cost $1,000+. Third-party tools removed the artificial speed limits, enabling overnight autonomous loops.</p><p>The community reaction was strong — subscription cancellations, <a href="https://paddo.dev/blog/anthropic-walled-garden-crackdown/">front-page Hacker News discussion</a>, and criticism from prominent developers, including this entertaining take from Primeagen:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="guuGl34HI3Q"></div></div><p>Regardless of where you stand, the takeaway is clear: <strong>if you rely on Claude's subscription plans, be aware they're scoped to first-party tools only.</strong></p><blockquote><p>This is a good reminder that we should never be too reliant on a single tool or provider. Having alternatives is always a good idea.</p></blockquote><h2><a id="opencode-open-source-alternative" href="#opencode-open-source-alternative">OpenCode: Open-Source Alternative</a></h2><p>Speaking of alternatives, if the Anthropic crackdown has you looking around, <a href="https://github.com/anomalyco/opencode/">OpenCode</a> is worth knowing about.</p><p>OpenCode is an open-source AI coding agent with a terminal UI that supports <strong>75+ LLM providers</strong> — Claude, GPT, Gemini, local models, you name it. It's been growing fast: 56K+ GitHub stars and 450+ contributors.</p><p>Key features:</p><ul><li><strong>Provider flexibility</strong>: swap providers or bring your own API keys — no lock-in</li><li><strong>Privacy-first</strong>: no code storage, suitable for regulated environments</li><li><strong>Client/server architecture</strong>: enables remote sessions (Docker containers, mobile control)</li><li><strong>LSP support</strong>: language-aware editing, multi-session, shareable links</li></ul><p>Having briefly tried OpenCode myself, I quite like it, especially as it already supports useful features such as <strong>custom slash commands and skills</strong>, meaning I can reuse many of the workflows I already built for Claude Code.</p><p>Check it out here:</p><ul><li><a href="https://opencode.ai/">OpenCode</a> (source on <a href="https://github.com/anomalyco/opencode/">GitHub</a>)</li><li><a href="https://www.builder.io/blog/opencode-vs-claude-code">OpenCode vs. Claude Code comparison</a></li></ul><h2><a id="claude-code-resources" href="#claude-code-resources">Claude Code Resources</a></h2><p>If you're using (or getting started with) Claude Code, here are some of the best resources I came across this month:</p><ul><li>📝 <a href="https://x.com/eyad_khrais/status/2010076957938188661">The Complete Claude Code Tutorial</a> — a viral X thread (4.7M views) by Eyad Khrais. Core message: <strong>think before typing</strong>. Plan mode outperforms ad-hoc prompting 10 out of 10 times.</li><li>📝 <a href="https://sankalp.bearblog.dev/my-experience-with-claude-code-20-and-how-to-get-better-at-using-coding-agents/">A Guide to Claude Code 2.0</a> — a deep technical guide covering Opus 4.5 workflows, sub-agents, MCP servers, and hooks. Interesting note: the author finds GPT-5.2-Codex superior for code review, while Claude excels at code generation.</li><li>📝 <a href="https://anthropic.skilljar.com/claude-code-in-action">Claude Code in Action</a> — free 21-lesson course from Anthropic (also on <a href="https://www.coursera.org/learn/claude-code-in-action">Coursera</a>). Covers everything from basics to Hooks and the SDK.</li><li>📝 <a href="https://x.com/trq212/status/2014480496013803643">Todos → Tasks in Claude Code 2.1</a> — the latest Claude Code update introduces session-scoped Tasks (replacing Todos) for complex dependency management and parallel sub-agent coordination.</li></ul><h2><a id="latest-from-code-with-andrea" href="#latest-from-code-with-andrea">Latest from Code with Andrea</a></h2><h3><a id="📝-my-2025-in-review-freefall-and-a-new-direction" href="#📝-my-2025-in-review-freefall-and-a-new-direction">📝 My 2025 in Review: Freefall and a New Direction</a></h3><p>At the start of this month, I published my <a href="https://codewithandrea.com/meta/my-2025-retro/">2025 retrospective</a>. It was an honest look at a challenging year: reduced content output, declining traffic and revenue, and the broader headwinds hitting coding educators everywhere.</p><p>But the article is ultimately forward-looking. I'm pivoting Code with Andrea towards <strong>agentic AI coding</strong> — using tools like Claude Code to build Flutter apps faster and smarter. I won't be teaching traditional Dart/Flutter tutorials anymore, but I'll continue to cover Flutter in this newsletter and focus my content on AI-assisted development workflows.</p><p>If you haven't read it yet, I'd love to hear your thoughts:</p><ul><li><a href="https://codewithandrea.com/meta/my-2025-retro/">My 2025 in Review: Freefall and a New Direction</a></li></ul><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>This is by far the most AI-heavy newsletter I've ever written. <strong>AI coding agents have crossed a threshold</strong>, and I find that learning this new abstraction layer is well worth the effort.</p><p>My advice? Start small. Try Claude Code or OpenCode on a side project. Give the agent success criteria instead of step-by-step instructions. And review everything it produces — at least for now 🙂</p><p>I'd love to hear where you are on the AI coding journey! Let me know on <a href="https://x.com/biz84">X (Twitter)</a>, <a href="https://www.linkedin.com/in/andreabizzotto/">LinkedIn</a> or <a href="https://bsky.app/profile/codewithandrea.com">BlueSky</a>.</p><p>Thanks for reading, and happy coding! 🎉</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/december-2025/</guid><title>December 2025: Flutter GenUI SDK, Build Runner Speedups, 2025 LLM Year in Review</title><description>Also included: Material/Cupertino decoupling progress, GPT 5.2 release, running AI agents safely in DevContainers, and MCP becoming an Open Standard.</description><link>https://codewithandrea.com/newsletter/december-2025/</link><pubDate>Mon, 22 Dec 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>As we wrap up 2025, it's been interesting to see how much AI is seeping into the Flutter ecosystem. This year brought us the <a href="https://docs.flutter.dev/ai/mcp-server">Flutter MCP server</a>, <a href="https://docs.flutter.dev/ai/ai-rules">Flutter AI Rules</a>, the <a href="https://docs.flutter.dev/ai/genui">GenUI SDK</a>, and the <a href="https://docs.flutter.dev/ai-toolkit">Flutter AI Toolkit</a>—all official tools for building AI-powered apps with Flutter.</p><p>Meanwhile, the broader AI landscape has seen big leaps in frontier models (Gemini 3, Opus 4.5, GPT-5.2) and agentic coding tools like Claude Code which, while impressive, are still in their <a href="https://www.youtube.com/watch?v=BlVnGXEzFow">early days</a>.</p><p>Let's dive into the latest Flutter videos and AI news from the past month.</p><h2><a id="flutter-videos" href="#flutter-videos">Flutter Videos</a></h2><p>The Flutter team has been pumping out new videos lately. Here are some highlights that caught my attention.</p><h3><a id="📹-getting-started-with-genui" href="#📹-getting-started-with-genui">📹 Getting started with GenUI</a></h3><p>Flutter now has an official SDK for building AI-generated user interfaces, and it's called <a href="https://docs.flutter.dev/ai/genui">GenUI</a>.</p><p>The idea: instead of your LLM responding with walls of text, it can populate UI using a catalog of Flutter widgets—dynamic carousels, workout cards, or any custom widget, created on demand.</p><p>You define a catalog of widgets with JSON schemas, the AI agent generates "surfaces" (UI chunks), and you render them with <code>GenUiSurface</code> widgets. The video walks through a practical example, including Gemini CLI integration and hot reload support.</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="nWr6eZKM6no"></div></div><p>There's also a follow-up video that goes deeper into building agent-powered apps:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="K2p5Nrn2OSU"></div></div><blockquote><p>GenUI is still in alpha, so expect API changes. If you're building regular chat-like experiences, consider the <a href="https://docs.flutter.dev/ai-toolkit">Flutter AI Toolkit</a> instead—it just hit v1.0.0 and offers a more stable set of AI chat widgets.</p></blockquote><h3><a id="📹-strengthening-flutters-core-widgets" href="#📹-strengthening-flutters-core-widgets">📹 Strengthening Flutter's core widgets</a></h3><p>Earlier this year, the Flutter team decided to <a href="https://github.com/flutter/flutter/issues/101479">decouple the Material and Cupertino libraries from the core framework</a>—and this video explains the why, what, and when.</p><p>The tight coupling has been causing issues: design updates lead to breaking changes, third-party design libraries are harder to build, and contributions are harder. The fix? Move design-agnostic logic into the widgets library and eventually publish Material/Cupertino as separate packages on pub.dev.</p><p>Here's the roadmap:</p><ul><li><strong>Phase 1 (now)</strong>: Move common logic into the widgets library; introduce "raw widgets" (e.g., <code>RawRadio</code>) as low-level building blocks</li><li><strong>Phase 2 (2026)</strong>: Publish Material/Cupertino on pub.dev; deprecate the old libraries</li><li><strong>Phase 3 (late 2026)</strong>: Remove legacy libraries from the framework</li></ul><p>For developers, there's no immediate action required. Eventually, you'll be able to version Material and Cupertino independently from the framework, making migrations more predictable:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="W4olXg91iX8"></div></div><h3><a id="📹-accelerating-dart-code-generation" href="#📹-accelerating-dart-code-generation">📹 Accelerating Dart code generation</a></h3><p>If you've ever stared at your terminal waiting for <a href="https://pub.dev/packages/build_runner">build_runner</a> to finish, you know the pain. Good news: a ground-up rewrite of build_runner's transitive import tracking has landed, and it's fast.</p><p>How fast? In one test with 3,000 generated libraries, code generation ran <strong>twice as fast</strong>. Community feedback confirms similar gains in real-world projects using <code>json_serializable</code>, <code>freezed</code>, <code>built_value</code>, and <code>go_router</code>.</p><p>To get the speedup, just upgrade to build_runner 2.10.4+. After that, caching kicks in—changing one library only rebuilds that library, while watch mode (<code>dart run build_runner watch</code>) keeps things snappy as you edit.</p><p>The video also previews what's coming: <strong>augmentations</strong> (inject members directly into classes), <strong>part imports</strong> (generated parts with their own imports), and <strong>primary constructors</strong> (declare constructor params right after the class name). Less boilerplate ahead!</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="6PKIv7yUKwQ"></div></div><h2><a id="ai-news" href="#ai-news">AI News</a></h2><p>If you've been following the AI space, you know there's always something new. Here are some selected reads. 👇</p><h3><a id="📝-2025-llm-year-in-review" href="#📝-2025-llm-year-in-review">📝 2025 LLM Year in Review</a></h3><p>Karpathy's year-end review identifies six paradigm shifts that defined 2025:</p><ol><li><strong>RLVR</strong> (Reinforcement Learning from Verifiable Rewards): Instead of humans rating outputs, LLMs now learn from automated verification—code that compiles, math that checks out. This is why coding models improved so dramatically.</li></ol><ol start="2"><li><strong>Jagged Intelligence</strong>: LLMs are "ghosts," not "animals." They can ace PhD-level physics while failing at basic addition. Understanding this helps set proper expectations.</li></ol><ol start="3"><li><strong>Cursor</strong>: The new LLM app layer emerged—AI-native editors that deeply integrate with codebases, not just chat interfaces bolted onto IDEs.</li></ol><ol start="4"><li><strong>Claude Code</strong>: AI that lives on your computer, browsing files and running commands autonomously. A shift from "AI as tool" to "AI as teammate."</li></ol><ol start="5"><li><strong>Vibe coding</strong>: Programming through natural language. You describe what you want, the AI builds it, and you iterate on vibes rather than syntax.</li></ol><ol start="6"><li><strong>Nano banana</strong>: LLM GUIs are evolving beyond chat. Expect more visual, interactive interfaces that feel less like messaging and more like collaboration.</li></ol><p>If you're building with AI (or just trying to keep up), this is essential reading:</p><ul><li><a href="https://karpathy.bearblog.dev/year-in-review-2025/">2025 LLM Year in Review</a></li></ul><h3><a id="📝-mcp-becomes-an-open-standard" href="#📝-mcp-becomes-an-open-standard">📝 MCP Becomes an Open Standard</a></h3><p>Earlier this month, Anthropic <a href="https://www.anthropic.com/news/donating-the-model-context-protocol-and-establishing-of-the-agentic-ai-foundation">donated the Model Context Protocol</a> to the Linux Foundation's new Agentic AI Foundation (AAIF). This is a big deal.</p><p>If you're not familiar with MCP, it's the protocol that lets AI tools connect to external systems—databases, APIs, file systems, you name it. What makes this donation significant:</p><ul><li><strong>Co-founded by competitors</strong>: Anthropic, OpenAI, and Block, with support from Google, Microsoft, and AWS. When rivals join forces on infrastructure, you know it matters.</li><li><strong>Already widely adopted</strong>: 10,000+ active MCP servers, 97M+ monthly SDK downloads, used by Claude, ChatGPT, Gemini, Cursor, VS Code, and GitHub Copilot.</li><li><strong>Solves a real problem</strong>: Instead of building separate integrations for each AI tool, you build one MCP server and it works everywhere.</li></ul><p>The <a href="https://github.blog/open-source/maintainers/mcp-joins-the-linux-foundation-what-this-means-for-developers-building-the-next-era-of-ai-tools-and-agents/">GitHub blog has a great writeup</a> on what this means for developers. If you're building AI workflows that connect with external data sources, MCP is becoming the infrastructure layer you'll rely on.</p><h3><a id="🔥-gpt-52-is-here" href="#🔥-gpt-52-is-here">🔥 GPT-5.2 is Here</a></h3><p>After the recent release of <a href="https://deepmind.google/models/gemini/">Gemini 3</a> and <a href="https://www.anthropic.com/news/claude-opus-4-5">Opus 4.5</a>, OpenAI was <a href="https://arstechnica.com/ai/2025/12/openai-ceo-declares-code-red-as-gemini-gains-200-million-users-in-3-months/">feeling the heat</a> and decided to respond with <a href="https://openai.com/index/introducing-gpt-5-2/">GPT-5.2</a>.</p><p>Here are the "specs":</p><ul><li><strong>Three variants</strong>: Instant (speed), Thinking (complex work), Pro (maximum accuracy)</li><li><strong>400K token context window</strong> with 128K max output tokens</li><li><strong>38% fewer errors</strong> than the previous version</li><li><strong>State-of-the-art on SWE-Bench Pro</strong>, the benchmark for software engineering tasks</li><li><strong>Knowledge cutoff</strong>: August 2025</li></ul><p>The pricing is higher than GPT-5.1, but if you're doing serious coding work, the improved accuracy might be worth it. It's also available in <a href="https://forum.cursor.com/t/gpt-5-2-is-now-available-in-cursor/145978">Cursor</a> and <a href="https://github.blog/changelog/2025-12-11-openais-gpt-5-2-is-now-in-public-preview-for-github-copilot/">GitHub Copilot</a> now.</p><p>For all the details, read the official announcement:</p><ul><li><a href="https://openai.com/index/introducing-gpt-5-2/">Introducing GPT-5.2</a></li></ul><h2><a id="latest-from-code-with-andrea" href="#latest-from-code-with-andrea">Latest from Code with Andrea</a></h2><p>Every week, I read headlines such as this: <a href="https://www.reddit.com/r/ClaudeAI/comments/1pgxckk/claude_cli_deleted_my_entire_home_directory_wiped/">"Claude CLI deleted my entire home directory"!</a> And it's not just Claude: every AI agent is prone to this kind of security risk, and in my previous newsletter I included a section about <a href="https://codewithandrea.com/newsletter/november-2025/%23%25E2%259A%25A0%25EF%25B8%258F-understanding-agentic-coding-security-risks">understanding Agentic Coding Security Risks</a>.</p><p>But talk is cheap! So I decided to figure out how to run my AI agents in isolated environments, and share my solution in public.</p><p>Here's what I learned. 👇</p><h3><a id="📝-how-to-safely-run-ai-agents-like-cursor-and-claude-code-inside-a-devcontainer" href="#📝-how-to-safely-run-ai-agents-like-cursor-and-claude-code-inside-a-devcontainer">📝 How to Safely Run AI Agents Like Cursor and Claude Code Inside a DevContainer</a></h3><p>If you've used Claude Code or Cursor, you know the friction: constant permission prompts every time the agent wants to read a file or run a command. It's there for good reason (security!), but it slows things down.</p><p>I wrote a guide on using DevContainers to solve this. The idea is to run your AI agent inside an isolated Docker container where it can operate freely without risking your host system.</p><p>What you get:</p><ul><li>Use <code>--dangerously-skip-permissions</code> safely (because the container is sandboxed)</li><li>Protection from prompt injection attacks</li><li>Full support for code generation, file modification, Git, and terminal access</li><li>Step-by-step setup for Docker and the Dev Containers extension</li></ul><p>Admittedly, this setup is not perfect since you can't use it to run iOS/Android emulators, and visual UI testing needs to be done separately on the host machine.</p><p>But when I want to let the agents rip without baby-sitting permissions, this is my go-to solution:</p><ul><li><a href="https://codewithandrea.com/articles/run-ai-agents-inside-devcontainer/">How to Safely Run AI Agents Like Cursor and Claude Code Inside a DevContainer</a></li></ul><blockquote><p>After publishing this article, someone <a href="https://x.com/Blackgentoo/status/1996826290146758967">suggested</a> creating a separate user account (without admin privileges) on my dev machine, and using that in YOLO mode. For some, this might be a reasonable compromise between security and productivity.</p></blockquote><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>2025 is nearly over. I've spent much of it honing my AI skills and applying them to my app development work. Admittedly, I haven't shared as much content as I would have liked, largely due to my recent move to Italy (and all the logistics involved).</p><p>With that said, I have exciting plans for 2026, and I'll be sharing them with you soon. If time allows, I'll also try to share my 2025 retro in the coming weeks (if you're curious, here's the <a href="https://codewithandrea.com/meta/my-2024-retro/">2024 edition</a>).</p><p>But for now, I wish you a happy festive season, and see you in 2026! 🎉</p><p>Happy coding!</p><p>Andrea</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/articles/run-ai-agents-inside-devcontainer/</guid><title>How to Safely Run AI Agents Like Cursor and Claude Code Inside a DevContainer</title><description>Learn how to bypass AI permission prompts safely by running Claude Code in an isolated Docker container.</description><link>https://codewithandrea.com/articles/run-ai-agents-inside-devcontainer/</link><pubDate>Wed, 3 Dec 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>AI coding agents like Claude Code are powerful, but they ask for permission <em>a lot</em>. Every file read, every bash command, every tool invocation: it's permission prompt after permission prompt.</p><p><strong>That's a good thing.</strong> These guardrails exist for your safety because AI agents can make mistakes. Another risk is <a href="https://simonwillison.net/2025/Apr/9/mcp-prompt-injection/">prompt injection attacks</a>, where malicious content tricks the AI into executing harmful commands on your system.</p><p>But sometimes you want to hand off a complex task and let Claude Code run autonomously without babysitting every action. Claude Code offers a <code>--dangerously-skip-permissions</code> flag for this, but the name says it all:</p><figure><picture><source srcset="images/claude-bypass-permissions.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Claude dangerously skip permissions" srcset="images/claude-bypass-permissions.png 2x"/></picture></figure><p>Without permission gates, you're one prompt injection away from a compromised system.</p><p><strong>The solution?</strong> Run Claude Code inside a Docker container. The <a href="https://code.claude.com/docs/en/security">official security documentation</a> recommends this approach: use DevContainers to create isolated environments where Claude Code can operate freely without risking your host machine.</p><h2><a id="why-containers-work" href="#why-containers-work">Why Containers Work</a></h2><p>Containers give you both speed and security:</p><ul><li><strong>Isolation</strong>: AI agents operate in a sandbox. If something goes wrong, only the container is affected.</li><li><strong>File System Protection</strong>: Agents can only access directories you explicitly mount. Personal files, system configs, and sensitive data remain untouched.</li></ul><h3><a id="what-works-well" href="#what-works-well">What Works Well</a></h3><ul><li><strong>Code generation and modification</strong>: Claude can read, write, and modify files</li><li><strong>Terminal commands</strong>: Run tests, analyzers, linters, package managers</li><li><strong>Version control</strong>: Git operations work normally</li><li><strong>Basic IDE Integration</strong>: You can open your project in Cursor/VSCode, navigate the codebase, and edit files.</li></ul><h3><a id="what-doesnt-work" href="#what-doesnt-work">What Doesn't Work</a></h3><ul><li><strong>Simulators/Emulators</strong>: iOS and Android emulators require native platform support (macOS for iOS, more complex setup for Android)</li><li><strong>Visual testing</strong>: Can't see app UI running inside the container</li></ul><p>In this guide, I'll walk you through setting up a DevContainer for Claude Code with Flutter support. By the end, you'll have a secure sandbox where you can run <code>--dangerously-skip-permissions</code> without the actual danger.</p><h2><a id="prerequisites-install-docker" href="#prerequisites-install-docker">Prerequisites: Install Docker</a></h2><p>DevContainers require Docker. Install <a href="https://www.docker.com/products/docker-desktop/">Docker Desktop</a> for your platform and verify it's running before proceeding:</p><figure><picture><source srcset="images/docker-desktop.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Docker Desktop running" srcset="images/docker-desktop.png 2x"/></picture></figure><h2><a id="step-1-install-the-dev-containers-extension" href="#step-1-install-the-dev-containers-extension">Step 1: Install the Dev Containers Extension</a></h2><ul><li><strong>VSCode users</strong>: Install the <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers">Dev Containers</a> and <a href="https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-containers">Container Tools</a> extensions.</li></ul><ul><li><strong>Cursor users</strong>: Install the "Cursor Dev Containers" extension by Anysphere:</li></ul><figure><picture><source srcset="images/dev-containers-extension.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Cursor Dev Containers extension" srcset="images/dev-containers-extension.png 2x"/></picture></figure><blockquote><p>To better understand how containers work, read: <a href="https://code.visualstudio.com/docs/devcontainers/containers">Developing inside a Container</a>.</p></blockquote><h2><a id="step-2-add-the-devcontainer-configuration" href="#step-2-add-the-devcontainer-configuration">Step 2: Add the DevContainer Configuration</a></h2><p>Clone one of these repositories to get the <code>.devcontainer</code> folder:</p><ul><li><strong>Claude Code only</strong>: <a href="https://github.com/anthropics/claude-code">anthropics/claude-code</a></li><li><strong>Claude Code + Flutter</strong>: <a href="https://github.com/bizz84/claude-code-flutter-devcontainer">bizz84/claude-code-flutter-devcontainer</a></li></ul><p>Copy the <code>.devcontainer</code> folder into your project root:</p><figure><picture><source srcset="images/devcontainer-files.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Devcontainer files" srcset="images/devcontainer-files.png 2x"/></picture></figure><h2><a id="step-3-open-the-project-in-the-container" href="#step-3-open-the-project-in-the-container">Step 3: Open the Project in the Container</a></h2><p>Close and reopen your project. You should see "Reopen in Container":</p><figure><picture><source srcset="images/reopen-in-container.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Reopen in Container option" srcset="images/reopen-in-container.png 2x"/></picture></figure><p>Click it. The container will build and you'll see "Container Claude Code Sandbox":</p><figure><picture><source srcset="images/container-claude-code-sandbox.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Claude Code Sandbox container" srcset="images/container-claude-code-sandbox.png 2x"/></picture></figure><p><strong>Verify the setup</strong>: Open a terminal and run <code>pwd</code>. You should see <code>/workspace</code> rather than the project root in your local filesystem:</p><figure><picture><source srcset="images/pwd-workspace.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Running pwd in the container workspace" srcset="images/pwd-workspace.png 2x"/></picture></figure><p>This confirms your project is mounted inside the container. Any commands executed here affect only the container, <strong>not your host machine</strong>.</p><h2><a id="step-4-set-up-claude-code-in-the-container" href="#step-4-set-up-claude-code-in-the-container">Step 4: Set Up Claude Code in the Container</a></h2><p>Run <code>claude</code> in the terminal. You'll see the initial setup process:</p><figure><picture><source srcset="images/claude-get-started.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Claude setup process" srcset="images/claude-get-started.png 2x"/></picture></figure><p>That's because the container has its own Claude Code installation and <strong>it doesn't share credentials with your host machine</strong>. Log in with your Claude subscription or Console account:</p><figure><picture><source srcset="images/claude-login-method.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Claude login method" srcset="images/claude-login-method.png 2x"/></picture></figure><p>Complete the setup steps. You only need to do this once per container. Subsequent runs will go straight to the Claude Code prompt:</p><figure><picture><source srcset="images/claude-code-version.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Claude Code prompt" srcset="images/claude-code-version.png 2x"/></picture></figure><h2><a id="step-5-run-claude-code-without-permission-prompts" href="#step-5-run-claude-code-without-permission-prompts">Step 5: Run Claude Code Without Permission Prompts</a></h2><p>Remember the original goal? We want to safely bypass all permission prompts when running Claude Code.</p><p>Run this:</p><pre><code><div class="highlight"><span></span>claude<span class="w"> </span>--dangerously-skip-permissions
</div></code></pre><p>Claude Code will display a warning:</p><figure><picture><source srcset="images/claude-bypass-permissions.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Claude dangerously skip permissions" srcset="images/claude-bypass-permissions.png 2x"/></picture></figure><p>Accept it. Since you're inside a sandboxed container, Claude Code can only affect the mounted project and <strong>your host system remains protected</strong>.</p><h2><a id="step-6-flutter-support" href="#step-6-flutter-support">Step 6: Flutter Support</a></h2><p>If you used the <a href="https://github.com/bizz84/claude-code-flutter-devcontainer">Claude Code + Flutter DevContainer</a>, you'll be set with a minimal Flutter setup inside the container.</p><p>To test it, you can run <code>flutter doctor</code>, and you should see something like this:</p><pre><code><div class="highlight"><span></span><span class="n">Doctor</span> <span class="n">summary</span> <span class="p">(</span><span class="n">to</span> <span class="n">see</span> <span class="n">all</span> <span class="n">details</span><span class="p">,</span> <span class="n">run</span> <span class="n">flutter</span> <span class="n">doctor</span> <span class="o">-</span><span class="n">v</span><span class="p">):</span>
<span class="p">[</span><span class="err">✓</span><span class="p">]</span> <span class="n">Flutter</span> <span class="p">(</span><span class="n">Channel</span> <span class="n">stable</span><span class="p">,</span> <span class="mf">3.38</span><span class="p">.</span><span class="mi">3</span><span class="p">,</span> <span class="n">on</span> <span class="n">Debian</span> <span class="n">GNU</span><span class="o">/</span><span class="n">Linux</span> <span class="mi">12</span> <span class="p">(</span><span class="n">bookworm</span><span class="p">)</span> <span class="mf">6.10</span><span class="p">.</span><span class="mi">14</span><span class="o">-</span><span class="n">linuxkit</span><span class="p">,</span> <span class="n">locale</span> <span class="n">en_US</span><span class="p">.</span><span class="n">UTF</span><span class="o">-</span><span class="mi">8</span><span class="p">)</span>
<span class="p">[</span><span class="err">✗</span><span class="p">]</span> <span class="n">Android</span> <span class="n">toolchain</span> <span class="o">-</span> <span class="n">develop</span> <span class="k">for</span> <span class="n">Android</span> <span class="n">devices</span>
    <span class="err">✗</span> <span class="n">Unable</span> <span class="n">to</span> <span class="n">locate</span> <span class="n">Android</span> <span class="n">SDK</span><span class="p">.</span>
      <span class="n">Install</span> <span class="n">Android</span> <span class="n">Studio</span> <span class="n">from</span><span class="p">:</span> <span class="n">https</span><span class="p">:</span><span class="c1">//developer.android.com/studio/index.html</span>
      <span class="n">On</span> <span class="bp">first</span> <span class="n">launch</span> <span class="n">it</span> <span class="n">will</span> <span class="n">assist</span> <span class="n">you</span> <span class="k">in</span> <span class="n">installing</span> <span class="n">the</span> <span class="n">Android</span> <span class="n">SDK</span> <span class="n">components</span><span class="p">.</span>
      <span class="p">(</span><span class="n">or</span> <span class="n">visit</span> <span class="n">https</span><span class="p">:</span><span class="c1">//flutter.dev/to/linux-android-setup for detailed instructions).</span>
      <span class="n">If</span> <span class="n">the</span> <span class="n">Android</span> <span class="n">SDK</span> <span class="n">has</span> <span class="n">been</span> <span class="n">installed</span> <span class="n">to</span> <span class="n">a</span> <span class="n">custom</span> <span class="n">location</span><span class="p">,</span> <span class="n">please</span> <span class="n">use</span>
      <span class="p">`</span><span class="n">flutter</span> <span class="n">config</span> <span class="o">--</span><span class="n">android</span><span class="o">-</span><span class="n">sdk</span><span class="p">`</span> <span class="n">to</span> <span class="n">update</span> <span class="n">to</span> <span class="n">that</span> <span class="n">location</span><span class="p">.</span>

<span class="p">[</span><span class="err">✗</span><span class="p">]</span> <span class="n">Chrome</span> <span class="o">-</span> <span class="n">develop</span> <span class="k">for</span> <span class="n">the</span> <span class="n">web</span> <span class="p">(</span><span class="n">Cannot</span> <span class="bp">find</span> <span class="n">Chrome</span> <span class="n">executable</span> <span class="n">at</span> <span class="n">google</span><span class="o">-</span><span class="n">chrome</span><span class="p">)</span>
    <span class="o">!</span> <span class="n">Cannot</span> <span class="bp">find</span> <span class="n">Chrome</span><span class="p">.</span> <span class="n">Try</span> <span class="n">setting</span> <span class="n">CHROME_EXECUTABLE</span> <span class="n">to</span> <span class="n">a</span> <span class="n">Chrome</span> <span class="n">executable</span><span class="p">.</span>
<span class="p">[</span><span class="err">✗</span><span class="p">]</span> <span class="n">Linux</span> <span class="n">toolchain</span> <span class="o">-</span> <span class="n">develop</span> <span class="k">for</span> <span class="n">Linux</span> <span class="n">desktop</span>
    <span class="err">✗</span> <span class="n">clang</span><span class="o">++</span> <span class="k">is</span> <span class="kr">required</span> <span class="k">for</span> <span class="n">Linux</span> <span class="n">development</span><span class="p">.</span>
      <span class="n">It</span> <span class="k">is</span> <span class="n">likely</span> <span class="n">available</span> <span class="n">from</span> <span class="n">your</span> <span class="n">distribution</span> <span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="n">g</span><span class="p">.:</span> <span class="n">apt</span> <span class="n">install</span> <span class="n">clang</span><span class="p">),</span> <span class="n">or</span> <span class="n">can</span> <span class="n">be</span> <span class="n">downloaded</span> <span class="n">from</span> <span class="n">https</span><span class="p">:</span><span class="c1">//releases.llvm.org/</span>
    <span class="p">...</span>
<span class="p">[</span><span class="err">✓</span><span class="p">]</span> <span class="n">Connected</span> <span class="n">device</span> <span class="p">(</span><span class="mi">1</span> <span class="n">available</span><span class="p">)</span>
<span class="p">[</span><span class="err">✓</span><span class="p">]</span> <span class="n">Network</span> <span class="n">resources</span>
</div></code></pre><p>This means that you can run <code>flutter</code> sub-commands such as <code>analyze</code>, <code>test</code>, <code>pub</code> directly from the terminal or via Claude Code. But you can't use <code>flutter run</code> to run the app from the container (more on this below).</p><h3><a id="flutter-ide-integration" href="#flutter-ide-integration">Flutter IDE Integration</a></h3><p>The Dart and Flutter extensions that were installed in your local machine won't automatically work inside the container.</p><p>To fix this, open the extension panel and install them inside the Container.</p><figure><picture><source srcset="images/dart-extensions.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Install Dart extensions in the Container" srcset="images/dart-extensions.png 2x"/></picture></figure><p>Additionally, hit <strong>CMD+SHIFT+P</strong> &gt; <strong>Flutter: Change SDK</strong> and ensure this points to <code>/opt/flutter</code>:</p><figure><picture><source srcset="images/flutter-change-sdk.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Flutter Change SDK" srcset="images/flutter-change-sdk.png 2x"/></picture></figure><p>This ensures you get proper syntax highlighting within the containerized IDE. If this doesn't work right away, hit <strong>CMD+SHIFT+P</strong> &gt; <strong>Dart: Restart Analysis Server</strong>.</p><h3><a id="running-flutter-apps-from-the-container?" href="#running-flutter-apps-from-the-container?">Running Flutter Apps from the Container?</a></h3><p>Unfortunately, you <strong>can't</strong> use <code>flutter run</code> to run the app from the container, since the provided Dockerfile doesn't install any platform-specific tools like Chrome, Android Studio, or Xcode.</p><p>As a workaround, you can open two IDE windows: <strong>one for the container</strong> and <strong>one for your host machine</strong>.</p><ul><li><strong>Container</strong>: For running AI agents and bypassing permission prompts</li><li><strong>Host machine</strong>: For running the app manually and testing the UI</li></ul><p>Here's an example showing things side-by-side:</p><figure><picture><source srcset="images/ide-side-by-side.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Left: Claude code within Cursor (DevContainer mode). Middle: Cursor project on the host machine. Right: iOS Simulator" srcset="images/ide-side-by-side.png 2x"/></picture><figcaption><center><i>Left: Claude code within Cursor (DevContainer mode). Middle: Cursor project on the host machine. Right: iOS Simulator</i></center></figcaption></figure><p>In practice, you'll toggle between the two IDEs as needed, making better use of your screen real estate.</p><h2><a id="container-vs-host-when-to-use-each" href="#container-vs-host-when-to-use-each">Container vs Host: When to Use Each</a></h2><p><strong>Use the Container for:</strong></p><ul><li>Web research on untrusted sources (mitigate prompt injection)</li><li>Long-running autonomous tasks (no constant approval needed)</li><li>New feature development (code generation + file modifications)</li><li>Running MCP servers (which may have security vulnerabilities)</li><li>Parallel execution (useful for benchmarking or testing multiple approaches)</li><li>Installing/updating packages (supply chain attack protection)</li><li>Exploring third-party codebases (unknown code can't affect host)</li></ul><p><strong>Use the Host for:</strong></p><ul><li>Testing the UI (needs simulators/emulators)</li><li>Debugging with breakpoints (IDE debugger integration)</li><li>Hot reload during iteration (faster feedback loop)</li><li>Quick, targeted fixes (verify changes immediately)</li></ul><p>The pattern that emerges: let AI handle the repetitive, high-volume work while you focus on decisions that require human judgment—UI polish, debugging complex state, and verifying the app behaves correctly.</p><h2><a id="bonus-run-claude-code-from-a-standalone-terminal" href="#bonus-run-claude-code-from-a-standalone-terminal">Bonus: Run Claude Code from a Standalone Terminal</a></h2><p>Sometimes you don't need an IDE at all. Maybe you're running Claude Code on a remote VPS, or you just want a lightweight terminal-only workflow for tasks like code generation or refactoring.</p><p>You can use the <code>devcontainer</code> CLI to run containers without VS Code or Cursor:</p><pre><code><div class="highlight"><span></span><span class="c1"># Install the CLI (once)</span>
npm<span class="w"> </span>install<span class="w"> </span>-g<span class="w"> </span>@devcontainers/cli

<span class="c1"># Start the container (only works if the .devcontainer folder is present)</span>
devcontainer<span class="w"> </span>up<span class="w"> </span>--workspace-folder<span class="w"> </span>.

<span class="c1"># Run Claude Code inside it</span>
devcontainer<span class="w"> </span><span class="nb">exec</span><span class="w"> </span>--workspace-folder<span class="w"> </span>.<span class="w"> </span>claude<span class="w"> </span>--dangerously-skip-permissions
</div></code></pre><p>Or start an interactive shell session:</p><pre><code><div class="highlight"><span></span>devcontainer<span class="w"> </span><span class="nb">exec</span><span class="w"> </span>--workspace-folder<span class="w"> </span>.<span class="w"> </span>zsh
</div></code></pre><p>Example:</p><figure><picture><source srcset="images/devcontainer-terminal-claude.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Running Claude Code from a standalone terminal" srcset="images/devcontainer-terminal-claude.png 2x"/></picture></figure><p>This is useful for:</p><ul><li><strong>VPS or cloud servers</strong>: Run autonomous AI agents on remote machines</li><li><strong>CI/CD pipelines</strong>: Automate code generation or refactoring tasks</li><li><strong>Lightweight workflows</strong>: Skip the IDE overhead when you only need the terminal</li></ul><h2><a id="bonus-useful-aliases" href="#bonus-useful-aliases">Bonus: Useful aliases</a></h2><p>To make development faster, I have included these useful aliases in a file called <a href="https://github.com/bizz84/claude-code-flutter-devcontainer/blob/main/.zshrc_dev"><code>.zshrc_dev</code></a>:</p><pre><code><div class="highlight"><span></span><span class="c1"># Aliases for Flutter commands</span>
<span class="nb">alias</span><span class="w"> </span><span class="nv">fclean</span><span class="o">=</span><span class="s2">&quot;flutter clean&quot;</span>
<span class="nb">alias</span><span class="w"> </span><span class="nv">fpg</span><span class="o">=</span><span class="s2">&quot;flutter pub get&quot;</span>
<span class="nb">alias</span><span class="w"> </span><span class="nv">fpu</span><span class="o">=</span><span class="s2">&quot;flutter pub upgrade&quot;</span>

<span class="nb">alias</span><span class="w"> </span><span class="nv">brb</span><span class="o">=</span><span class="s2">&quot;dart run build_runner build -d&quot;</span>
<span class="nb">alias</span><span class="w"> </span><span class="nv">brw</span><span class="o">=</span><span class="s2">&quot;dart run build_runner watch -d&quot;</span>

<span class="nb">alias</span><span class="w"> </span><span class="nv">fpgbrb</span><span class="o">=</span><span class="s2">&quot;fpg &amp;&amp; brb&quot;</span>
<span class="nb">alias</span><span class="w"> </span><span class="nv">fpgbrw</span><span class="o">=</span><span class="s2">&quot;fpg &amp;&amp; brw&quot;</span>

<span class="c1"># Aliases for Claude Code</span>
<span class="nb">alias</span><span class="w"> </span>c-dsp<span class="o">=</span><span class="s1">&#39;claude --dangerously-skip-permissions&#39;</span>

<span class="c1"># Custom prompt or other configurations</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;🚀 Flutter Dev Environment Ready!&quot;</span>
</div></code></pre><p>To use these aliases in the container, copy the file to your home directory:</p><pre><code><div class="highlight"><span></span>cp<span class="w"> </span>.zshrc_dev<span class="w"> </span>~/.zshrc_dev
</div></code></pre><p>Then, rebuild and reopen the container, and the file will be loaded automatically.</p><p>As a result, you'll be able to run the aliases directly:</p><pre><code><div class="highlight"><span></span>fpg<span class="w"> </span><span class="c1"># same as flutter pub get</span>
c-dsp<span class="w"> </span><span class="c1"># same as claude --dangerously-skip-permissions</span>
</div></code></pre><h2><a id="conclusion" href="#conclusion">Conclusion</a></h2><p>You now have a secure setup for running AI agents without permission prompts:</p><ol><li><strong>DevContainer isolation</strong> ensures Claude Code can only access your mounted project</li><li><strong><code>--dangerously-skip-permissions</code></strong> lets Claude Code run autonomously</li><li><strong>Your host machine remains protected</strong> from prompt injection attacks</li></ol><p>This approach gives you the productivity benefits of autonomous AI agents while maintaining the security boundaries that matter.</p><p>More importantly, it changes <em>how</em> you work. Instead of approving every file read and command, you define the task, let Claude Code execute, and review the results. Your role shifts from supervisor to architect—setting direction, evaluating outcomes, and handling the parts that still require human judgment.</p><h3><a id="source-code-and-support-for-other-agentic-ides-and-tools" href="#source-code-and-support-for-other-agentic-ides-and-tools">Source Code and Support for Other Agentic IDEs and Tools</a></h3><p>You can find my custom <code>.devcontainer</code> files on GitHub:</p><ul><li><a href="https://github.com/bizz84/claude-code-flutter-devcontainer">bizz84/claude-code-flutter-devcontainer</a></li></ul><p>This enables Dev Containers on Cursor, VSCode and Claude Code within the built-in terminal.</p><p>If you'd like to add support for Codex and Gemini, feel free to open a PR. 🙂</p><blockquote><p><strong>Note</strong>: <a href="https://antigravity.google/">Antigravity</a> uses the <a href="https://open-vsx.org/">Open VSX</a> marketplace, which doesn't have a Dev Containers extension. If you figure out how to use Dev Containers with Antigravity, please let me know! Meanwhile, you should probably <a href="https://embracethered.com/blog/posts/2025/security-keeps-google-antigravity-grounded/">stay away from it</a>. 😱</p></blockquote><p>Happy coding!</p><h3><a id="resources" href="#resources">Resources</a></h3><ul><li><a href="https://code.claude.com/docs/en/security">Claude Code Security Documentation</a></li><li><a href="https://github.com/bizz84/claude-code-flutter-devcontainer">Claude Code + Flutter DevContainer</a></li><li><a href="https://code.visualstudio.com/docs/devcontainers/containers">VSCode DevContainers Guide</a></li><li><a href="https://github.blog/ai-and-ml/github-copilot/how-githubs-agentic-security-principles-make-our-ai-agents-as-secure-as-possible/">How GitHub’s agentic security principles make our AI agents as secure as possible</a></li><li><a href="https://timsh.org/claude-inside-docker/">Switching to Claude Code + VSCode inside Docker</a></li></ul>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/november-2025/</guid><title>November 2025: Flutter 3.38, Dart 3.10, The AI Coding Wars (Gemini 3 vs Claude Opus 4.5)</title><description>Also included: Google Antigravity IDE, understanding agentic coding security risks, and a different perspective on how AI coding sucks.</description><link>https://codewithandrea.com/newsletter/november-2025/</link><pubDate>Fri, 28 Nov 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>November 2025 was a packed month for both Flutter and AI.</p><p>On the Flutter side, we got Flutter 3.38 with Dart 3.10, bringing dot shorthand syntax, 16KB Android page size support, and full iOS 26 compatibility.</p><p>But the bigger story this month is what I'm calling "The AI Coding Wars." Google launched Gemini 3 alongside their new Antigravity IDE (a free, agent-first development platform), while Anthropic countered with Claude Opus 4.5 (the most capable coding model yet).</p><p>Let's dive in! 👇</p><h2><a id="flutter-338-&-dart-310" href="#flutter-338-&-dart-310">Flutter 3.38 & Dart 3.10</a></h2><p>Flutter 3.38 dropped earlier this month, bringing Dart 3.10 along with it. This is a significant release that includes some breaking changes you'll need to know about.</p><p>If you're maintaining a Flutter app in production, the Android 16KB requirement and Java 17 migration are two things that need your immediate attention. But there's also some great stuff here, like the <a href="https://dart.dev/language/dot-shorthands">dot shorthand syntax</a> finally being enabled by default! 🎉</p><p>Here are the highlights:</p><p><strong>Dart 3.10 Language Features:</strong></p><ul><li><strong>Dot shorthand syntax</strong> is now enabled by default. You can write <code>.value</code> instead of <code>SomeEnum.value</code>, making your code more concise and readable (similar to Swift's syntax)</li><li><strong>Build hooks are now stable</strong>, making it easier to integrate native code (C++, Rust, Swift) without platform-specific build files</li><li>New <strong>analyzer plugin system</strong> for writing custom static analysis rules</li></ul><p><strong>Flutter 3.38 Updates:</strong></p><ul><li><strong>Android 16KB page size support</strong> - This is now required for Google Play. If you haven't updated yet, you need to do this now</li><li><strong>Full iOS 26, Xcode 26, and macOS 26 support</strong> with UIScene lifecycle migration</li><li><strong>Java 17 is now required</strong> for Android development (Gradle 8.14 minimum)</li><li><strong>Fixed a major memory leak</strong> that affected all Flutter Android apps since version 3.29.0 (finally! 🙌)</li><li>Web dev config file support for better team consistency</li><li>DevTools improvements addressing top user pain points</li><li>New <strong>Gemini CLI Extension and MCP support</strong> for AI integration</li></ul><p>Read the full announcements here:</p><ul><li><a href="https://blog.flutter.dev/whats-new-in-flutter-3-38-3f7b258f7228">What's New in Flutter 3.38</a></li><li><a href="https://blog.dart.dev/announcing-dart-3-10-ea8b952b6088">Announcing Dart 3.10</a></li><li><a href="https://blog.flutter.dev/meet-the-flutter-extension-for-gemini-cli-f8be3643eaad">Meet the Flutter Extension for Gemini CLI</a></li></ul><p>There's also an <a href="https://www.youtube.com/watch?v=-AuAZTyRelY">official announcement video</a> if you prefer video content.</p><h2><a id="the-ai-coding-wars" href="#the-ai-coding-wars">The AI Coding Wars</a></h2><p>November marked a major escalation in AI-powered development tools. Google, Anthropic, and OpenAI made huge announcements within days of each other, and the competition is intensifying fast.</p><h3><a id="📝-google-gemini-3" href="#📝-google-gemini-3">📝 Google Gemini 3</a></h3><p>Last week, Google announced <a href="https://blog.google/products/gemini/gemini-3/">Gemini 3</a>, their most capable AI model family yet. This is a massive release that topped the <a href="https://lmarena.ai/leaderboard">LMArena Leaderboard</a> with a 1492 Elo score.</p><p>Here's what stood out to me:</p><ul><li><strong>PhD-level reasoning</strong>: 37.5% on Humanity's Last Exam, 91.9% on GPQA Diamond</li><li><strong>1M+ token context window</strong> with multimodal understanding (text, images, video, audio, PDFs)</li><li><strong>Generative UI</strong> - can create entire interactive experiences, not just text responses</li><li><strong>Gemini Agent</strong> for multi-step tasks with Calendar and Gmail integration</li><li>Google claims it's their <strong>"best vibe coding model ever"</strong> (yes, they actually said that 😅)</li></ul><p>The launch was coordinated across Google Search, Gemini App, AI Studio, Vertex AI, Gemini CLI, and their new Antigravity IDE.</p><h3><a id="📝-google-antigravity-ide" href="#📝-google-antigravity-ide">📝 Google Antigravity IDE</a></h3><p>Alongside Gemini 3, Google launched <a href="https://developers.googleblog.com/build-with-google-antigravity-our-new-agentic-development-platform/">Antigravity</a>, a new IDE forked from VS Code (yes, another one 😄). It's free in public preview and available for Mac, Windows, and Linux.</p><p>What makes Antigravity different:</p><ul><li><strong>Agent-first architecture</strong> - agents autonomously plan, execute, and verify tasks</li><li>Agents have dedicated access to <strong>Code Editor, Terminal, and Browser</strong></li><li><strong>Knowledge base system</strong> for agents to save and learn from context</li><li>Supports <strong>Gemini 3 Pro, Claude Sonnet 4.5, and GPT models</strong></li><li>Built by the ex-Windsurf team (Google acquired them for $2.4B in July)</li></ul><p>If you want a hands-on perspective, check out this video:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="G5Rf0imkTPE"></div></div><h3><a id="📝-claude-opus-45" href="#📝-claude-opus-45">📝 Claude Opus 4.5</a></h3><p>Not to be outdone, Anthropic announced <a href="https://www.anthropic.com/news/claude-opus-4-5">Claude Opus 4.5</a> just days after Gemini 3. They're billing it as "the best model in the world for coding, agents, and computer use."</p><p>The numbers are impressive:</p><ul><li><strong>State-of-the-art on SWE-bench Verified</strong> (real-world software engineering tasks)</li><li><strong>Leads on 7 of 8 programming languages</strong> in multilingual benchmarks</li><li><strong>15% improvement over Sonnet 4.5</strong> on Terminal Bench for long-horizon tasks</li><li>Uses <strong>76% fewer output tokens</strong> than Sonnet 4.5 while matching performance (this means huge cost savings 💰)</li><li>New <strong>"effort parameter"</strong> for balancing capability vs speed/cost</li><li>Pricing: $5/$25 per million tokens (input/output)</li></ul><p>For those of us using Claude Code daily, this is a big deal. The efficiency improvements mean we can tackle more complex agentic coding tasks without burning through credits as fast.</p><blockquote><p><strong>My take:</strong> Frontier models are getting extremely good, but we can only unlock their value by creating <strong>truly agentic workflows</strong>, and that is a whole skill in itself. Thanks to <a href="https://code.claude.com/docs/en/sub-agents">subagents</a>, <a href="https://code.claude.com/docs/en/skills">skills</a>, <a href="https://code.claude.com/docs/en/mcp">MCP servers</a>, and <a href="https://code.claude.com/docs/en/slash-commands#custom-slash-commands">custom slash commands</a>, Claude Code still has a big lead against other agentic coding CLIs, and it's very unlikely I'll switch over unless the competition catches up.</p></blockquote><h3><a id="📝-gpt-51-&-codex" href="#📝-gpt-51-&-codex">📝 GPT 5.1 & Codex</a></h3><p>OpenAI also joined the party with <a href="https://openai.com/index/gpt-5-1/">GPT-5.1</a>, the latest in their GPT-5 series. The model dynamically adapts how much time it spends "thinking" based on task complexity, making it faster and more token-efficient for simpler tasks.</p><p>But the bigger news for developers is the <strong>Codex family</strong>:</p><ul><li><strong>GPT-5.1-Codex</strong> and <strong>GPT-5.1-Codex-Mini</strong> - optimized for long-running, agentic coding tasks</li><li><strong>GPT-5.1-Codex-Max</strong> - the flagship model that can <a href="https://venturebeat.com/ai/openai-debuts-gpt-5-1-codex-max-coding-model-and-it-already-completed-a-24">work independently for 24+ hours</a> on a single task</li></ul><p>Codex-Max introduces "compaction" - the ability to work coherently across millions of tokens, enabling project-scale refactors and deep debugging sessions. On SWE-Bench Verified, it scored <strong>77.9%</strong>.</p><p>For <a href="https://github.blog/changelog/2025-11-13-openais-gpt-5-1-gpt-5-1-codex-and-gpt-5-1-codex-mini-are-now-in-public-preview-for-github-copilot/">GitHub Copilot users</a>, the full GPT-5.1 suite is now available in public preview for Pro, Pro+, Business, and Enterprise plans.</p><h3><a id="⚠️-understanding-agentic-coding-security-risks" href="#⚠️-understanding-agentic-coding-security-risks">⚠️ Understanding Agentic Coding Security Risks</a></h3><p>With all these new agentic coding tools, it's worth understanding the security risks they introduce. Shortly after Antigravity's launch, security researchers discovered <a href="https://embracethered.com/blog/posts/2025/security-keeps-google-antigravity-grounded/">serious vulnerabilities</a> that highlight broader concerns with agent-first approaches:</p><ul><li><strong>Data exfiltration via prompt injection</strong>: Attackers can hide malicious instructions in 1-point font on webpages, forcing the AI to <a href="https://simonwillison.net/2025/Nov/25/google-antigravity-exfiltrates-data/">bypass file protections</a> and exfiltrate secrets</li><li><strong>Bypassing .gitignore</strong>: AI agents can use system commands to access files that should be protected</li></ul><p>These aren't just Antigravity problems—they're challenges any agentic coding tool must address. GitHub published an excellent breakdown of <a href="https://github.blog/ai-and-ml/github-copilot/how-githubs-agentic-security-principles-make-our-ai-agents-as-secure-as-possible/">their agentic security principles</a>, identifying three main threat categories:</p><ol><li><strong>Data Exfiltration</strong> - Agents with internet access could leak sensitive data, including credentials</li><li><strong>Impersonation &amp; Attribution</strong> - Unclear accountability for agent actions</li><li><strong>Prompt Injection</strong> - Malicious instructions hidden in repositories or web pages</li></ol><p>Their recommended safeguards include network firewalling, limited data access, reversibility requirements (PRs instead of direct commits), and clear action attribution.</p><blockquote><p><strong>My take</strong>: As AI tools gain more autonomy, security becomes critical. At minimum, consider using a <a href="https://code.claude.com/docs/en/devcontainer">sandboxed development container</a> for agentic workflows, as <a href="https://www.youtube.com/watch?v=ZnN9HXEIDcI">explained here</a>. And always review what permissions you're granting these tools.</p></blockquote><h2><a id="the-counter-argument-to-ai-coding" href="#the-counter-argument-to-ai-coding">The Counter-Argument to AI Coding</a></h2><p>With all the AI hype this month, I think it's important to acknowledge the other side of the story. Not everyone is having a great time with AI coding tools, and their concerns are valid.</p><h3><a id="📹-ai-coding-sucks" href="#📹-ai-coding-sucks">📹 AI Coding Sucks</a></h3><p>Earlier this month, CJ posted a viral rant titled "AI Coding Sucks" that struck a note across developer communities.</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="0ZUkQF6boNg"></div></div><p>The main criticisms:</p><ul><li><strong>Lost joy of programming</strong> - Endless back-and-forth with unpredictable LLMs that take shortcuts</li><li><strong>Code quality concerns</strong> - AI-generated code can be hard to maintain and understand</li><li><strong>The "skill issue" narrative</strong> - Evangelists dismissing legitimate concerns as user error</li></ul><p><a href="https://x.com/mattpocockuk/">Matt Pocock</a>, a prominent TypeScript educator, had a <a href="https://x.com/mattpocockuk/status/1976380948393373991">thoughtful response</a>, pointing out that <strong>careful planning and context management</strong> can prevent many of the issues with AI coding.</p><h2><a id="latest-from-code-with-andrea" href="#latest-from-code-with-andrea">Latest from Code with Andrea</a></h2><p>Following the "AI Coding Sucks" debate, I've been thinking a lot about one fundamental question: <strong>when should you write code yourself, and when should you use AI?</strong></p><h3><a id="📹-when-to-code-when-to-prompt?-my-2x2-decision-matrix" href="#📹-when-to-code-when-to-prompt?-my-2x2-decision-matrix">📹 When to Code, When to Prompt? My 2x2 Decision Matrix</a></h3><p>The result is this video, aiming to help you decide between AI assistance and manual coding for different Flutter development tasks:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="HOhYX9lA6T8"></div></div><p>The decision matrix is based on comparing <strong>prompting effort</strong> vs <strong>coding effort</strong>:</p><ul><li><strong>Low prompting/High coding effort</strong> → Use AI (boilerplate, tests, refactors)</li><li><strong>High prompting/Low coding effort</strong> → Code manually (visual issues, tiny fixes)</li><li><strong>Low/Low</strong> → Either approach works (simple tweaks)</li><li><strong>High/High</strong> → Collaborative AI approach (complex features, full-stack)</li></ul><p>The core principle is simple: compare prompting effort against coding effort. AI offers speed and knowledge, but "accuracy is not guaranteed"—so you need to factor in the cost of reviewing and fixing AI-generated code.</p><h3><a id="🔥-black-friday-sale-2025" href="#🔥-black-friday-sale-2025">🔥 Black Friday Sale 2025</a></h3><p>Speaking of AI and productivity... if you've been thinking about leveling up your Flutter skills, now's the time. I'm running my annual Black Friday sale:</p><ul><li><strong>50% off</strong> <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> course</li><li><strong>$100 off</strong> the <a href="https://codewithandrea.com/courses/all-courses-bundle/">5x Flutter Course Bundle</a></li></ul><p>These are the best prices you'll see for a while. The sale is live now, so don't wait too long!</p><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>November was a big month for both Flutter and AI development tools. Flutter 3.38 brings some important updates (especially for Android and iOS compatibility), while the AI landscape is evolving faster than ever.</p><p>The competition between Google (Gemini 3 + Antigravity), Anthropic (Claude Opus 4.5 + Claude Code), and OpenAI (GPT 5.1 + Codex) is really heating up, and honestly, I think this innovation unlocks more value and increasingly advanced agentic workflows for all of us.</p><p>As always, remember that <strong>AI is a mutliplier that amplifies both your skills and your mistakes</strong>. So, learn to use it well, and don't feel like you need to go all-in. Sometimes the old-fashioned way of writing code manually is still the right call.</p><p>What's your take on the AI coding wars? What's your favorite AI coding tool? Let me know on <a href="https://x.com/biz84">X (Twitter)</a>, <a href="https://www.linkedin.com/in/andreabizzotto/">LinkedIn</a> or <a href="https://bsky.app/profile/codewithandrea.com">BlueSky</a>.</p><p>Thanks for reading, and happy coding! 🎉</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/videos/when-to-code-vs-prompt-with-ai/</guid><title>When to Code, When to Prompt: My 2x2 Decision Matrix</title><description>A balanced perspective on when to lean on agentic AI vs coding manually, along with a useful decision matrix for common Flutter app development tasks.</description><link>https://codewithandrea.com/videos/when-to-code-vs-prompt-with-ai/</link><pubDate>Wed, 12 Nov 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Is your AI coding workflow causing more friction than flow? Do you wonder when to trust AI and when to just write the code yourself?</p><p>This video offers a balanced perspective on "When to Code vs. Prompt with AI" and introduces a practical decision matrix to guide your development workflow.</p><h2><a id="intro" href="#intro">Intro</a></h2><p>AI agents are excellent for prototyping, ideation, and boilerplate tasks. But you also know the frustration: AI frequently gets stuck or misunderstands, forcing you to take over and write code manually. While some might dismiss this as a "skill issue," the reality is that AI isn't infallible, and you shouldn't force an AI workflow in every scenario.</p><p>So, how do you decide between prompting with AI and traditional coding? This video provides a balanced perspective and a decision matrix to help you choose the right approach. Ultimately, this decision boils down to a simple question: "Which is greater? Your prompting effort, or your coding effort?" The answer, as you'll see, varies significantly by task.</p><h2><a id="common-flutter-tasks" href="#common-flutter-tasks">Common Flutter Tasks</a></h2><p>Consider this list of common tasks you might encounter during Flutter app development:</p><ul><li>Complex charting solutions</li><li>Text rendering issues</li><li>Boilerplate (crash reporting, analytics...)</li><li>Full-stack features</li><li>Small UI/animation tweaks</li><li>Pull to refresh</li><li>IAP + entitlements</li><li>Writing tests</li><li>Dense business logic</li><li>Simple refactors</li><li>Background tasks + iOS/Android specific</li><li>Localization</li><li>Theming system</li><li>Big refactors</li><li>Offline caching + syncing</li><li>Codegen</li><li>Layout errors</li><li>One-line bug fixes</li></ul><h2><a id="the-prompt-vs-coding-effort-matrix" href="#the-prompt-vs-coding-effort-matrix">The Prompt vs. Coding Effort Matrix</a></h2><p>This 2x2 matrix shows which tasks fall in which quadrants depending on their coding vs prompting effort:</p><figure><picture><source srcset="images/effort-code-vs-prompting.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Prompt vs. Coding Effort Matrix" srcset="images/effort-code-vs-prompting.png 2x"/></picture></figure><h3><a id="low-prompting-effort-high-coding-effort" href="#low-prompting-effort-high-coding-effort">Low Prompting Effort, High Coding Effort</a></h3><p>This quadrant contains tasks that require minimal prompting but generate a significant amount of code. AI excels here because these tasks often involve <strong>pattern recognition and repetition</strong> or <strong>filling in the blanks</strong>. Examples include:</p><ul><li><strong>Boilerplate code:</strong> For features like crash reporting or analytics setup.</li><li><strong>Writing tests:</strong> AI can effectively analyze existing code and generate comprehensive test cases, covering various edge cases.</li><li><strong>Localization:</strong> AI can identify hard-coded strings in the UI and generate correct ARB files for multiple languages, streamlining the setup.</li><li><strong>Theming systems:</strong> AI can plan and implement a robust theming system, for instance, by refactoring hard-coded colors and text styles into a consistent theme.</li><li><strong>Big refactors:</strong> Large-scale code rearrangements that follow clear patterns.</li></ul><p>These tasks are not intrinsically hard but are code-intensive, making them ideal for AI to handle.</p><h3><a id="high-prompting-effort-low-coding-effort" href="#high-prompting-effort-low-coding-effort">High Prompting Effort, Low Coding Effort</a></h3><p>This quadrant describes tasks that demand very specific and detailed prompting to get the desired result, but ultimately produce minimal code. In these scenarios, direct coding is often more efficient. Examples include:</p><ul><li><strong>Text rendering issues:</strong> Fixing wrapping, ellipsis, or font rendering quirks requires precise explanations of visual problems.</li><li><strong>Layout errors:</strong> Issues like overflows or unbounded heights need visual descriptions, screenshots, and desired behavior specified, often for a single-line fix (e.g., wrapping a widget in <code>Expanded</code>).</li><li><strong>One-line bug fixes:</strong> Such as adding a missing <code>notifyListeners()</code> call.</li><li><strong>Dense business logic:</strong> Algorithms like binary search, while short in code (less than 20 lines), require verbose and precise natural language prompts to describe, often making the prompt less clear than the code itself.</li></ul><p>Note the inherent <strong>ambiguity of natural language</strong> and the <strong>fidelity gap</strong> that can arise when AI attempts to make precise visual UI changes. If you already understand how to solve the problem, directly editing the code saves time and avoids iterative prompt refinement.</p><h3><a id="low-prompting-effort-low-coding-effort" href="#low-prompting-effort-low-coding-effort">Low Prompting Effort, Low Coding Effort</a></h3><p>These tasks are straightforward to describe with minimal prompting and result in a small amount of code. Examples include:</p><ul><li><strong>Small UI/animation tweaks:</strong> Adjusting an animation curve or duration.</li><li><strong>Adding pull-to-refresh:</strong> Wrapping a <code>ListView</code> with a <code>RefreshIndicator</code> and an <code>onRefresh</code> callback.</li><li><strong>Simple refactors:</strong> Minor code adjustments you can do quickly.</li></ul><p>For problems you're already familiar with, relying on your own <strong>muscle memory</strong> is often faster than context-switching to AI, writing a prompt, and waiting. Furthermore, you'll likely <strong>trust your own code</strong> more.</p><p>I also place <strong>codegen</strong> in this quadrant. While tools like <code>build_runner</code> might generate a lot of code, your human input is minimal (e.g., adding a new property to a data model), and you trust the output because it's a <strong>deterministic process</strong>.</p><h3><a id="high-prompting-effort-high-coding-effort" href="#high-prompting-effort-high-coding-effort">High Prompting Effort, High Coding Effort</a></h3><p>This quadrant encompasses tasks that are significantly complex in terms of specification, planning, implementation, and verification. Working with AI here is rarely a "one-shot" prompt, but rather an <strong>iterative and conversational process</strong>. Examples include:</p><ul><li><strong>Offline caching and syncing.</strong></li><li><strong>Full-stack features.</strong></li><li><strong>Background tasks</strong> with platform-specific (iOS/Android) code.</li><li><strong>Complex charting solutions.</strong></li><li><strong>In-app purchases and entitlements.</strong></li></ul><p>To succeed with AI on these tasks, several critical steps are needed:</p><ul><li><strong>Detailed and specific requirements:</strong> This is the <strong>cost of providing context</strong> to AI, and it requires your significant <strong>domain expertise</strong>. Without it, AI is likely to miss details and go down the wrong path.</li><li><strong>Break down complex problems:</strong> Decompose the main problem into smaller, manageable sub-problems that AI can tackle individually.</li><li><strong>Ensure maintainability:</strong> Follow project standards and conventions.</li><li><strong>Thorough verification:</strong> This is often the most complex part. Debugging AI-generated code can be particularly challenging because you didn't write it yourself, and AI might have introduced subtle errors.</li></ul><p>These tasks have a <strong>high cost of failure</strong>. Careful human oversight and robust guardrails are essential, even with AI assistance.</p><figure><picture><source srcset="images/ai-assisted-coding-feedback.webp 2x" type="image/webp"/><img class="bottom-12px" alt="My AI-assisted coding workflow" srcset="images/ai-assisted-coding-feedback.png 2x"/></picture><figcaption><center><i>My AI-assisted coding workflow</i></center></figcaption></figure><h2><a id="summary" href="#summary">Summary</a></h2><p>Before coding with AI, always consider whether your prompting effort or coding effort will be greater. The answer is task-dependent.</p><p>Additionally, consider your <strong>cognitive load</strong>. A short prompt might still require significant mental effort to formulate. A one-line code change in an unfamiliar system can also be cognitively demanding, where AI can assist you in understanding.</p><h3><a id="agentic-ai-vs-autocomplete" href="#agentic-ai-vs-autocomplete">Agentic AI vs. Autocomplete</a></h3><p>Your choice of tool also depends on the amount of code you need:</p><ul><li>For a few lines of code, <strong>autocomplete and IDE assists</strong> are more surgical and efficient.</li><li>For a lot of code or big changes, <strong>agentic AI workflows</strong> make more sense. AI can handle heavy lifting (planning, implementation), while you focus on providing good specifications and verifying results.</li></ul><p>This mindset is crucial for AI coding in 2025. As AI tools improve, the "high prompting" effort for complex problems will likely decrease, causing the quadrants to shift. What's "high prompt, high code" today might become "low prompt, high code" tomorrow. While AI offers incredible <strong>knowledge</strong> and <strong>speed</strong>, <strong>accuracy is not guaranteed</strong>.</p><p>Your human insights, muscle memory, and direct debugging skills remain indispensable. An important skill in this new era is <strong>knowing <em>when</em> to code and <em>when</em> to prompt</strong>, being selective between full agentic AI mode and autocomplete mode to achieve results more quickly and without frustration.</p><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/october-2025/</guid><title>October 2025: Flutter &amp; Figma MCP, Platform &amp; UI threads merge, Andrej Karpathy on AGI</title><description>Also included: Fluttercon EU videos, Wasm 3.0, my 3-folder system for effective AI coding in Flutter.</description><link>https://codewithandrea.com/newsletter/october-2025/</link><pubDate>Fri, 24 Oct 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Welcome back to another edition of my Flutter newsletter!</p><p>Just like always, I'm excited to share a mix of Flutter and AI-related updates, along with the latest from Code with Andrea.</p><p>Let’s dive in! 🚀</p><h2><a id="flutter-updates" href="#flutter-updates">Flutter Updates</a></h2><p>These updates cover the latest from the official Flutter channel, the recent WASM 3.0 release, and I've also included all the FlutterCon EU 2025 videos for you to catch up on.</p><h3><a id="📺-flutter-&-figma-mcp-|-observable-flutter-#70" href="#📺-flutter-&-figma-mcp-|-observable-flutter-#70">📺 Flutter & Figma MCP | Observable Flutter #70</a></h3><p>It's been a little while since I've shared an Observable Flutter video, but this one is fantastic! Craig Labenz hosts Muhammad Hamza, the creator of a new <a href="https://github.com/mhmzdev/figma-flutter-mcp">MCP server</a> that converts Figma components directly into Flutter widgets.</p><p>In this video, Muhammad explains:</p><ul><li>how the Figma MCP server works its magic</li><li>how you can import typography, colors, and custom buttons into code for your Flutter apps</li><li>how to handle images and SVGs, which often require a bit of special attention.</li></ul><p>He then goes on to show how to generate entire screens (like login and profile pages) from Figma, and even discusses responsive design support and other exciting upcoming features for the MCP server.</p><p>If you're tired of manually creating Flutter widgets from Figma, this tool could be a huge time-saver!</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="d7qrvytOxSA"></div></div><h3><a id="📺-the-great-thread-merge" href="#📺-the-great-thread-merge">📺 The great thread merge</a></h3><p>If you haven't heard about the Platform &amp; UI threads merge yet (<a href="https://github.com/flutter/flutter/issues/150525">it was first enabled in Flutter 3.32</a>), this video will get you up to speed.</p><p>Inside, Craig Labenz breaks down the roles of the UI, Raster, and Platform threads, and explains the reasoning behind the merge. This merge makes it possible to call into native code from Dart <strong>synchronously</strong> (using ffi), which leads to a much smoother development experience.</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="miW7vCmQwnw"></div></div><h3><a id="📝-integrating-swift-foundation-models-in-flutter-apps-with-pigeon" href="#📝-integrating-swift-foundation-models-in-flutter-apps-with-pigeon">📝 Integrating Swift Foundation Models in Flutter Apps with Pigeon</a></h3><p>As part of the iOS 26 SDK, Apple released the <a href="https://developer.apple.com/documentation/foundationmodels">Foundation Models</a> framework, which lets you use on-device LLMs in your apps.</p><p>But how do you call these new APIs from Flutter? The answer is <a href="https://pub.dev/packages/pigeon">Pigeon</a>! In this tutorial, Pranav Masekar shows you how to build a simple chat app as a proof of concept:</p><ul><li><a href="https://sungod.hashnode.dev/foundation-models-in-flutter">Integrating Swift Foundation Models in Flutter Apps with Pigeon</a></li></ul><h3><a id="📝-wasm-30-is-here" href="#📝-wasm-30-is-here">📝 Wasm 3.0 is here</a></h3><p>I've been following WASM support in Flutter web with great interest ever since it was first announced <a href="https://codewithandrea.com/newsletter/april-2023/#%F0%9F%93%B9-flutter-dart-and-wasm-gc-a-new-model-for-web-applications-by-kevin-moore-@-wasm-io-2023">back in 2023</a>.</p><p>So, I was thrilled to hear that Wasm 3.0 has arrived! It's a significant step forward, bringing exciting features like:</p><ul><li>64-bit address space</li><li>Garbage collection</li><li>Typed references</li><li>Tail calls</li><li>Exception handling</li></ul><p>Many of these features are now supported across all major browsers, and I'm really looking forward to seeing what performance improvements this will bring to Flutter web apps.</p><ul><li><a href="https://webassembly.org/news/2025-09-17-wasm-3.0/">Wasm 3.0 Completed</a></li></ul><h3><a id="🎥-fluttercon-eu-2025-videos" href="#🎥-fluttercon-eu-2025-videos">🎥 FlutterCon EU 2025 Videos</a></h3><p>I didn't get a chance to attend FlutterCon this year, so I'm super happy to see that the videos have now been published on YouTube!</p><p>Just like in previous years, some of the top Flutter developers delivered fantastic talks (including some advanced topics), and I'm sure you'll find plenty of useful insights.</p><p>Here's the full playlist:</p><ul><li><a href="https://www.youtube.com/watch?v=cWPxlrKxcxk&list=PL9Pfzam3fFdd9B9H_VO90jyJ0OZKXHaGk">FlutterCon EU 2025 Videos</a></li></ul><h2><a id="ai-news" href="#ai-news">AI News</a></h2><p>Despite some growing <a href="https://www.youtube.com/watch?v=Rc0kNnYgImg">AI-bubble fears</a>, the rollercoaster keeps going, and new tools and updates are popping up every single day. <a href="https://www.anthropic.com/news/claude-sonnet-4-5">Sonnet 4.5</a> recently launched, while OpenAI released <a href="https://openai.com/index/sora-2/">Sora 2</a> and a new <a href="https://openai.com/index/introducing-chatgpt-atlas/">web browser</a> <em>(though I wouldn't trust it to be <a href="https://youtu.be/5uSboan45Zg?si=5WSahL3938zVj-lR&t=113">secure or private</a> 😱).</em></p><p>But what's most relevant for us developers? Here's my top pick from this month. 👇</p><h3><a id="🎧-andrej-karpathy-—-agi-is-still-a-decade-away" href="#🎧-andrej-karpathy-—-agi-is-still-a-decade-away">🎧 Andrej Karpathy — AGI is still a decade away</a></h3><p>Every time Andrej Karpathy speaks publicly, I'm always so impressed by his insights. So, I couldn't miss this new podcast with Dwarkesh Patel, where he shares his thoughts on the evolution of AI and why he believes AGI is still a ways off.</p><p>He also discusses recent trends in agentic AI and the three current ways of writing code:</p><ol><li><strong>From scratch</strong> (he no longer recommends this).</li><li><strong>Autocomplete</strong> with Cursor.</li><li><strong>Agentic</strong>: You prompt, and then let the LLM do the work.</li></ol><p>Interestingly, Andrej often finds himself using autocomplete as the most optimal balance between speed and control, rather than trying to use agents that tend to perform poorly when writing innovative and "intellect-intensive" code.</p><p>The podcast is almost 2 hours and 30 minutes long, but I highly recommend it (a full transcript is also available):</p><ul><li><a href="https://www.dwarkesh.com/p/andrej-karpathy">Andrej Karpathy — AGI is still a decade away</a></li></ul><h2><a id="latest-from-code-with-andrea" href="#latest-from-code-with-andrea">Latest from Code with Andrea</a></h2><p>Over the last month, I've started working on a <a href="https://currency-converter-ab.web.app/">new currency converter app</a>, and I've also published a new video. 👇</p><h3><a id="📺-beyond-prompts-my-3-folder-system-for-effective-ai-coding-in-flutter" href="#📺-beyond-prompts-my-3-folder-system-for-effective-ai-coding-in-flutter">📺 Beyond Prompts: My 3-Folder System for Effective AI Coding in Flutter</a></h3><p>The more I work with AI, the more I realize how crucial it is to have a structured workflow and clear guidelines to shape its output.</p><p>After many successes and failures, I've settled on a 3-folder system that helps me:</p><ul><li>Stay organized and follow a consistent workflow.</li><li>Make it much easier for AI to follow my guidelines.</li><li>Reduce a lot of friction by letting me reuse battle-tested patterns, commands, and prompts.</li></ul><p>And in this video, I cover all the details:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="D68mZzbs6JY"></div></div><h3><a id="until-next-time" href="#until-next-time">Until Next Time</a></h3><p>Over the next few weeks, I'll be ramping up my YouTube content with both long-form and <a href="https://www.youtube.com/shorts/dfR6ByROn7o">short-form</a> videos, sharing my experiences with AI coding. So stay tuned for more updates!</p><p>Happy coding! 🎉</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/videos/ai-folders-for-flutter-development/</guid><title>Beyond Prompts: My 3-Folder System for Effective AI Coding in Flutter</title><description>Dive into my personal 3-folder AI strategy for reducing friction, enforcing coding guidelines, and ensuring an organized and consistent workflow.</description><link>https://codewithandrea.com/videos/ai-folders-for-flutter-development/</link><pubDate>Mon, 20 Oct 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Is there a secret to making your AI agents truly understand your Flutter codebase and massively speed up your development workflow? Well, for me, it's all about these <strong>3 essential folders</strong>.</p><ul><li><strong><code>ai_toolkit</code></strong>: My collection of LLM-friendly commands and patterns.</li><li><strong><code>ai_specs</code></strong>: My prompts and plans for ongoing work.</li><li><strong><code>ai_docs</code></strong>: My persistent knowledge base for AI agents.</li></ul><p>In the video above, I break down exactly what these folders are, how they fit into my AI-assisted workflow, and how you can make the most of them in your AI coding sessions.</p><figure><picture><source srcset="images/ai-assisted-coding-feedback.webp 2x" type="image/webp"/><img class="bottom-12px" alt="My AI-assisted coding workflow" srcset="images/ai-assisted-coding-feedback.png 2x"/></picture><figcaption><center><i>My AI-assisted coding workflow</i></center></figcaption></figure><p>You see, in the last few months, I've been leaning heavily into AI agents like Claude code and Codex. These powerful tools are creating a fundamental shift in how we develop software. It's less about directly writing every line of code ourselves, and more about <strong>orchestrating AI</strong>, <strong>providing precise context</strong>, and <strong>carefully verifying its output</strong>. To get the most out of them, we need to <strong>rethink our entire workflow</strong>.</p><p>But here's the thing: even with powerful AI, they won't produce <strong>consistently</strong> good results unless you give them the right context.</p><p>In fact, it's fair to say that <strong>the results you get are only as good as your prompts</strong>.</p><p>So how can you, as an engineer, learn to use AI to its full potential and leverage this context effectively? That's precisely what these 3 essential folders help you do in your Flutter apps.</p><h2><a id="ai-toolkit-standardizing-ai-interactions" href="#ai-toolkit-standardizing-ai-interactions">AI Toolkit: Standardizing AI Interactions</a></h2><p>First up is the <code>ai_toolkit</code> folder. This is essentially where I keep all my LLM-friendly commands and patterns specifically tailored for Flutter development. I created this because setting up new projects takes time, boilerplates can be too opinionated, and AI agents, while smart, have a high failure rate without proper guardrails.</p><p>Thanks to this toolkit, I can do some really cool things:</p><ul><li>I can prime my AI agents with relevant breaking changes in Dart and Flutter, so they don't generate outdated code.</li><li>I can quickly run commands I use often.</li><li>It helps me enforce consistent project guidelines and code style across all my coding sessions, no matter which AI I'm using.</li><li>And it lets me quickly scaffold new projects with components and functions in a standardized way.</li></ul><p>In practice, I "seed context" into my AI sessions using custom commands from this toolkit. This removes ambiguity, making sure the AI follows my specific code patterns and style. If AI ever misses the mark, I can just add a new guideline to my toolkit and include it in the context. This ensures consistent, higher-quality code. Plus, I've created variants that work across different AI agents, and I include commands for things like conventional commits, planning out features, and even letting the AI know about my local shell aliases to reduce friction.</p><p>Ultimately, the <code>ai_toolkit</code> is all about giving AI the right context and removing friction. I use it as a submodule across my projects, which helps accelerate onboarding, enforces code consistency, and reduces my cognitive load by letting AI handle repetitive decisions. It's a personal tool for now, but I might make it available later!</p><h2><a id="ai-specs-structured-planning-and-execution" href="#ai-specs-structured-planning-and-execution">AI Specs: Structured Planning and Execution</a></h2><p>Next, we have the <code>ai_specs</code> folder. The main idea here is that while agentic AI can plan, build, and verify, the results are still only as good as my prompts. So, I need guardrails to keep AI agents on track. This folder helps me keep track of all the prompts and plans I use for agentic coding, ensuring I follow a consistent workflow.</p><p>My workflow typically involves:</p><ol><li><strong>Creating a requirements file:</strong> This markdown file outlines everything needed for a feature, bug fix, or refactor – it's the "thinking" part.</li><li><strong>Generating a plan:</strong> I feed this requirements file to my AI agent, asking it to make a detailed plan. I'll iterate on this plan until I'm happy, then save it as a markdown file right here in <code>ai_specs</code>.</li><li><strong>Implementing the plan:</strong> I then have the AI implement the plan, ideally one stage at a time, so I can easily review and course-correct.</li><li><strong>Verification:</strong> I let the AI run tests to ensure they're green, and I also manually test the app myself.</li></ol><p>This process is highly iterative, and things don't always go perfectly. I might discover missing requirements, overly complex implementations, or code duplication. The key is to set a high bar for quality. Since producing code with AI is "cheap," I can afford to iterate, correct course, or even restart to ensure the app is maintainable long-term.</p><p>Once everything is complete, tests are green, and the app works, I push changes to GitHub. A neat trick is to instruct the AI to include the completed plan as the Pull Request description. The <code>ai_specs</code> folder then becomes a historical record of all my prompts and plans, almost like a local copy of my GitHub issues and PRs.</p><h2><a id="ai-docs-persistent-project-knowledge" href="#ai-docs-persistent-project-knowledge">AI Docs: Persistent Project Knowledge</a></h2><p>Finally, let's talk about the <code>ai_docs</code> folder. Think of this as a persistent knowledge base specifically for your AI agents. It's a great place for storing crucial information that AI needs but might not find directly in the code or <code>ai_toolkit</code>.</p><p>This includes things like:</p><ul><li>API documentation and integrations.</li><li>Architecture and design documents.</li><li>Hidden non-code business logic (important rules the AI needs to know).</li><li>Project-specific patterns that might be unique to this project, even if they differ from the general toolkit.</li></ul><p>I use this folder in my projects to outline API specifications and UI functionality. As a project grows, I keep this folder updated with all the critical information my AI agents need. For older projects, I've even used AI to help generate this documentation, feeding these documents to the AI when starting a new session so it can understand the project's architecture and generate more consistent code.</p><h2><a id="why-these-folders-matter" href="#why-these-folders-matter">Why These Folders Matter</a></h2><p>So, to summarize, I use these three essential folders in my Flutter development workflow:</p><ul><li><strong><code>ai_toolkit</code></strong>: My collection of LLM-friendly commands and patterns.</li><li><strong><code>ai_specs</code></strong>: My prompts and plans for ongoing work.</li><li><strong><code>ai_docs</code></strong>: My persistent knowledge base for AI agents.</li></ul><p>The main difference is that <code>ai_docs</code> holds knowledge that evolves over time, like living documentation, while <code>ai_specs</code> is more of a historical record of specific tasks, which might become outdated.</p><p>Overall, these folders are incredibly helpful because they:</p><ul><li>Help me stay organized and follow a consistent workflow.</li><li>Make it much easier for AI to follow my guidelines and stay on track.</li><li>Reduce a lot of friction by letting me reuse battle-tested patterns, commands, and prompts.</li></ul><p>I really encourage you to create your own similar folders. Just think about repetitive tasks or situations where AI gets it wrong, and then write your own guidelines to steer it. While there's an upfront time investment, it absolutely pays off in the long run, leading to faster development, fewer bugs, and more consistent quality.</p><p>By the way, in this video, I haven't even touched on advanced topics like MCP servers or sub-agents. Honestly, I feel like I'm just scratching the surface of what's possible with AI agents. But as I keep exploring and learning, I'll be sharing more videos.</p><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/september-2025/</guid><title>September 2025: Riverpod 3.0, Migrating to Flutter, Flutter AI Rules, Best AI Agents</title><description>Also included: Liquid Glass and the cupertino_native package, Flutter vs web wrappers, AI service issues at Anthropic and OpenAI.</description><link>https://codewithandrea.com/newsletter/september-2025/</link><pubDate>Fri, 26 Sep 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Autumn's here, and so is a fresh edition of my newsletter, packed with the latest in Flutter and AI:</p><ul><li>Riverpod 3.0</li><li>The ultimate guide to migrating to Flutter</li><li>Liquid Glass: what it means for you</li><li>AI rules for Flutter and Dart</li><li>Latest from the Flutter community</li><li>Best AI Coding Agents and my takeaways</li></ul><p>Let's dive in!</p><h2><a id="riverpod-30-is-here" href="#riverpod-30-is-here">Riverpod 3.0 is here</a></h2><p>The long-awaited Riverpod 3.0 has landed!</p><p>This new release brings experimental support for offline persistence and mutations, plus quality-of-life improvements like automatic retry, <code>Ref.mounted</code>, generics support, new testing utilities, and more.</p><p>Get a full summary of the changes here:</p><ul><li><a href="https://riverpod.dev/docs/whats_new#other-changes">What's new in Riverpod 3.0</a></li></ul><p>But what about migrating from Riverpod 2.x? After updating some of my own apps, I found some easy changes (like replacing <code>.valueOrNull</code> with <code>.value</code>). But I also hit unexpected bugs, such as <a href="https://github.com/rrousselGit/riverpod/issues/4282">this one</a>.</p><p>While the official docs cover <a href="https://riverpod.dev/docs/3.0_migration">migrating from 2.0 to 3.0</a>, it might be wise to hold off until the initial bugs are ironed out.</p><h2><a id="migration-to-flutter-the-ultimate-guide" href="#migration-to-flutter-the-ultimate-guide">Migration to Flutter: The Ultimate Guide</a></h2><p>Flutter has a strong value proposition: high-quality, multi‑platform apps from a single codebase. But for enterprises with legacy codebases, the path to migration is rarely simple or certain.</p><p>This guide by <a href="https://leancode.co?utm_source=newsletter_bizzotto&utm_medium=email">LeanCode</a> is a practical, battle‑tested playbook for navigating that complexity, born from real-world transformations at Virgin Money, Crédit Agricole Bank Polska, Sonova, NOS, and other large-scale programs.</p><figure><picture><source srcset="images/leancode-migration-to-flutter.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Migration to Flutter: The Ultimate Guide" srcset="images/leancode-migration-to-flutter.png 2x"/></picture></figure><p>I did get early access to the guide, and I highly recommend it. You can get your copy here:</p><ul><li><a href="https://leancode.co/ebook/migration-to-flutter?utm_source=newsletter_bizzotto&utm_medium=email">Migration to Flutter</a></li></ul><h2><a id="liquid-glass-is-here-what-it-means-for-you" href="#liquid-glass-is-here-what-it-means-for-you">Liquid Glass is here: what it means for you</a></h2><p>Apple's <a href="https://www.youtube.com/watch?v=31MbUHX7W8k">latest device lineup</a> is out, bringing iOS 26 and its new <a href="https://developer.apple.com/documentation/technologyoverviews/liquid-glass">Liquid Glass UI</a>.</p><figure><picture><source srcset="images/apple-liquid-ui.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Liquid Glass UI Design System" srcset="images/apple-liquid-ui.png 2x"/></picture></figure><p>But what does this mean for us Flutter developers? And how hard will it be to update our iOS apps to Liquid Glass?</p><p>According to <a href="https://github.com/flutter/flutter/issues/170310">this GitHub issue</a>, official work on the iOS 26 UI will only begin <strong>after</strong> the core Material and Cupertino libraries are <a href="https://github.com/flutter/flutter/issues/101479">moved outside the Flutter SDK</a>.</p><p>Meanwhile, the <a href="https://serverpod.dev/">Serverpod</a> team had an idea: leverage Platform Views and method channels to bring pixel-perfect Liquid Glass UI to Flutter on iOS.</p><p>The result is a new package called <a href="https://pub.dev/packages/cupertino_native">cupertino_native</a>, which works well and is very performant (<em>note: this was vibe-coded with Codex and GPT-5, so it's not ready for production yet</em>).</p><h2><a id="ai-rules-for-flutter-and-dart" href="#ai-rules-for-flutter-and-dart">AI rules for Flutter and Dart</a></h2><p>When building with AI, clear rules and guidelines are crucial for enforcing best practices in code style and design.</p><p>Good news: the Flutter team has released an official set of <a href="https://docs.flutter.dev/ai/ai-rules">AI rules for Flutter and Dart</a>. This includes a <a href="https://raw.githubusercontent.com/flutter/flutter/refs/heads/master/docs/rules/rules.md"><code>rules.md</code></a> template that you can feed directly to your AI model.</p><p>Find all the usage instructions here:</p><ul><li><a href="https://docs.flutter.dev/ai/ai-rules">AI rules for Flutter and Dart</a></li></ul><blockquote><p>Just like you'd start with a default set of <strong>lint rules</strong> for your projects, you can view this as a "sensible defaults" template for Flutter AI coding, which can be further customized to your own project needs.</p></blockquote><h2><a id="latest-from-the-flutter-community" href="#latest-from-the-flutter-community">Latest from the Flutter community</a></h2><p>As I write this, <a href="https://www.fluttercon.dev/">FlutterCon EU</a> is taking place in Berlin. I couldn't make it this year, but I'm keen to catch up on the session videos once they're available.</p><p>The Flutter community has also been active on <a href="https://www.reddit.com/r/FlutterDev/">r/FlutterDev</a> and the official <a href="https://forum.itsallwidgets.com/">Flutter forum</a>. Beyond the usual <a href="https://www.reddit.com/r/FlutterDev/comments/1n5u12a/google_play_must_scrap_this_ridiculous_testing/">Google Play Store complaints</a>, some cool posts can also be found, like this one: 👇</p><h3><a id="📱-flutter-vs-web-wrappers" href="#📱-flutter-vs-web-wrappers">📱 Flutter vs Web Wrappers</a></h3><p>Can you spot a web wrapper masquerading as a native mobile app? Common giveaways include slow loading screens, sluggish UX, and poor offline support.</p><p>Take the HomeDepot app, for instance – it's a web wrapper for their official site. While browsing <a href="https://www.reddit.com/r/FlutterDev/">r/FlutterDev</a>, I found this post a developer who rebuilt it in Flutter. They then shared this <a href="https://cdn.prayershub.com/misc/thd2.mp4">cool video showing the two apps side-by-side</a>. What a difference! 💪</p><p>Check out the full post:</p><ul><li><a href="https://www.reddit.com/r/FlutterDev/comments/1ne7p77/homedepot_app_sucks_so_i_made_a_new_one_not/">HomeDepot app sucks, so I made a new one (not affiliated with Home Depot)</a></li></ul><h2><a id="latest-ai-news" href="#latest-ai-news">Latest AI News</a></h2><p>The past month in AI has been wild! Here's my rundown of the most relevant news.</p><h3><a id="🤖-best-ai-coding-agents-with-some-crazy-upsets" href="#🤖-best-ai-coding-agents-with-some-crazy-upsets">🤖 Best AI Coding Agents with some crazy upsets</a></h3><p>We've seen new, high-performant coding LLMs emerge, including <a href="https://platform.openai.com/docs/models/gpt-5-codex">GPT-5-codex</a>, <a href="https://github.com/QwenLM/Qwen3-Coder">Qwen3 Coder</a>, and <a href="https://x.ai/news/grok-code-fast-1">Grok Code Fast 1</a>.</p><p>The AI coding tool landscape is also expanding, with <a href="https://openai.com/index/openai-codex/">Codex</a>, <a href="https://github.com/QwenLM/qwen-code">Qwen code</a>, <a href="https://roocode.com/">Roocode</a>, and <a href="https://github.com/musistudio/claude-code-router">Claude code router</a> (which enables selecting different models under the same Claude Code CLI), among many others.</p><p>So, which ones should you use for AI-assisted coding? This video offers an excellent summary, complete with benchmarks from real-world evaluations:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="bp5TNTl3bZM"></div></div><p>Here are some key takeaways, complemented with observations from my own experience:</p><ul><li>The gap at the top is narrowing; Claude Code is no longer the only game in town.</li><li>Model choice matters <strong>a lot</strong>. It's crucial to balance <strong>speed</strong>, <strong>cost</strong>, and <strong>output quality</strong>.</li><li>If you want to get good results <strong>consistently</strong>, prompting and context engineering remain as important as ever.</li></ul><p>Model selection also hinges on another critical factor: <strong>reliability</strong>. 👇</p><h3><a id="⚠️-ai-service-issues" href="#⚠️-ai-service-issues">⚠️ AI Service Issues</a></h3><p>While leading AI companies battle for model and tool supremacy, they've also been experiencing significant service issues, as confirmed by the <a href="https://status.openai.com/">OpenAI status</a> and <a href="https://status.claude.com/">Claude status</a> pages.</p><p>Anthropic's situation has been particularly problematic, leading them to share this recent postmortem:</p><ul><li><a href="https://www.anthropic.com/engineering/a-postmortem-of-three-recent-issues">A postmortem of three recent issues</a></li></ul><p>This is the current reality: models are improving, demand is soaring, and AI companies are struggling to guarantee high uptime. Many users on monthly subscriptions have already switched vendors, and I expect this trend to continue.</p><blockquote><p>While uptime guarantees may be less concerning during development, they're absolutely critical when AI is built into production systems (think automotive or medical applications). It will be interesting to see how companies will account for and mitigate downtime.</p></blockquote><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>Having recently moved countries, some things are taking longer than expected, so I'm not yet back at full speed.</p><p>That said, I've started some <a href="https://www.linkedin.com/posts/andreabizzotto_currencyconverter-activity-7373669367185825792-9r3p">new projects</a>, which are proving to be a great way to experiment with the latest AI tools.</p><p>Despite their unreliability, I'm using these tools for increasingly complex tasks, including planning, refactoring, test generation, and more.</p><p>Keep an eye on my YouTube channel; I'll be sharing some agentic AI coding videos soon!</p><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/relative-absolute-imports-dart/</guid><title>Relative &amp; Absolute Imports in Dart</title><description>You can use absolute imports for reusable Dart files that are copy-pasted across projects.</description><link>https://codewithandrea.com/tips/relative-absolute-imports-dart/</link><pubDate>Tue, 23 Sep 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>You can use absolute imports for reusable Dart files that are copy-pasted across projects.</p><p>This way, they are always imported correctly (as long as they live in the same location relative to the project root).</p><figure><picture><source srcset="images/256.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Relative & Absolute Imports in Dart" srcset="images/256.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/matrix4-vector3/</guid><title>Deprecated APIs in Matrix4</title><description>Matrix4 APIs such as translate and scale have been deprecated in Flutter 3.35. Here's how to update your code.</description><link>https://codewithandrea.com/tips/matrix4-vector3/</link><pubDate>Wed, 3 Sep 2025 03:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Some Matrix4 APIs such as <code>translate</code> and <code>scale</code> have been deprecated in the latest Flutter 3.35 release.</p><p>To update them:</p><ul><li>Import <code>vector_math</code> explicitly</li><li>Use the new APIs which take a Vector3 argument</li></ul><figure><picture><source srcset="images/255.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Deprecated APIs in Matrix4" srcset="images/255.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/august-2025/</guid><title>August 2025: Flutter 3.35, Widget Previews, Flutter MCP Server, Latest AI News</title><description>Also included: Dart 3.9, Decoupling Material &amp; Cupertino from the core SDK, new Package of the Week videos, recent studies about AI.</description><link>https://codewithandrea.com/newsletter/august-2025/</link><pubDate>Wed, 27 Aug 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>My long summer break is over, and I’m back in business! This month, we're diving into the new Flutter 3.35 release, the stable Flutter &amp; Dart MCP Server, the significant move to decouple Material &amp; Cupertino, and of course, the ever-evolving world of AI. Let's get into it!</p><h2><a id="flutter-335-&-dart-39-highlights" href="#flutter-335-&-dart-39-highlights">Flutter 3.35 & Dart 3.9 Highlights</a></h2><p>Flutter 3.35 brings two long-awaited features:</p><ul><li><strong>Stateful hot reload on the web</strong>: First <a href="https://codewithandrea.com/tips/hot-reload-flutter-web/">introduced in Flutter 3.32</a>, now available as a stable release.</li><li><strong>Experimental Widget Previews</strong>: This allows you to see your widgets render in real-time, separate from a full app, in your Chrome browser.</li></ul><blockquote><p>I initially doubted the utility of a Flutter Widget Previewer, but after using it, I can see its value, especially for applications with many custom and reusable UI widgets. Check out <a href="https://docs.flutter.dev/tools/widget-previewer">this guide</a> to get started.</p></blockquote><p>Beyond these, the release includes:</p><ul><li>More control and polish in the Material and Cupertino libraries</li><li>Various framework, engine, and DevTools improvements</li></ul><p>Dart 3.9 also ships alongside Flutter 3.35, though it's a fairly minor update.</p><p>For the complete breakdown:</p><ul><li><a href="https://medium.com/flutter/whats-new-in-flutter-3-35-c58ef72e3766">What’s new in Flutter 3.35</a></li><li><a href="https://medium.com/dartlang/announcing-dart-3-9-ba49e8f38298">Announcing Dart 3.9</a></li></ul><p>But hold on, there's more!</p><h3><a id="🤖-dart-and-flutter-mcp-server" href="#🤖-dart-and-flutter-mcp-server">🤖 Dart and Flutter MCP server</a></h3><p>The Flutter MCP (model context protocol) server is now stable and helps AI agents to better understand your code context and perform actions, such as:</p><ul><li>Analyze and fix code errors</li><li>Resolve symbols, fetch documentation, and signature info</li><li>Introspect and interact with you running app</li><li>Search <code>pub.dev</code> for packages</li><li>Manage <code>pubspec.yaml</code> dependencies</li><li>Run tests and analyze the results</li><li>Format code with the same formatter and config as <code>dart format</code></li></ul><p>To learn how to set it up with popular AI tools, read the official guide:</p><ul><li><a href="https://dart.dev/tools/mcp-server">Dart and Flutter MCP server</a></li></ul><blockquote><p>Tools like Claude Code already perform some of the tasks above without extra setup. Now that the Flutter MCP server is stable, I'll do some experiments with and without it, compare the differences, and share my findings.</p></blockquote><h3><a id="🎨-decoupling-design-in-flutter" href="#🎨-decoupling-design-in-flutter">🎨 Decoupling Design in Flutter</a></h3><p>The Flutter team is embarking on a significant architectural change: moving the <code>material</code> and <code>cupertino</code> libraries out of the core Flutter SDK into <strong>standalone packages</strong>. This will make the framework lighter, more flexible, and allow for faster iteration on design libraries.</p><p>What does this mean for you?</p><ul><li><strong>Faster Updates</strong>: Critical bug fixes and design updates for Material and Cupertino will no longer be tied to Flutter's quarterly stable release cycle, reaching developers much faster.</li><li><strong>More Control &amp; Flexibility</strong>: Developers gain finer control over their design system versions and a leaner, un-opinionated core framework for custom UIs, fostering a more diverse Flutter ecosystem.</li></ul><p>This is a substantial undertaking, with the transition expected to complete in 2026. You can track the progress and learn more about the rationale in these resources:</p><ul><li><a href="https://docs.google.com/document/d/189AbzVGpxhQczTcdfJd13o_EL36t-M5jOEt1hgBIh7w/edit?tab=t.0">Decoupling Design in Flutter (Full Proposal)</a></li><li><a href="https://github.com/orgs/flutter/projects/220/views/1">Decoupling Design GitHub Project Board</a></li></ul><h3><a id="🧱-featured-packages" href="#🧱-featured-packages">🧱 Featured Packages</a></h3><p>If your app does some form of audio recording or playback, you might want to check out the <a href="https://pub.dev/packages/record">record</a> and <a href="https://pub.dev/packages/flutter_soloud">flutter_soloud</a> packages, which were recently featured in the <a href="https://youtu.be/2t6Bt04EyLw?si=74o0pWM0MpGKCVN9">Package of the Week</a> playlist:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="Vv2A_nUL1tw"></div></div><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="2t6Bt04EyLw"></div></div><h2><a id="latest-ai-news" href="#latest-ai-news">Latest AI News</a></h2><p>LLMs and AI tools continue to evolve rapidly. A few weeks ago, both <a href="https://openai.com/index/introducing-gpt-5/">GPT-5</a> and <a href="https://www.anthropic.com/news/claude-opus-4-1">Claude Opus 4.1</a> were released, claiming the top spots in popular benchmarks and leaderboards like <a href="https://lmarena.ai/leaderboard/webdev">WebDev Arena</a>.</p><p>But let's not get carried away by the hype: GPT-5 still managed to fail on a <a href="https://x.com/GaryMarcus/status/1954938268618990066">kindergarten worksheet</a>, and <a href="https://x.com/Steve_Yegge">Steve Yegge</a>'s recent attempt to use Claude in production ended with <a href="https://x.com/steve_yegge/status/1946360175339974807?s=12">catastrophic results</a>.</p><p>Moreover, a recent MIT report pointed out that <a href="https://fortune.com/2025/08/18/mit-report-95-percent-generative-ai-pilots-at-companies-failing-cfo/">95% of generative AI pilots at companies are failing</a>, and another study found that <a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/">experienced open-source developers took 19% longer to complete various tasks when using AI tools</a> <em>(note: the developers in the study used Cursor Pro with <strong>Sonnet 3.5/3.7</strong>, which is no longer a frontier model)</em>.</p><p>So, what does this all mean for you?</p><ul><li><strong>Benchmarks don't tell the real story</strong>: Real world usage is what really matters, and there isn't a single tool or LLM that works best for all use cases (even Andrey Karpathy said so in this <a href="https://x.com/karpathy/status/1959703967694545296?s=12">recent and insightful post</a>).</li><li><strong>AI tools can't be trusted</strong>: Make sure you remain in control and put guardrails in place.</li></ul><p>My takeaway is that AI tools can still unlock big productivity gains. I've experienced this firsthand, but not before spending a fair chunk of time learning how to extract value from them (and to be honest, I still have a lot to learn and share).</p><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>I've been a bit quiet recently as I was busy <a href="https://codewithandrea.com/meta/joining-ai-revolution/%23im-moving-to-italy!-%25F0%259F%2587%25AE%25F0%259F%2587%25B9">moving back to my home country</a> 🇮🇹. Now that (almost) everything is settled, I look forward to getting back to work, and sharing my journey with Flutter, AI, and more!</p><p>So stay tuned and keep an eye out on my <a href="https://www.youtube.com/@CodeWithAndrea">YouTube</a>, socials, and this newsletter.</p><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/june-2025/</guid><title>June 2025: Apple Liquid Glass, Software 3.0, how to use LLMs, AI-Assisted coding, Claude Code Crash Course</title><description>Also included: 12 lessons from AI pair programming, and my code review of an open-source Flutter app with 100K+ lines of code.</description><link>https://codewithandrea.com/newsletter/june-2025/</link><pubDate>Fri, 27 Jun 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>This is my 36th monthly newsletter, and this means that I've been writing this Flutter newsletter for <strong>3 years now</strong> (here's the <a href="https://codewithandrea.com/newsletter/archive/">full archive</a>).</p><p>Much has changed since then, and while I was collecting links for this edition, I realized most of my bookmarks from this month were about AI, rather than Flutter.</p><p>Indeed, agentic AI tools have been dramatically changing my development workflow (in a good way), and the overall trend is too hard to ignore by now.</p><p>So, in this edition, I decided to include some relevant AI-related content that will help you expand your knowledge and stay up to date.</p><p>But before that, we need to talk about Apple Liquid Glass! 😅</p><h2><a id="apple-liquid-glass" href="#apple-liquid-glass">Apple Liquid Glass</a></h2><p>Apple announced all their latest software and developer SDK updates at <a href="https://developer.apple.com/wwdc25/">WWDC 2025</a>. Their keynote was focused on <a href="https://www.apple.com/os/">Liquid Glass</a>, a visual refresh of their design system, based on a <a href="https://youtu.be/jGztGfRujSE?si=-ockuL_24SloOZqc">glass-like UI</a>.</p><figure><picture><source srcset="images/apple-liquid-glass.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Preview of Apple Liquid Glass on iOS 26 beta 1" srcset="images/apple-liquid-glass.png 2x"/></picture><figcaption><center><i>Preview of Apple Liquid Glass on iOS 26 beta 1</i></center></figcaption></figure><p>Initial reactions were mixed, with some people praising the new design system, while others criticized it for its poor contrast and accessibility (which was much improved in the <a href="https://x.com/_Lew_X/status/1937248836927234501">second beta</a>).</p><p>But what does this mean for us as Flutter developers?</p><p>Since Flutter draws every pixel (no native UI), apps using the existing Cupertino widgets risk appearing visually outdated on the upcoming <a href="https://en.wikipedia.org/wiki/IOS_26">iOS 26</a> release.</p><p>This triggered a new wave of "Flutter is dead" and "Flutter can't do this" comments, which were quickly followed by open source demos (such as <a href="https://x.com/reNotANumber/status/1935237293305909517">this</a> and <a href="https://x.com/sbis04/status/1934923713335710168">this</a>) showing how to achieve a very similar effect using shaders:</p><figure><picture><source srcset="images/flutter-liquid-glass.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Liquid Glass effect with Flutter shaders (demo by @reNotANumber)" srcset="images/flutter-liquid-glass.png 2x"/></picture><figcaption><center><i>Liquid Glass effect with Flutter shaders (demo by @reNotANumber)</i></center></figcaption></figure><p>The whole conversation is very much in flux, and this umbrella issue was created to track Liquid Glass support in Flutter:</p><ul><li><a href="https://github.com/flutter/flutter/issues/170310">Support for iOS 26 “Liquid Glass” Design in Cupertino Widgets</a></li></ul><blockquote><p>For now, you don't need to do anything. iOS 26 will be released in September, and many things are still subject to change.</p></blockquote><h2><a id="ai-videos" href="#ai-videos">AI Videos</a></h2><p>This is my first AI-related newsletter, so I decided to share some good introductory videos that helped me improve my mindset about LLMs and AI in general. 👇</p><h3><a id="📹-andrej-karpathy-software-is-changing-again" href="#📹-andrej-karpathy-software-is-changing-again">📹 Andrej Karpathy: Software Is Changing (Again)</a></h3><p><a href="https://x.com/karpathy">Andrej Karpathy</a> is an elite AI researcher and engineer, having worked as director of AI at Tesla and founding member of OpenAI.</p><p>In this video, he talks about the evolution of software development paradigms:</p><ul><li><strong>Software 1.0</strong> is written as <strong>traditional code</strong></li><li><strong>Software 2.0</strong> is written as <strong>neural networks</strong>, able to solve specific tasks (e.g. language translation) better than traditional code does</li><li><strong>Software 3.0</strong> is written by LLMs programmed with <strong>natural language prompts</strong></li></ul><p>In his talk, Andrej likens LLMs to operating systems, and <a href="https://x.com/karpathy/status/1617979122625712128">English prompts to programs</a>.</p><p>He also describes LLMs as "stochastic simulations of people" with encyclopedic knowledge but also cognitive deficits like hallucination and "jagged intelligence".</p><p>As a result, he believes <strong>partial autonomy with human verification</strong> is the way forward, and, as a professional developer, I completely agree with his assessment.</p><p>If you have to watch only one video from this newsletter, make it this one:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="LCEmiRjPEtQ"></div></div><h3><a id="📹-karpathy-vs-mckinsey-the-truth-about-ai-agents-software-30" href="#📹-karpathy-vs-mckinsey-the-truth-about-ai-agents-software-30">📹 Karpathy vs. McKinsey: The Truth About AI Agents (Software 3.0)</a></h3><p>I chose to feature this video because it's all about "war at the heart of AI" between business consultants (like McKinsey) and builders (like Andrej Karpathy).</p><p>Here are the two contrasting views:</p><ul><li>Andrej encourages viewing AI as a design problem. He emphasizes that current AI agents need significant human supervision, and that software should be designed with humans as validators, where AI generates and humans validate.</li><li>MkKinsey focuses on "agentic mesh" and a world where agents and data can plug in like USB ports. The video criticises this as "fiction" that misleads CEOs and leads to failed enterprise AI projects.</li></ul><p>Overall, consultants should be honest about the complexity and challenges of building AI systems. It is possible to take Andrej's Software 3.0 vision and tell good business stories, without oversimplifying things to the level of "USB plug and play".</p><p>More realism and less hype is the way to go:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="xZX4KHrqwhM"></div></div><h3><a id="📹-how-i-use-llms" href="#📹-how-i-use-llms">📹 How I use LLMs</a></h3><p>Yet another video from Andrej, this time about how he uses LLMs.</p><p>While this is intended for a general audience, it's a great overview about how LLMs work, along with the extra features they gained over the recent years.</p><p>Here's what's covered:</p><ul><li>Explanation of the concept of tokens</li><li>Context window as working memory</li><li>Pre-training and post-training</li><li>Tool use (internet search, deep search, Python interpreter)</li><li>Multimodality (text, images, audio, video)</li></ul><p>Many examples are included, showing how you might use all these different features in your daily life (and not strictly related to coding).</p><p>If you have the time, I highly recommend it (maybe even share it with less technical friends &amp; family 🙂):</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="EWvNQjAaOHw"></div></div><blockquote><p>I loved the top comment about this video: "Just canceled my wedding this weekend to make time to watch this" 😅</p></blockquote><h2><a id="ai-articles" href="#ai-articles">AI Articles</a></h2><p>If you have digested the videos above and want to get your hands dirty with AI coding, these two articles are a great starting point.</p><h3><a id="📝-ai-assisted-coding-for-teams-that-cant-get-away-with-vibes" href="#📝-ai-assisted-coding-for-teams-that-cant-get-away-with-vibes">📝 AI-assisted coding for teams that can't get away with vibes</a></h3><p>Here's a very good article with tips for building high-quality software faster with AI.</p><p>Here are the main takeaways:</p><ul><li><strong>AI is a multiplier:</strong> To make AI good, get good yourself. If you are a small coefficient, you won’t see much gain. If you are a negative coefficient, expect negative gains.</li><li><strong>What helps the human helps the AI</strong>: Software engineering is the art and science of maintaining a large body of well-defined mental models that achieve a business or economic need. A rich environment and context helps the AI work better.</li><li><strong>Tools and techniques in the editor</strong>: Use the best frontier models (don't cheap out), be excellent at providing context, implementing new features, refactoring, and debugging.</li><li><strong>Tools and techniques outside the editor</strong>: Use AI to grow your skills and knowledge, create extensive documentation, microfriction lubricants, code review, debugging and monitoring live applications, performance optimisations.</li></ul><p>Read on for all the details:</p><ul><li><a href="https://blog.nilenso.com/blog/2025/05/29/ai-assisted-coding/">AI-assisted coding for teams that can't get away with vibes</a></li></ul><h3><a id="📝-what-actually-works-12-lessons-from-ai-pair-programming" href="#📝-what-actually-works-12-lessons-from-ai-pair-programming">📝 What Actually Works: 12 Lessons from AI Pair Programming</a></h3><p>Another good article with practical tips and techniques that move the needle, from someone who spent 6 months pair-programming with AI.</p><p>I used most of these techniques in my own work, and can confirm they work:</p><ul><li><a href="https://forgecode.dev/blog/ai-agent-best-practices/">What Actually Works: 12 Lessons from AI Pair Programming</a></li></ul><blockquote><p><strong>Top tip</strong>: did you know that you can replace <code>github.com</code> with <code>gitingest.com</code> on any GitHub repo URL, and get a single text digest of the codebase? Super useful for feeding a codebase into a LLM. Try it here: <a href="https://gitingest.com/">gitingest.com</a></p></blockquote><h2><a id="latest-from-code-with-andrea" href="#latest-from-code-with-andrea">Latest from Code with Andrea</a></h2><p>A couple of years ago, my opinion about AI was along the lines of "this is cute and somewhat useful". But only recently I started to see gains along the lines of "it just one-shotted a complex feature I couldn't tackle myself 🤯".</p><p>I haven't been this excited about coding for a long time, so much so that I decided to start making YouTube videos again, and share some of my experiences with AI. 👇</p><h3><a id="📹-build-flutter-apps-faster-with-claude-code-opus-4" href="#📹-build-flutter-apps-faster-with-claude-code-opus-4">📹 Build Flutter Apps FASTER with Claude Code Opus 4</a></h3><p>Here's a full crash course on how to use Claude Code to build a <a href="https://voicetimerapp.com/">voice-activated timer app</a>.</p><p>Inside, I share my dev workflow and tips for using Claude Code effectively, while showing you how to build the app from scratch, using AI to generate all the code.</p><p>Watch the video for a real glimpse of modern agentic AI coding:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="FNpQawHnIrI"></div></div><h3><a id="📹-code-review-of-cashew-app-lessons-from-a-flutter-app-with-100k+-downloads" href="#📹-code-review-of-cashew-app-lessons-from-a-flutter-app-with-100k+-downloads">📹 Code Review of Cashew App: Lessons from a Flutter App with 100k+ Downloads</a></h3><p>By <a href="https://www.linkedin.com/posts/andreabizzotto_ive-been-digging-into-popular-open-source-activity-7329156735296835585-cLty/">popular request</a>, I also started a new video series where I dig into popular open-source Flutter apps to see how they’re really built.</p><p>The first video is about <a href="https://github.com/jameskokoska/Cashew">Cashew</a>, a popular budget-tracking app with 100k+ downloads, and 100K+ lines of code, too! Inside, I offer a breakdown of an app’s architecture, patterns, code style, and so on.</p><p>If you've ever needed to make yourself familiar with a new codebase, or simply want to learn how big OSS apps are built, you'll like this format:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="KNsYFMmHfkg"></div></div><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>That's a wrap up for my first AI-heavy newsletter. In this edition, I wanted to include some foundational content about AI, as well as practical tips, and share my first-hand experience building a Flutter app with Claude Code.</p><p>I have a ton of ideas for AI-related content in the future. But for now, I'd like to hear your opinion: did you like this new newsletter format? Is there anything specific you'd like me to cover? Or should I go back to the "Flutter-only" format?</p><p>Let me know on <a href="https://x.com/biz84">X (Twitter)</a>, <a href="https://www.linkedin.com/in/andreabizzotto/">LinkedIn</a> or <a href="https://bsky.app/profile/codewithandrea.com">BlueSky</a>.</p><p>Thanks for reading, and happy coding! 🎉</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/videos/build-flutter-apps-faster-claude-code-opus4/</guid><title>Build Flutter Apps FASTER with Claude Code Opus 4</title><description>A crash course on using Claude Code to build a non-trivial Flutter app involving native integrations, including speech recognition and permissions.</description><link>https://codewithandrea.com/videos/build-flutter-apps-faster-claude-code-opus4/</link><pubDate>Wed, 25 Jun 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>This is an AI transcript/summary of the video above, where show you how I used Claude Code to build a voice-activated timer app from scratch. This isn't a step-by-step tutorial, but a real-world test to see how Claude Code performs on a non-trivial Flutter project involving native integrations like speech recognition and permissions.</p><p>The app idea stemmed from my calisthenics training, where it's impractical to manually control a timer during exercises like the <a href="https://www.tiktok.com/@califitness2/video/7519174055522667798">frog stand</a>. A voice-activated timer that responds to "start" and "stop" commands solves this perfectly. This project was an excellent candidate to test Claude Code's ability to handle both UI and underlying logic, especially involving platform-specific features and permissions.</p><figure><picture><source srcset="images/voice-timer-screenshots.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Voice Timer App Screenshots" srcset="images/voice-timer-screenshots.png 2x"/></picture><figcaption><center><i>Voice Timer App Screenshots</i></center></figcaption></figure><p>Having used Claude Code (specifically the Max plan with access to Opus 4 and Sonnet 4) for a few weeks, I've been impressed. In this crash course, I'll recap the key aspects of my workflow, highlight where Claude Code shined, where it struggled, and share my overall conclusions and tips for using it effectively.</p><h3><a id="useful-links" href="#useful-links">Useful links</a></h3><ul><li><a href="https://www.anthropic.com/claude-code">Claude Code</a></li><li><a href="https://github.com/bizz84/voice_timer_claude_demo">Voice Timer Demo on GitHub</a></li><li><a href="https://voicetimerapp.com/">Voice Timer App on the App Store</a></li></ul><h3><a id="getting-started-with-claude-code" href="#getting-started-with-claude-code">Getting Started with Claude Code</a></h3><p>(Based on 02:21 - 04:24)</p><p>If you're looking to try Claude Code yourself, the setup is straightforward. It's a command-line tool that integrates well with your existing IDE (like Cursor, which I use).</p><ol><li><strong>Installation:</strong> You install it globally using npm:<ul></ul></li></ol><pre><code><div class="highlight"><span></span>npm<span class="w"> </span>install<span class="w"> </span>-g<span class="w"> </span>@anthropic-ai/claude-code
</div></code></pre><ol><li><strong>Running:</strong> Navigate to your project directory in the terminal and simply run <code>claude</code>.</li><li><strong>Authentication:</strong> You'll need to authenticate. The best value, especially if you plan to use it regularly and leverage the most powerful models like Opus 4, is a paid subscription ($20/month for Pro, $100/month for Max). Alternatively, you can use pay-as-you-go billing via an Anthropic API key. As I mentioned in the video, if you code daily and can afford it, the $100/month Max plan is absolutely worth it for Opus 4 access.</li><li><strong>IDE Integration:</strong> Running <code>claude</code> from your IDE's built-in terminal (like in Cursor) is ideal. It allows Claude Code to interact with your project files, while you can easily switch to your editor to review and manually adjust code when needed.</li></ol><p>Claude Code supports different models. On the Max plan, you often have access to Opus 4 for a portion of your usage, after which it might fall back to Sonnet 4. This is important to note, as model performance can vary significantly.</p><h3><a id="getting-started-with-the-flutter-app-&-initial-requirements" href="#getting-started-with-the-flutter-app-&-initial-requirements">Getting Started with the Flutter App & Initial Requirements</a></h3><p>(Based on 04:24 - 07:34)</p><p>I started with a completely empty Flutter project – no extra dependencies, just the default "hello world" app.</p><p>Before writing any code, my most important piece of advice when using AI coding tools is this: <strong>Always start by writing detailed and specific instructions.</strong> This dramatically increases the chances that the AI will produce the code you want and significantly reduces later refactoring.</p><p>For this app, I created a <code>specs</code> folder with an <code>initial-requirements.md</code> file. This document outlined both functional and non-functional requirements:</p><p><strong>Functional Requirements:</strong></p><ul><li>Single-page iOS app built with Flutter.</li><li>Voice-activated timer for calisthenics.</li><li>Basic UI: Timer display showing elapsed time, Start/Stop button, Reset button (like the iOS stopwatch). Use a <code>Stopwatch</code> object.</li><li>Voice Mode: Request microphone and speech recognition permissions on startup. Disable features if denied.</li><li>Voice Commands: Respond to "start" and "stop". Start/continue timer on "start", stop on "stop".</li><li>Audio Feedback: Play a beep sound when a voice command is recognized (initially planned as a TODO).</li></ul><p><strong>Non-Functional Requirements:</strong></p><ul><li>Dark mode only, theming done in <code>MaterialApp</code> (no hardcoded colors/sizes).</li><li>Proper separation of concerns (suitable folder structure like <code>features</code>, <code>shared</code>, <code>core</code>).</li><li>Prefer small, composable widgets.</li><li>Prefer flex values over hardcoded sizes for responsive UI.</li><li>Use <code>log</code> from <code>dart:developer</code> for logging instead of <code>print</code> or <code>debugPrint</code>.</li></ul><p>These opinionated requirements are crucial. Without them, Claude might produce code that functions but doesn't align with your preferred architecture, styling, or conventions, leading to tedious refactoring down the line. For larger projects, I'd add even more detail on linting, dependency injection, testing practices, etc.</p><h3><a id="structuring-the-workflow-with-claude-code" href="#structuring-the-workflow-with-claude-code">Structuring the Workflow with Claude Code</a></h3><p>(Based on 07:34 - 20:15)</p><p>Claude Code offers features that help structure your AI-assisted development workflow.</p><ol><li><strong><code>/init</code> and <code>CLAUDE.md</code>:</strong> Running <code>/init</code> analyzes your project and generates a <code>CLAUDE.md</code> file. This file acts as a permanent context for Claude across sessions. It captures project overview, key requirements it inferred, proposed architecture, coding guidelines, and detected assets. It's a good idea to review and edit this file to ensure it accurately reflects your project's state and guidelines. While Claude might initially be a bit overzealous with bash commands during <code>/init</code>, you can guide it to focus on generating the <code>.md</code> file itself.</li><li><strong>Plan Mode:</strong> Accessible by pressing <code>Shift + Tab</code> twice, Plan Mode is specifically designed for reasoning and breaking down requirements into actionable steps. I used it, referencing my <code>initial-requirements.md</code>, to generate an implementation plan.<ul></ul></li></ol><pre><code><div class="highlight"><span></span><span class="n">Think</span> <span class="n">hard</span> <span class="n">about</span> <span class="n">how</span> <span class="n">to</span> <span class="n">implement</span> <span class="n">all</span> <span class="n">the</span> <span class="n">requirements</span> <span class="n">described</span> <span class="k">in</span> <span class="p">@</span><span class="n">specs</span><span class="o">/</span><span class="n">initial</span><span class="o">-</span><span class="n">requirements</span><span class="p">.</span><span class="n">md</span><span class="p">.</span> <span class="n">Make</span> <span class="n">a</span> <span class="n">plan</span> <span class="n">with</span> <span class="n">all</span> <span class="n">the</span> <span class="n">proposed</span> <span class="n">tasks</span> <span class="n">and</span> <span class="n">subtasks</span><span class="p">,</span> <span class="n">focusing</span> <span class="n">on</span> <span class="n">the</span> <span class="n">UI</span> <span class="bp">first</span><span class="p">.</span> <span class="n">Write</span> <span class="n">all</span> <span class="n">the</span> <span class="n">proposed</span> <span class="n">tasks</span> <span class="n">and</span> <span class="n">subtasks</span> <span class="n">to</span> <span class="n">PLAN</span><span class="p">.</span><span class="n">md</span><span class="p">.</span>
</div></code></pre><ol><li><strong>Iterating on the Plan:</strong> Claude generated a detailed plan with phases, tasks, and subtasks in <code>PLAN.md</code>. I reviewed it and provided feedback to adjust the order, specify technical details (like using a <code>Ticker</code> for UI updates, formatting time as SS.S, using a <code>Stopwatch</code> directly in the UI, removing the "lap" button), and ensure the plan allowed for testing at each step. This iterative planning phase in Plan Mode is invaluable before writing any code.</li><li><strong><code>PLAN.md</code> as Master Plan:</strong> Once the plan was satisfactory, I explicitly told Claude to write it to a file (<code>PLAN.md</code>). This document becomes your single source of truth for the project's implementation steps. Adding a line referencing this file in <code>CLAUDE.md</code> ensures Claude always has access to it.</li><li><strong>Custom Commands:</strong> A powerful feature is the ability to create custom, project-specific commands by adding files to the <code>.claude/commands</code> folder. I created an <code>update-plan-commit</code> command that automatically updates <code>PLAN.md</code> with completed tasks, stages all changes, and creates a clear Git commit message. This command streamlines the process of keeping the plan and version control in sync after completing each step.<ul></ul></li></ol><ol><li><strong>Executing the Plan:</strong> With the plan in place, you tell Claude to proceed step-by-step, stopping after each one for review.</li><li><strong>Using <code>@</code> References:</strong> When giving instructions or asking Claude to modify a specific file, using the <code>@</code> symbol followed by the file path (e.g., <code>@lib/theme/app_theme.dart</code>) is highly effective. It focuses Claude's attention, uses fewer tokens, and generally leads to better results than letting it guess which file you mean.</li><li><strong>Managing Context with <code>/compact</code>:</strong> Long conversation histories can consume tokens. The <code>/compact</code> command shrinks the existing chat history while retaining a summary of key information in the context, helping manage token usage without losing important project knowledge. Alternatively, use the <code>/clear</code> command which resets the context completely; as long as <code>CLAUDE.md</code> and other referenced files like <code>PLAN.md</code> are present, Claude will pick up the project context.</li></ol><p>This structured workflow, leveraging planning, context management, and automation, is key to getting consistent results from Claude Code.</p><h3><a id="building-the-core-timer-ui" href="#building-the-core-timer-ui">Building the Core Timer UI</a></h3><p>(Based on 20:15 - 41:37)</p><p>Following the plan, Claude Code started implementing the core UI:</p><ul><li><strong>Project Setup &amp; Theming:</strong> It correctly set up the project structure, added the specified font to <code>pubspec.yaml</code>, and configured the basic dark theme in <code>MaterialApp</code> and <code>app_theme.dart</code>, following the non-functional requirements. It even caught a linter warning about a deprecated property and fixed it, demonstrating its ability to understand and act on analyzer feedback.</li><li><strong>Timer Display &amp; Page:</strong> Claude created the <code>TimerDisplay</code> widget, correctly using a <code>Ticker</code> for smooth updates based on the screen refresh rate and implementing the requested SS.S time formatting. It also created the <code>TimerPage</code> widget to hold the <code>Stopwatch</code> and pass it down to the <code>TimerDisplay</code>. This initial UI implementation was one-shotted perfectly based on the plan.</li><li><strong>Timer Controls (Buttons):</strong> It then implemented the Start/Stop and Reset buttons. Claude correctly used widget composition, creating a reusable <code>TimerControlButton</code> and then specific <code>StartStopButton</code> and <code>ResetButton</code> widgets that wrapped it. This demonstrated good code structure and saved manual refactoring.</li></ul><p>At each step, I reviewed the generated code (often using the source control diff view in Cursor) and committed the changes using my custom <code>update-plan-commit</code> command.</p><p><strong>Using Screenshots for UI Design (28:27):</strong></p><p>Wanting a more visually appealing UI, I decided to give Claude Code a screenshot of the desired design from the production app. This is a fantastic feature – you can drag and drop an image into the prompt.</p><pre><code><div class="highlight"><span></span><span class="n">Using</span> <span class="n">the</span> <span class="n">provided</span> <span class="n">screenshot</span> <span class="k">as</span> <span class="n">inspiration</span><span class="p">,</span> <span class="n">update</span> <span class="n">the</span> <span class="n">styling</span> <span class="n">and</span> <span class="n">design</span> <span class="n">of</span> <span class="n">the</span> <span class="n">timer</span> <span class="n">display</span><span class="p">,</span> <span class="n">start</span> <span class="n">and</span> <span class="n">stop</span> <span class="n">button</span><span class="p">,</span> <span class="n">reset</span> <span class="n">button</span><span class="p">,</span> <span class="n">and</span> <span class="n">voice</span> <span class="n">indicator</span> <span class="n">widget</span> <span class="p">(</span><span class="n">we</span><span class="err">&#39;</span><span class="n">ll</span> <span class="n">use</span> <span class="n">this</span> <span class="n">later</span><span class="p">).</span> <span class="n">Note</span> <span class="n">the</span> <span class="n">start</span> <span class="n">and</span> <span class="n">reset</span> <span class="n">button</span> <span class="n">should</span> <span class="n">still</span> <span class="n">appear</span> <span class="n">side</span> <span class="n">by</span> <span class="n">side</span> <span class="n">inside</span> <span class="n">the</span> <span class="n">row</span><span class="p">.</span> <span class="n">Keep</span> <span class="k">as</span> <span class="n">much</span> <span class="n">styling</span> <span class="k">as</span> <span class="n">possible</span> <span class="k">in</span> <span class="n">the</span> <span class="n">top</span> <span class="n">level</span> <span class="n">theme</span><span class="p">,</span> <span class="n">declaring</span> <span class="n">constants</span> <span class="n">with</span> <span class="n">meaningful</span> <span class="n">names</span> <span class="k">in</span> <span class="n">a</span> <span class="n">separate</span> <span class="n">file</span> <span class="n">rather</span> <span class="n">than</span> <span class="n">hard</span> <span class="n">coding</span> <span class="n">values</span> <span class="k">in</span> <span class="n">the</span> <span class="n">widget</span> <span class="n">themselves</span><span class="p">.</span>
</div></code></pre><ul><li><strong>First Attempt (with Sonnet 4):</strong> Claude Code attempted to match the design. It created an <code>app_constants.dart</code> file for colors and sizes, updated the theme, and modified the UI widgets. However, the result wasn't perfect – it added an unwanted linear gradient, some constants were too widget-specific for a global file, the app bar title didn't use the correct font, and there was a layout overflow issue. This attempt ran after Opus 4 usage limit was reached, potentially impacting the result.</li><li><strong>Decision Point:</strong> I had a choice: manually refactor or discard and try again with a better prompt (and hopefully Opus 4). I chose the latter to see if Claude Code could nail it with clearer instructions.</li><li><strong>Second Attempt (with Opus 4):</strong> I discarded the changes, explicitly added a file with my desired <code>AppColors</code>, and modified the prompt. I emphasized using the provided colors, ensuring all text used the correct font, and adjusting layout factors. This time, running on Opus 4, the result was much better. Claude correctly used the provided colors, applied the font consistently via the theme, and produced a layout much closer to the screenshot, fixing the overflow issue. While some minor tweaks were still needed (like adding button labels or adjusting the voice indicator border), the result was good enough to proceed or make final adjustments manually.<ul></ul></li></ul><pre><code><div class="highlight"><span></span><span class="n">Using</span> <span class="n">the</span> <span class="n">updated</span> <span class="n">screenshot</span> <span class="k">as</span> <span class="n">inspiration</span><span class="p">,</span> <span class="n">update</span> <span class="n">the</span> <span class="n">styling</span> <span class="n">and</span> <span class="n">design</span> <span class="n">on</span> <span class="n">the</span> <span class="n">timer</span> <span class="n">display</span><span class="p">,</span> <span class="n">start</span> <span class="n">stop</span> <span class="n">button</span><span class="p">,</span> <span class="n">reset</span> <span class="n">button</span><span class="p">,</span> <span class="n">and</span> <span class="n">voice</span> <span class="n">indicator</span> <span class="n">widget</span><span class="p">.</span> <span class="n">Keep</span> <span class="k">as</span> <span class="n">much</span> <span class="n">styling</span> <span class="k">as</span> <span class="n">possible</span> <span class="k">in</span> <span class="n">the</span> <span class="n">top</span> <span class="n">level</span> <span class="n">theme</span> <span class="n">and</span> <span class="n">ensure</span> <span class="n">all</span> <span class="n">text</span> <span class="n">widgets</span> <span class="n">use</span> <span class="n">the</span> <span class="n">Babas</span> <span class="n">new</span> <span class="n">font</span><span class="p">.</span> <span class="n">When</span> <span class="n">styling</span> <span class="n">the</span> <span class="n">UI</span><span class="p">,</span> <span class="n">use</span> <span class="n">the</span> <span class="n">colors</span> <span class="k">in</span> <span class="p">@</span><span class="n">lib</span><span class="o">/</span><span class="n">constants</span><span class="o">/</span><span class="n">app_colors</span><span class="p">.</span><span class="n">dart</span><span class="p">.</span>
</div></code></pre><p>This demonstrated that providing clear guidance, managing context (by providing the color file), and leveraging the best model (Opus 4) significantly improves the quality of the output, especially for complex tasks like UI design from an image.</p><h3><a id="implementing-permissions" href="#implementing-permissions">Implementing Permissions</a></h3><p>(Based on 41:37 - 54:48)</p><p>Next, we moved on to integrating native permissions for microphone and speech recognition:</p><ul><li><strong>Adding Dependencies &amp; Platform Setup:</strong> Claude correctly identified the need for the <code>permission_handler</code> package and added it to <code>pubspec.yaml</code>. Crucially, it also figured out the necessary platform-specific configuration for iOS, adding the privacy usage descriptions to <code>Info.plist</code> and the required build configurations to the Podfile based on the package's README. This ability to read documentation and apply platform setup is a major strength.</li><li><strong>Requesting Permissions:</strong> Claude created a <code>PermissionService</code> class to encapsulate the permission logic. It correctly used the <code>permission_handler</code> API to request both microphone and speech recognition permissions simultaneously, returning true only if both are granted.</li><li><strong>Integrating into UI Lifecycle:</strong> Initially, Claude placed the permission request logic in <code>main.dart</code>. I guided it to move this logic to the <code>initState</code> method of the <code>TimerPage</code> widget, which made more sense for this single-page app and kept UI-related state management within the page widget. Claude successfully refactored the code as requested.</li><li><strong>Handling Denials with Alerts:</strong> The requirements specified showing an alert if permissions are denied. Claude implemented this, but initially used a standard Material AlertDialog. To match the native iOS look, I introduced my own <code>show_alert_dialog.dart</code> helper file (which uses <code>showAdaptiveDialog</code>) and instructed Claude to use it instead. Claude successfully integrated my helper function, including wiring up the "Open Settings" button action.</li></ul><p>Debugging the permission flow required running on a real device (simulator behavior can differ). We encountered an issue where the native permission dialog wasn't appearing on first launch. Claude Code, with some prompting ("Is some more platform-specific configuration needed?"), correctly identified the missing Podfile configuration requirement from the <code>permission_handler</code> README, which resolved the issue. This again showcased its strength in understanding documentation and platform-specific setup.</p><h3><a id="building-voice-recognition" href="#building-voice-recognition">Building Voice Recognition</a></h3><p>(Based on 54:48 - 1:10:52)</p><p>Implementing the continuous voice recognition loop was the most complex part and where Claude Code faced the biggest challenge in the live demo.</p><ul><li><strong>Detailed Requirements:</strong> Given the complexity, I created a specific <code>voice-recognition.md</code> document outlining the desired behavior: initialize the service if permissions are granted, start a continuous listening loop, stop listening as soon as a command ("start" or "stop") is recognized, process the command, play a beep (TODO), and restart the listening loop. It also included guidelines for simple, stateless implementation and graceful error recovery.</li><li><strong>Planning the Feature:</strong> Using Plan Mode and referencing the new requirement document, Claude generated a plan for implementing the voice recognition feature, including adding the necessary package (<code>speech_to_text</code>), creating a <code>VoiceCommandService</code>, integrating it with the <code>TimerPage</code>, and updating the voice indicator UI. Ensuring Claude took the detailed <code>voice-recognition.md</code> into account required a specific prompt in Plan Mode.</li><li><strong>Initial Implementation:</strong> Claude added the <code>speech_to_text</code> package and created the <code>VoiceCommandService</code>. The initial code had some issues: it held state variables (<code>isListening</code>, <code>isInitialized</code>) that ideally belonged in the UI layer, used non-idiomatic Dart (<code>setCallbacks</code>), and the continuous listening logic was complex, involving <code>Future.delayed</code> calls and recursive method calls (<code>startListeningLoop</code>). The <code>onSpeechResult</code> handling had duplicate logic.</li><li><strong>Iterating for Improvements:</strong> Recognizing the code wasn't ideal, I created a detailed <code>voice-recognition-improvements.md</code> document listing specific suggestions: make the service stateless, remove <code>setCallbacks</code> and <code>Future.delayed</code>, use an enum for commands, improve the <code>startListening</code> API to return a <code>Future&lt;TimerCommand&gt;</code>, use a <code>Completer</code> to signal command recognition and stop listening before completing, and remove unnecessary helper methods.</li><li><strong>Second Attempt:</strong> I provided this document to Claude Code. It made significant changes, resulting in a much simpler and cleaner <code>VoiceCommandService</code> that used a <code>Completer</code> as suggested. It successfully made the service stateless and updated the <code>TimerPage</code> to manage the state and the listening loop logic.</li><li><strong>Testing Challenges:</strong> Testing on a real device confirmed that voice recognition worked for the first command ("start" or "stop"), but the listening loop would get stuck if any other speech was detected. While Claude Code's code was much improved, this specific bug in the continuous listening loop logic was complex and proved difficult to resolve within the demo session, requiring manual trial and error later in the production app.</li></ul><p>This experience highlighted that while Claude Code is excellent at understanding requirements and generating complex patterns (like using a <code>Completer</code> or generating tests, as seen later), intricate state management and lifecycle issues (like a robust continuous listening loop) can still be challenging for it to perfect without extensive guidance or manual debugging.</p><h3><a id="beyond-the-demo-production-app-examples" href="#beyond-the-demo-production-app-examples">Beyond the Demo: Production App Examples</a></h3><p>(Based on 1:11:38 - 1:14:19)</p><p>While the live coding session showed the process and some challenges, the completed production app (available on the App Store) demonstrates Claude Code's ability to handle even more:</p><ul><li><strong>Custom UI (<code>_StadiumBorderPainter</code>):</strong> The animated circling border around the voice indicator is a custom painter. I showed Claude a screenshot of a similar effect and described what I needed. It generated the entire <code>_StadiumBorderPainter</code> class with animation logic, saving significant time on complex drawing code.</li><li><strong>Info Button &amp; Action Sheet:</strong> Implementing the info button that brings up a native-looking action sheet with voice commands was a one-shot success based on a single prompt.</li><li><strong>Advanced Time Formatting &amp; Unit Tests:</strong> The ability to display and <em>speak</em> the time in different formats was a complex task. I defined a <code>TimeFormat</code> enum with <code>formatTime</code> (for UI) and <code>formatTimeForSpeech</code> methods. Generating the nuanced speech formatting logic for each format based on examples I provided was impressive. Even more so, Claude Code generated a comprehensive suite of unit tests for this logic and used its agentic capabilities to run them, find failures, and help me fix bugs in the generated code. This combination of complex logic generation <em>and</em> test-driven bug fixing was a major win.</li></ul><p>These production examples show that when properly guided, Claude Code can generate complex UI elements, handle nuanced logic, and even write tests, significantly accelerating development.</p><h3><a id="key-takeaways-and-tips-for-effective-use" href="#key-takeaways-and-tips-for-effective-use">Key Takeaways and Tips for Effective Use</a></h3><p>(Based on 1:14:00 - 1:16:40)</p><p>So, what are the key lessons learned from using Claude Code to build this app? It boils down to this: Claude Code is a powerful skill multiplier, but it's not a magic wand. The value you get is directly proportional to how well you use it.</p><p>Here are my top tips for effective AI-assisted development with Claude Code:</p><ul><li><strong>Be Specific &amp; Write Detailed Requirements:</strong> Forget "vibe coding." Invest time upfront to write clear, detailed requirements covering functional needs, UI details, desired behavior, and crucial non-functional preferences like code style (in a spec document like <code>@docs/initial-requirements.md</code>). The more precise you are, the better Claude will understand your intent and the less refactoring you'll need later.</li><li><strong>Use Planning &amp; Structure Your Workflow:</strong> Break down the project into smaller, manageable tasks using Claude's Plan Mode (<code>PLAN.md</code>). Keep the plan updated and follow it step-by-step. This structured approach, combined with reviewing and committing changes after each task, maintains a working state and makes it easy to revert if needed. Consider asking Claude to ensure your app is testable at each step.</li><li><strong>Actively Guide &amp; Review Code Ruthlessly:</strong> Don't blindly accept Claude's output. Read the code, make sure you understand it, and be critical. If it's not what you want – maybe it's overly complex or doesn't fit your patterns – be prepared to discard the changes and provide more specific guidance or a better prompt. Like when it first attempted the <code>VoiceCommandService</code>, I had to provide specific feedback to steer it in the right direction. Remember: <strong>Generating code is cheap; maintaining bad code is expensive.</strong></li><li><strong>Optimize Context &amp; Use <code>@</code> References:</strong> Be specific in your prompts and use <code>@</code> references to relevant files (<code>@docs/initial-requirements.md</code>, <code>@PLAN.md</code>, specific code files) to ensure Claude has the necessary context without wasting tokens.</li><li><strong>Leverage Opus 4:</strong> Use the most powerful model available. I found Opus 4 significantly better than Sonnet 4 for planning, complex problem-solving, and generating more accurate code upfront.</li><li><strong>Automate Repetitive Tasks:</strong> Identify recurring actions (like updating the plan, committing, or running specific checks) and create custom commands for them to streamline your workflow.</li><li><strong>Speed Up Verification with Tests:</strong> Use Claude to help write unit tests for the code it generates. Since Claude is agentic, it can then run these tests after making changes, automating part of the verification process and helping you catch bugs faster.</li></ul><p>Applying these principles makes Claude Code a powerful skill multiplier. It can handle boilerplate, figure out API usage from documentation, generate complex logic and tests, and write most of the code for you, which honestly feels quite magical sometimes. This structured, deliberate approach makes all the difference between "Vibe coding" (which often only produces sloppy products) and "AI-assisted software development" (which helps you build robust and maintainable apps). I'd much rather be in that second category!</p><h3><a id="from-prototype-to-production-ready-app" href="#from-prototype-to-production-ready-app">From Prototype to Production-Ready App</a></h3><p>(Based on 1:16:40 - 1:17:33)</p><p>As we saw, the app built <em>during the video</em> was a working prototype, and a pretty good one thanks to Claude Code. However, a truly production-ready app requires more: polish, robust error handling, analytics, potentially backend integrations, continuous integration, etc.</p><p>The final app I published includes crucial features like analytics, user feedback mechanisms, and error monitoring with Sentry. Many of these are standard boilerplate or patterns I already had battle-tested in my other apps. For those, it was often quicker for me to copy-paste and adapt my existing code rather than asking Claude to implement them from scratch.</p><p>So, while Claude Code is fantastic for building core features and tackling new problems, don't hesitate to reuse your own proven code for standard infrastructure pieces. You could even explore writing custom Claude commands to help integrate your pre-existing code patterns more easily into new apps.</p><h3><a id="conclusion" href="#conclusion">Conclusion</a></h3><p>(Based on 1:17:33 - 1:18:42)</p><p>Ultimately, Claude Code can do a lot of the heavy lifting for you and significantly accelerate development. So, my advice is to give it a go. If you're a professional software developer and you can afford it, the Max plan is absolutely worth the investment, and Opus 4 is a big step forward compared to other models available in different tools.</p><p>Now, you might be thinking: What about Cursor? Should you still use it, or is it better to use Claude Code for everything? As you've seen, I've been using Claude Code extensively for planning, generating code, and tackling complex tasks. However, I still find Cursor incredibly useful for its deep IDE integration – things like seamless tab-autocompletion directly in the editor, and for making quick, small inline changes or refactors manually. So, at the moment, I'm actually still paying for both tools. This hybrid approach allows me to leverage Claude Code's powerful model and agentic capabilities for larger tasks and planning, while still benefiting from Cursor's tight IDE features for day-to-day coding and manual adjustments. Using them both strategically lets me maximize my productivity.</p><p>On a final note, we've only scratched the surface of what Claude Code can do in this video. If you want to learn more about its features or dive deeper into AI-assisted development, you can find some useful links and resources below.</p><h3><a id="additional-resources" href="#additional-resources">Additional Resources</a></h3><ul><li><a href="https://www.youtube.com/live/6eBSHbLKuN0?si=oMf659w0wVoo9lF3">Mastering Claude Code in 30 minutes</a></li><li><a href="https://www.anthropic.com/engineering/claude-code-best-practices">Claude Code: Best practices for agentic coding</a></li><li><a href="https://youtu.be/TiNpzxoBPz0?si=qYeKUsHdgapA8euK">Claude Code - 47 PRO TIPS in 9 minutes</a></li><li><a href="https://blog.nilenso.com/blog/2025/05/29/ai-assisted-coding/">AI-assisted coding for teams that can't get away with vibes</a></li><li><a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview">Prompt engineering overview</a></li></ul>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/multiple-flutter-versions-puro/</guid><title>Use Multiple Flutter Versions with Puro</title><description>Puro is a powerful tool that lets you install multiple versions of Flutter and easily switch between them in your projects.</description><link>https://codewithandrea.com/tips/multiple-flutter-versions-puro/</link><pubDate>Mon, 9 Jun 2025 03:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Puro is a powerful tool that lets you install multiple versions of Flutter and easily switch between them in your projects.</p><p>It's very fast and can automatically configure your IDE to use your desired Flutter version.</p><figure><picture><source srcset="images/254.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Use multiple Flutter versions with Puro" srcset="images/254.png 2x"/></picture></figure><p>Before Puro, I was using <a href="https://pub.dev/packages/fvm">FVM</a>.</p><p>But after trying it, I have converted:</p><ul><li>Faster downloads</li><li>Better IDE integration</li><li>Nicer CLI interface</li></ul><p>Learn more and install here:</p><ul><li><a href="https://puro.dev/">Puro</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/videos/code-review-cashew-app/</guid><title>Code Review of Cashew App: Lessons from a Flutter App with 100k+ Downloads</title><description>An in-depth code review of Cashew, a budget and expense tracking app with 4.9 star ratings, 100,000+ downloads, and just as many lines of code.</description><link>https://codewithandrea.com/videos/code-review-cashew-app/</link><pubDate>Mon, 9 Jun 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>This video provides a comprehensive analysis of the <a href="https://cashewapp.web.app/">Cashew budgeting app</a>, an <a href="https://github.com/jameskokoska/Cashew">open-source</a> Flutter application with over 100,000 downloads and exceptional user reviews. While the app demonstrates remarkable business success, the codebase reveals critical lessons about technical debt and software engineering practices.</p><blockquote><p>Below is an AI summary of the video. Here's the <a href="https://github.com/jameskokoska/Cashew">GitHub repository</a>.</p></blockquote><h2><a id="app-overview-and-success-metrics" href="#app-overview-and-success-metrics">App Overview and Success Metrics</a></h2><p>Cashew stands as a testament to what a single developer can achieve. The finance budgeting app boasts:</p><ul><li>Over 100,000 downloads on Google Play</li><li>4.9-star rating from 15,700+ reviews</li><li>Similar high ratings on the App Store</li><li>Featured multiple times on YouTube channels with substantial followings</li><li>Cross-platform availability (iOS, Android, Web)</li></ul><p>The app was actively developed from September 2021 to October 2024, spanning over three years of continuous development—far exceeding the typical lifespan of side projects. However, the last release occurred in July 2024, suggesting potential development challenges.</p><h2><a id="getting-the-app-running-version-compatibility-challenges" href="#getting-the-app-running-version-compatibility-challenges">Getting the App Running: Version Compatibility Challenges</a></h2><p>The first hurdle in reviewing this codebase was simply getting it to run. The project's extensive dependencies created immediate compatibility issues with current Flutter versions. Rather than upgrading dozens of packages individually, the solution involved downgrading to Flutter 3.22 using tools like Puro for version management.</p><p>This approach highlights a critical lesson: when inheriting legacy codebases, sometimes working backward to a compatible environment is more efficient than forcing everything forward. The experience underscores the importance of version management tools for developers working across multiple projects.</p><p>Android setup required manual Gradle script updates, while iOS proved more straightforward after removing the old Podfile.lock and running pod install. Interestingly, the repository includes Firebase configuration files—a security concern for open-source projects that should be addressed through proper backend security measures.</p><h2><a id="architecture-and-dependencies-analysis" href="#architecture-and-dependencies-analysis">Architecture and Dependencies Analysis</a></h2><h3><a id="package-dependencies" href="#package-dependencies">Package Dependencies</a></h3><p>The pubspec.yaml reveals a complex dependency structure:</p><ul><li><strong>Database</strong>: Drift with SQLite for local storage</li><li><strong>Authentication</strong>: Google Sign-In integration</li><li><strong>Backend</strong>: Firebase packages for cloud synchronization</li><li><strong>State Management</strong>: Provider (notably absent: dedicated solutions like BLoC or Riverpod)</li><li><strong>Development</strong>: Build Runner for code generation</li></ul><p>The absence of dedicated state management frameworks suggests custom-built solutions, which can work effectively when implemented properly but require careful architectural planning.</p><h3><a id="codebase-scale" href="#codebase-scale">Codebase Scale</a></h3><p>Using the <code>cloc</code> tool reveals staggering numbers:</p><ul><li><strong>103,000 lines of Dart code</strong> (excluding empty lines and generated files)</li><li>Largest files include database tables and schema versions</li><li>Individual pages reaching nearly 5,000 lines of code</li><li>The <code>addTransactionsPage.dart</code> file alone contains massive complexity</li></ul><h3><a id="testing-infrastructure" href="#testing-infrastructure">Testing Infrastructure</a></h3><p>Perhaps the most concerning discovery: the project contains no meaningful tests beyond the default Flutter project template. For a production application of this scale serving thousands of users, the absence of automated testing represents significant risk. Any code changes could potentially break existing functionality without detection.</p><h2><a id="code-architecture-deep-dive" href="#code-architecture-deep-dive">Code Architecture Deep Dive</a></h2><h3><a id="global-state-management-issues" href="#global-state-management-issues">Global State Management Issues</a></h3><p>The application relies heavily on global variables for dependency management, declared in files like <code>databaseGlobal.dart</code>. Dependencies such as SharedPreferences, database instances, and notification payloads are initialized as global variables rather than through proper dependency injection systems.</p><p>This approach creates several problems:</p><ul><li><strong>Unclear initialization order</strong>: Functions like <code>loadCurrencyJSON</code> set global variables without explicit return values</li><li><strong>Mutable global state</strong>: Variables can be modified from anywhere in the application</li><li><strong>Testing difficulties</strong>: Global state makes unit testing nearly impossible</li><li><strong>Maintenance challenges</strong>: Dependencies between components become opaque</li></ul><h3><a id="navigation-and-state-updates" href="#navigation-and-state-updates">Navigation and State Updates</a></h3><p>The app employs a complex system of global navigation keys defined in <code>navigationFramework.dart</code>. These keys enable widgets to force rebuilds of other widgets throughout the application using patterns like:</p><pre><code><div class="highlight"><span></span><span class="n">homepageStateKey</span><span class="p">.</span><span class="n">currentState</span><span class="p">.</span><span class="n">refreshState</span><span class="p">();</span>
</div></code></pre><p>This approach creates problematic interdependencies where widgets can trigger rebuilds of seemingly unrelated components. The <code>refreshState</code> method typically contains empty <code>setState()</code> calls that force complete widget rebuilds—a brute-force approach to UI updates that can cause performance issues.</p><h3><a id="main-application-structure" href="#main-application-structure">Main Application Structure</a></h3><p>The <code>main.dart</code> file demonstrates inconsistent initialization patterns:</p><ul><li>Some dependencies initialized as global variables in the main method</li><li>Others handled through inherited widgets at the widget tree's root</li><li>Error handling relies on <code>runZonedGuarded</code> primarily for development logging</li><li>No integration with production error monitoring services like Crashlytics or Sentry</li></ul><h2><a id="ui-implementation-analysis" href="#ui-implementation-analysis">UI Implementation Analysis</a></h2><h3><a id="homepage-complexity" href="#homepage-complexity">Homepage Complexity</a></h3><p>The homepage widget exemplifies the architectural challenges throughout the codebase:</p><ul><li>Build method spans nearly 200 lines</li><li>Complex conditional logic based on global app settings</li><li>Dynamic widget mapping using string keys</li><li>For loops within the build method creating widgets based on context</li></ul><p>This monolithic approach means any state change potentially rebuilds the entire homepage, creating performance bottlenecks and making the code difficult to reason about.</p><h3><a id="the-5000-line-monster-addtransactionspage" href="#the-5000-line-monster-addtransactionspage">The 5,000-Line Monster: AddTransactionsPage</a></h3><p>The <code>addTransactionsPage.dart</code> file represents the extreme end of the complexity spectrum:</p><ul><li>Over 5,000 lines in a single file</li><li>44 <code>setState()</code> calls throughout the file</li><li>Massive build method spanning from line 1,050 to 2,218</li><li>Business logic, UI code, database operations, and animations all intertwined</li></ul><p>The <code>addTransactionLocked</code> method demonstrates the architectural problems:</p><ol><li>Opens bottom sheets for user input</li><li>Performs data validation</li><li>Creates transaction objects</li><li>Handles conditional logic for related transactions</li><li>Updates the database</li><li>Manages animations</li><li>Sets notifications</li><li>Updates global app settings</li><li>Handles error cases</li></ol><p>This single method mixing UI, business logic, database operations, and side effects makes testing and maintenance extremely challenging.</p><h2><a id="database-implementation" href="#database-implementation">Database Implementation</a></h2><h3><a id="schema-complexity" href="#schema-complexity">Schema Complexity</a></h3><p>The database implementation in <code>tables.dart</code> spans over 7,000 lines, containing:</p><ul><li>Table definitions with extensive column specifications</li><li>Complex converter classes for data transformation</li><li>Migration strategies across multiple app versions</li><li>Intricate SQL queries for financial calculations</li></ul><h3><a id="migration-strategy-evolution" href="#migration-strategy-evolution">Migration Strategy Evolution</a></h3><p>The migration code reveals evolving development practices:</p><ul><li>Early migrations: Simple await calls without error handling</li><li>Later migrations: Try-catch blocks with print statements for debugging</li><li>Complex multi-step migrations for schema changes</li><li>Inconsistent error handling patterns</li></ul><h3><a id="query-complexity" href="#query-complexity">Query Complexity</a></h3><p>Database queries demonstrate sophisticated financial calculations but raise maintainability concerns:</p><ul><li>Methods like <code>watchTotalNetBeforeStartsDateTransactionCategoryWithDay</code> perform complex aggregations</li><li>Some queries depend on global app settings variables</li><li>Commented-out code suggests ongoing experimentation</li><li>Query optimization appears secondary to functionality</li></ul><h2><a id="folder-structure-and-organization" href="#folder-structure-and-organization">Folder Structure and Organization</a></h2><p>The project's organization reflects organic growth rather than planned architecture:</p><ul><li><strong>struct folder</strong>: Contains miscellaneous utilities (data formatting, animations, Firebase auth, biometrics)</li><li><strong>widgets/util folder</strong>: Another catch-all containing app links, debouncing, animations, mixins, file I/O</li><li>Inconsistent naming conventions</li><li>No clear separation between layers (presentation, domain, data)</li></ul><p>This structure makes navigation difficult and suggests code was placed wherever convenient rather than following established architectural patterns.</p><h2><a id="performance-and-maintainability-concerns" href="#performance-and-maintainability-concerns">Performance and Maintainability Concerns</a></h2><h3><a id="state-management-issues" href="#state-management-issues">State Management Issues</a></h3><p>The combination of global mutable state, massive widget classes, and forced rebuilds creates several performance risks:</p><ul><li>Unnecessary widget rebuilds due to global state changes</li><li>Complex interdependencies making optimization difficult</li><li>Memory leaks potential from global state retention</li><li>Difficult debugging due to unclear state flow</li></ul><h3><a id="code-maintainability" href="#code-maintainability">Code Maintainability</a></h3><p>Several factors compound maintenance difficulties:</p><ul><li>Massive files requiring extensive scrolling and mental mapping</li><li>Mixed concerns within single classes</li><li>Global dependencies making isolated testing impossible</li><li>Inconsistent architectural patterns throughout the codebase</li></ul><h2><a id="learning-opportunities-and-best-practices" href="#learning-opportunities-and-best-practices">Learning Opportunities and Best Practices</a></h2><h3><a id="what-went-right" href="#what-went-right">What Went Right</a></h3><p>Despite architectural challenges, the Cashew app demonstrates several positive aspects:</p><ul><li><strong>Successful product delivery</strong>: The app serves real users effectively</li><li><strong>Feature completeness</strong>: Comprehensive budgeting functionality</li><li><strong>User satisfaction</strong>: Exceptional ratings indicate strong user experience</li><li><strong>Open source contribution</strong>: Provides learning opportunities for the community</li><li><strong>Solo developer achievement</strong>: Remarkable accomplishment for a single person</li></ul><h3><a id="critical-issues-identified" href="#critical-issues-identified">Critical Issues Identified</a></h3><p><strong>1. No error monitoring</strong></p><ul><li><strong>Problem</strong>: Errors are silenced or only logged to console</li><li><strong>Solution</strong>: Implement Crashlytics or Sentry for production error tracking</li></ul><p><strong>2. No automated tests</strong></p><ul><li><strong>Problem</strong>: No automated tests for a 103,000-line codebase</li><li><strong>Solution</strong>: Start with integration tests for critical user flows before major refactoring</li></ul><p><strong>3. No linter tool</strong></p><ul><li><strong>Problem</strong>: No linting or static analysis</li><li><strong>Solution</strong>: Implement Flutter lints package, consider DCM (Dart Code Metrics) for advanced analysis</li></ul><p><strong>4. Poor separation of concerns</strong></p><ul><li><strong>Problem</strong>: Business logic, UI, and data access mixed throughout</li><li><strong>Solution</strong>: Implement layered architecture (presentation, domain, data layers)</li></ul><p><strong>5. Lack of dependency injection system / service locator</strong></p><ul><li><strong>Problem</strong>: Global variables for dependency injection</li><li><strong>Solution</strong>: Adopt proper DI system (Riverpod, GetIt) for dependency management</li></ul><p><strong>6. Global mutable state</strong></p><ul><li><strong>Problem</strong>: App settings and other state accessible/modifiable globally</li><li><strong>Solution</strong>: Implement proper state management (Riverpod, BLoC) with immutable state</li></ul><p><strong>7. Widget rebuilds via global navigator keys</strong></p><ul><li><strong>Problem</strong>: Widgets forcing other widgets to rebuild by directly accessing their navigator keys and resetting their state</li><li><strong>Solution</strong>: Move to reactive state management solutions for better control of widget rebuilds</li></ul><p><strong>8. Massive widget classes</strong></p><ul><li><strong>Problem</strong>: Massive widget classes with complex interdependencies</li><li><strong>Solution</strong>: Break down into smaller, focused widgets with clear responsibilities</li></ul><h3><a id="refactoring-strategy" href="#refactoring-strategy">Refactoring Strategy</a></h3><p>For a codebase of this scale, refactoring must be incremental:</p><ol><li><strong>Establish safety nets</strong>: Add integration tests for critical paths</li><li><strong>Implement error monitoring</strong>: Gain visibility into production issues</li><li><strong>Add code quality tools</strong>: Enable automated detection of issues</li><li><strong>Gradual architectural improvements</strong>: Apply Boy Scout Rule—leave code better than found</li><li><strong>Dependency injection migration</strong>: Slowly replace global variables with proper DI</li><li><strong>State management evolution</strong>: Introduce reactive state management incrementally</li><li><strong>Widget decomposition</strong>: Break large widgets into smaller, testable components</li></ol><h2><a id="technical-debt-impact" href="#technical-debt-impact">Technical Debt Impact</a></h2><p>The Cashew app illustrates how technical debt accumulates in successful projects:</p><ul><li><strong>Initial velocity</strong>: Quick development enabled rapid feature delivery</li><li><strong>Growing complexity</strong>: Each new feature became harder to implement</li><li><strong>Maintenance burden</strong>: Changes risk breaking existing functionality</li><li><strong>Developer fatigue</strong>: Complex codebase becomes overwhelming to maintain</li></ul><p>The lack of recent releases (since July 2024) may indicate the technical debt has reached a tipping point where continued development becomes prohibitively difficult.</p><h2><a id="architectural-recommendations" href="#architectural-recommendations">Architectural Recommendations</a></h2><h3><a id="immediate-priorities" href="#immediate-priorities">Immediate Priorities</a></h3><ol><li><strong>Error monitoring implementation</strong>: Essential for production app stability</li><li><strong>Critical path testing</strong>: Integration tests for core user journeys</li><li><strong>Dependency injection</strong>: Replace global variables with proper DI container</li><li><strong>State management</strong>: Introduce reactive patterns to reduce forced rebuilds</li></ol><h3><a id="long-term-improvements" href="#long-term-improvements">Long-term Improvements</a></h3><ol><li><strong>Layered architecture</strong>: Separate presentation, domain, and data concerns</li><li><strong>Widget decomposition</strong>: Break monolithic widgets into manageable components</li><li><strong>Code organization</strong>: Restructure folders following architectural principles</li><li><strong>Performance optimization</strong>: Address unnecessary rebuilds and memory issues</li></ol><h3><a id="development-process-improvements" href="#development-process-improvements">Development Process Improvements</a></h3><ol><li><strong>Code review practices</strong>: Implement standards for future changes</li><li><strong>Automated testing</strong>: Build comprehensive test suite incrementally</li><li><strong>Continuous integration</strong>: Automate quality checks and testing</li><li><strong>Documentation</strong>: Create architectural decision records and coding standards</li></ol><h2><a id="lessons-for-flutter-developers" href="#lessons-for-flutter-developers">Lessons for Flutter Developers</a></h2><h3><a id="for-solo-developers" href="#for-solo-developers">For Solo Developers</a></h3><ul><li><strong>Balance speed with quality</strong>: Technical debt compounds quickly in successful projects</li><li><strong>Invest in testing early</strong>: Manual testing doesn't scale with codebase growth</li><li><strong>Establish architectural patterns</strong>: Consistency prevents complexity explosion</li><li><strong>Plan for success</strong>: Consider maintenance burden as the app grows</li></ul><h3><a id="for-teams" href="#for-teams">For Teams</a></h3><ul><li><strong>Code review importance</strong>: Multiple perspectives prevent architectural drift</li><li><strong>Shared standards</strong>: Team conventions prevent inconsistent patterns</li><li><strong>Refactoring time</strong>: Budget time for technical debt reduction</li><li><strong>Knowledge sharing</strong>: Prevent single points of failure in understanding</li></ul><h3><a id="universal-principles" href="#universal-principles">Universal Principles</a></h3><ul><li><strong>Separation of concerns</strong>: Keep UI, business logic, and data access distinct</li><li><strong>Dependency management</strong>: Avoid global state; use proper injection patterns</li><li><strong>Error handling</strong>: Plan for failure scenarios from the beginning</li><li><strong>Performance considerations</strong>: Design for scale, even in early stages</li></ul><h2><a id="conclusion" href="#conclusion">Conclusion</a></h2><p>The Cashew app represents a fascinating case study in Flutter development. While the business success is undeniable—serving over 100,000 users with exceptional satisfaction—the technical implementation reveals the challenges of scaling a codebase without proper architectural foundations.</p><p>The developer's achievement in creating a successful app single-handedly deserves recognition. However, the codebase serves as a cautionary tale about technical debt accumulation and the importance of sustainable development practices.</p><p>Key takeaways for the Flutter community:</p><ol><li><strong>Technical debt is real</strong>: It accumulates silently and can eventually halt development</li><li><strong>Architecture matters</strong>: Even solo projects benefit from proper structural planning</li><li><strong>Testing is essential</strong>: Manual testing doesn't scale with complexity</li><li><strong>Refactoring is investment</strong>: Regular code improvement prevents future paralysis</li><li><strong>Tools help</strong>: Linting, error monitoring, and quality metrics provide early warnings</li></ol><p>The open-source nature of Cashew provides invaluable learning opportunities. By examining both its successes and challenges, developers can make more informed decisions about their own projects, balancing delivery speed with long-term maintainability.</p><p>For developers facing similar technical debt situations, the path forward involves incremental improvement, establishing safety nets through testing, and gradually introducing better architectural patterns. The Boy Scout Rule—leaving code better than you found it—becomes essential for managing legacy codebases while continuing to deliver value to users.</p><p>The Cashew app ultimately demonstrates that while technical perfection isn't required for business success, sustainable development practices become crucial as projects scale and mature. The challenge lies in finding the right balance between shipping features and maintaining code quality—a balance that becomes increasingly important as applications grow in complexity and user base.</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/may-2025/</guid><title>May 2025: Flutter 3.32, Dart 3.8, Material 3 Expressive, Local-First Apps with PowerSync</title><description>Also included: How Flutter works (video series), the definitive guide to Navigator 2.0, and the latest from Code with Andrea.</description><link>https://codewithandrea.com/newsletter/may-2025/</link><pubDate>Wed, 21 May 2025 06:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Google I/O is taking place <a href="https://io.google/2025/">this week</a>, and while all the <a href="https://io.google/2025/explore/google-keynote-1">new AI stuff</a> is grabbing the headlines, three <a href="https://io.google/2025/explore?topics=Flutter">Flutter sessions</a> are also planned.</p><p>As usual, this newsletter is all about the latest from the Flutterverse:</p><ul><li>The new Flutter 3.32 and Dart 3.8 releases</li><li>Flutter’s path towards seamless interop</li><li>A peek at Material 3 Expressive</li><li>New videos and articles from the community</li><li>All the latest from Code with Andrea</li></ul><p>Let's dive in!</p><h3><a id="📝-whats-new-in-flutter-332" href="#📝-whats-new-in-flutter-332">📝 What's new in Flutter 3.32</a></h3><p>Flutter 3.32 is here, unlocking new features like web hot reload, Cupertino squircles for native fidelity, and new AI integrations with Firebase. The release addresses various improvements across web, framework, Cupertino, Material, accessibility, text input, desktop, iOS, Android, and engine components.</p><p>Here are some highlights:</p><ul><li><strong>Web Hot Reload (Experimental)</strong>: Now available with the <code>--web-experimental-hot-reload</code> flag, as <a href="https://codewithandrea.com/tips/hot-reload-flutter-web/">explained here</a>.</li><li><strong>Cupertino Squircles</strong>: Added rounded superellipse shape for iOS fidelity in <code>CupertinoAlertDialog</code> and <code>CupertinoActionSheet</code>.</li><li><strong>Progress on multi-window support</strong> on desktop by Canonical.</li><li><strong>Flutter Property Editor</strong>: New tool for easily editing widget properties and reading documentation within the IDE (<a href="https://bsky.app/profile/codewithandrea.com/post/3lpo76epbp22o">here's a preview</a>).</li><li><strong>Accessibility</strong>: Introduced a new <code>SemanticsRole</code> API for precise control over how UI elements are interpreted by assistive technologies.</li></ul><p>Read on for all the details:</p><ul><li><a href="https://medium.com/flutter/whats-new-in-flutter-3-32-40c1086bab6e">What's new in Flutter 3.32</a></li></ul><blockquote><p>Detailed release notes for Flutter 3.32 are available <a href="https://docs.flutter.dev/release/release-notes/release-notes-3.32.0">here</a>.</p></blockquote><h3><a id="📝-flutter’s-path-towards-seamless-interop" href="#📝-flutter’s-path-towards-seamless-interop">📝 Flutter’s path towards seamless interop</a></h3><p>Flutter is launching an <strong>early access program</strong> for plugin authors to test and provide feedback on FFIgen and JNIgen, codegen solutions that aim to simplify access to native platform APIs by directly bridging Dart and native code.</p><p>These tools seek to replace method channels, which are time-consuming to implement and maintain, with a more efficient and seamless developer experience. For all the details and how to apply, read on:</p><ul><li><a href="https://medium.com/flutter/flutters-path-towards-seamless-interop-4bf7d4579d9a">Flutter’s path towards seamless interop</a></li></ul><h3><a id="📝-announcing-dart-38" href="#📝-announcing-dart-38">📝 Announcing Dart 3.8</a></h3><p>Flutter 3.32 includes Dart 3.8, which brings some welcome improvements:</p><ul><li><a href="https://codewithandrea.com/articles/updated-formatter-dart-3-8/">Formatter updates</a>, including a new option to <a href="https://codewithandrea.com/tips/preserve-trailing-commas-dart-3.8">preserve trailing commas</a>.</li><li><a href="https://github.com/dart-lang/language/issues/323">Null-aware elements</a>, allowing you to add elements to a collection if they are not null, using a <a href="https://codewithandrea.com/tips/null-aware-elements-dart-3.8">simplified syntax</a>.</li></ul><p>Read the announcement for all the details:</p><ul><li><a href="https://medium.com/dartlang/announcing-dart-3-8-724eaaec9f47">Announcing Dart 3.8</a></li></ul><h3><a id="📹-decoding-flutter-how-flutter-works-video-series" href="#📹-decoding-flutter-how-flutter-works-video-series">📹 Decoding Flutter: How Flutter Works (Video Series)</a></h3><p>The Flutter team shared an excellent 6-part video series that takes you on a deep dive into Flutter's internals. From high-level architecture to low-level engine details, this comprehensive series explains how Flutter actually works under the hood.</p><p>The series starts with Flutter's overall architecture and gradually builds up your understanding through the widget system, state management, rendering pipeline, and platform integration. You'll learn about the three trees (Widget, Element, and RenderObject), how state management really works, what happens during layout and painting, and how Flutter interfaces with native platforms.</p><p>It's a great watch if you want to understand the framework at a deeper level:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="0Xn1QhNtPkQ"></div></div><p>Here are all the episodes:</p><ul><li><a href="https://youtu.be/0Xn1QhNtPkQ?si=OwpES_LeH5RmxXHb">Architecture #DecodingFlutter (1/6)</a></li><li><a href="https://youtu.be/xiW3ahr4CRU?si=rjpiCi6F_HUX8q_5">The Three Trees #DecodingFlutter (2/6)</a></li><li><a href="https://youtu.be/FP737UMx7ss?si=4anAvpwTbAsg_EY9">The State class #DecodingFlutter (3/6)</a></li><li><a href="https://youtu.be/zcJlHVVM84I?si=9DM9ulOSjK7g-M9E">The RenderObjectWidget #DecodingFlutter (4/6)</a></li><li><a href="https://youtu.be/EuG12bebwac?si=CeMtrxSomHWRMFyu">The RenderObject #DecodingFlutter (5/6)</a></li><li><a href="https://youtu.be/Y2aBMjWVv2Y?si=-1yesJbne0zNlkaC">The Flutter Engine and Embedders #DecodingFlutter (6/6)</a></li></ul><blockquote><p>The official docs also include a high-level overview of the architecture of Flutter, including the core principles and concepts that form its design. Learn more here: <a href="https://docs.flutter.dev/resources/architectural-overview">Flutter architectural overview</a>.</p></blockquote><h2><a id="upcoming-material-3-expressive" href="#upcoming-material-3-expressive">Upcoming: Material 3 Expressive</a></h2><p>Have you heard? <a href="https://m3.material.io/blog/building-with-m3-expressive">Material 3 Expressive</a> was announced this month, and it's expected to ship with the upcoming Android 16 later this year:</p><figure><picture><source srcset="images/material-3-expressive-preview.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Material 3 Expressive preview" srcset="images/material-3-expressive-preview.png 2x"/></picture><figcaption><center><i>Material 3 Expressive preview</i></center></figcaption></figure><p>According to this <a href="https://github.com/flutter/flutter/issues/168813">umbrella issue on the Flutter repo</a>, the team is <strong>not</strong> actively developing Material 3 Expressive at this stage, but I'm sure we'll hear more about it in the coming months, so keep an eye out.</p><h2><a id="community-articles" href="#community-articles">Community Articles</a></h2><p>This month, two useful articles that caught my attention. 👇</p><h3><a id="📝-building-local-first-flutter-apps-with-riverpod-drift-and-powersync" href="#📝-building-local-first-flutter-apps-with-riverpod-drift-and-powersync">📝 Building Local-First Flutter Apps with Riverpod, Drift, and PowerSync</a></h3><p>If you want to build offline-ready apps that stay fast and responsive, even without internet, you'll need to shift from the traditional <strong>online-first</strong> approach to a <strong>local-first</strong> model.</p><p>This article by Dinko Marinac offers a great overview of key challenges like change tracking, conflict resolution, and error handling, and shows how PowerSync solves them. A practical implementation is included, showing how to integrate Riverpod, Drift, and PowerSync in a sample TODO app:</p><ul><li><a href="https://dinkomarinac.dev/building-local-first-flutter-apps-with-riverpod-drift-and-powersync">Building Local-First Flutter Apps with Riverpod, Drift, and PowerSync</a></li></ul><h3><a id="📝-the-definitive-guide-to-navigator-20-in-flutter" href="#📝-the-definitive-guide-to-navigator-20-in-flutter">📝 The Definitive Guide to Navigator 2.0 in Flutter</a></h3><p>Flutter’s navigation system has had a bit of a troubled past:</p><ul><li>Navigator 1.0 is easy, but breaks down on the web and in complex apps.</li><li>Navigator 2.0 is powerful but under-documented and hard to reason about.</li><li>GoRouter tries to simplify it, but brings its own set of bugs and edge cases.</li></ul><p>Given GoRouter's recent troubles, I decided to use Navigator 2.0 directly when implementing <a href="https://www.linkedin.com/posts/andreabizzotto_for-a-long-time-i-wanted-to-enable-url-navigation-activity-7328384136052695042-dfDG/">URL-navigation in my Flutter Tips app</a>.</p><p>While I'm happy with the result, it was challenging to make it work, and this latest article by Tadas Petra has helped me get a better conceptual understanding of all the moving parts:</p><ul><li><a href="https://www.hungrimind.com/articles/the-definitive-guide-to-navigator-2">The Definitive Guide to Navigator 2.0 in Flutter</a></li></ul><h2><a id="latest-from-code-with-andrea" href="#latest-from-code-with-andrea">Latest from Code with Andrea</a></h2><p>Over the last month, I shared a <a href="https://codewithandrea.com/tips/">bunch of new tips</a> and published two new articles.</p><h3><a id="📝-how-to-update-your-android-gradle-files-to-the-kotlin-dsl" href="#📝-how-to-update-your-android-gradle-files-to-the-kotlin-dsl">📝 How to Update Your Android Gradle Files to the Kotlin DSL</a></h3><p>Since Flutter 3.29, new Flutter projects use the new Kotlin DSL for Gradle files by default. This has some implications for projects that rely on custom Gradle configurations, such as flavors, code signing, and more.</p><p>If you need help with migrating to the new Kotlin DSL, this article breaks down what changed, how it affects you, and how to avoid common pitfalls:</p><ul><li><a href="https://codewithandrea.com/articles/flutter-android-gradle-kts/">Kotlin DSL in Flutter 3.29: How to Update Your Android Gradle Files</a></li></ul><h3><a id="📝-flutter-app-analytics-scalable-architecture-&-firebase-setup" href="#📝-flutter-app-analytics-scalable-architecture-&-firebase-setup">📝 Flutter App Analytics: Scalable Architecture & Firebase Setup</a></h3><p>App analytics are very useful because they help you make product decisions based on data, rather than guesswork.</p><p>My latest article shows how to track analytics in your Flutter app, including:</p><ul><li>What to track: Choosing the right events.</li><li>How to structure it: Simple and scalable architectures for event tracking.</li><li>Firebase Analytics setup: How to wire everything up in a real app.</li></ul><p>Read on for all the details:</p><ul><li><a href="https://codewithandrea.com/articles/flutter-app-analytics/">Flutter App Analytics: Scalable Architecture &amp; Firebase Setup</a></li></ul><blockquote><p>I've also updated a previous article, explaining <a href="https://codewithandrea.com/articles/updated-formatter-dart-3-8/">how to configure the updated code formatter in Dart 3.8</a>.</p></blockquote><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>Lately, my brain has already been churning new content ideas, including a new video series where I'll be <a href="https://www.linkedin.com/posts/andreabizzotto_ive-been-digging-into-popular-open-source-activity-7329156735296835585-cLty/">reviewing popular OSS Flutter apps</a> to see how they're built.</p><p>I'll be on vacation over the next week, but I'll be recording the first episode as soon as I get back, so stay tuned!</p><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/preserve-trailing-commas-dart-3.8/</guid><title>Preserve Trailing Commas in Dart 3.8</title><description>Dart 3.8 introduces a new formatting option to preserve trailing commas, overriding the new formatting style introduced in Dart 3.7.</description><link>https://codewithandrea.com/tips/preserve-trailing-commas-dart-3.8/</link><pubDate>Wed, 21 May 2025 05:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Dart 3.8 introduces a new formatting option to preserve trailing commas.</p><p>Enable this to override the default formatter behaviour (introduced in Dart 3.7), which is to remove trailing commas when parameter lists fit within the max page width.</p><figure><picture><source srcset="images/253.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Preserve trailing commas in Dart 3.8" srcset="images/253.png 2x"/></picture></figure><p>Some other formatter configuration options are available.</p><p>For all the details, read this updated article:</p><ul><li><a href="https://codewithandrea.com/articles/updated-formatter-dart-3-8/">How to Configure the Updated Code Formatter in Dart 3.8</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/null-aware-elements-dart-3.8/</guid><title>Null-aware elements in Dart 3.8</title><description>Null-aware elements allow you to add nullable elements to a collection with a single character (?).</description><link>https://codewithandrea.com/tips/null-aware-elements-dart-3.8/</link><pubDate>Wed, 21 May 2025 03:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Dart 3.8 introduces null-aware elements!</p><p>This allows you to add <strong>nullable</strong> elements to a collection with a single character (?).</p><p>Much shorter than using the collection-if syntax. 👍</p><figure><picture><source srcset="images/252.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Null-aware elements in Dart 3.8" srcset="images/252.png 2x"/></picture></figure><p>Here are some more examples (from the docs) showing how you might use this:</p><figure><picture><img class="bottom-40px" alt="Null-aware elements (more examples)" srcset="images/252.2.png 2x"/></picture></figure><p>More details on the feature specification:</p><ul><li><a href="https://github.com/dart-lang/language/issues/323">Null-aware elements</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/go-router-delegate-listener/</guid><title>GoRouter Delegate Listener for Screen Tracking</title><description>If your app uses GoRouter with shell routes, tracking screen views reliably requires some workarounds. Here's how to make it work.</description><link>https://codewithandrea.com/tips/go-router-delegate-listener/</link><pubDate>Thu, 8 May 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If your app uses GoRouter with shell routes, tracking screen views <strong>reliably</strong> requires some workarounds.</p><p>Here's how I do it (using Riverpod):</p><ol><li>Declare GoRouter inside a Riverpod provider</li><li>Create a stateful GoRouter listener to track screen views</li><li>Return it inside <code>MaterialApp.builder</code></li></ol><figure><picture><source srcset="images/251.webp 2x" type="image/webp"/><img class="bottom-40px" alt="GoRouter Delegate Listener for Screen Tracking" srcset="images/251.png 2x"/></picture></figure><p>Here's a PR showing how I implemented this in my Time Tracker app:</p><ul><li><a href="https://github.com/bizz84/starter_architecture_flutter_firebase/pull/162/files">Add GoRouterDelegateListener class to track screen views</a></li></ul><p>And here's the original GoRouter issue showing that <code>NavigatorObserver</code> doesn't work with shell routes:</p><ul><li><a href="https://github.com/flutter/flutter/issues/112196">ShellRoutes seem to cause NavigatorObserver to not fire (5.0.1)</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/button-styles-material3/</guid><title>Button Styles in Material 3</title><description>Material 3 supports 5 types of buttons: elevated, filled, filled tonal, outlined, and text. Here's how to use them in Flutter.</description><link>https://codewithandrea.com/tips/button-styles-material3/</link><pubDate>Wed, 7 May 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Material 3 supports 5 types of buttons:</p><ul><li>Elevated</li><li>Filled</li><li>Filled tonal</li><li>Outlined</li><li>Text</li></ul><p>Here's how to use them in Flutter 👇</p><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Button Styles in Material 3" srcset="images/twitter-card.png 2x"/></picture></figure><p>Example with source code:</p><ul><li><a href="https://github.com/flutter/flutter/blob/master/examples/api/lib/material/button_style/button_style.0.dart">Flutter button style example</a></li></ul><p>More complex example with custom icons and LTR/RTL support:</p><ul><li><a href="https://github.com/flutter/flutter/blob/master/examples/api/lib/material/button_style_button/button_style_button.icon_alignment.0.dart">Flutter button style example with icons, LTR/RTL support</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/themedata-platform/</guid><title>Test your UI with ThemeData.platform</title><description>By overriding ThemeData.platform inside MaterialApp, you can quickly test all your adaptive widgets and any conditional code that checks the platform.</description><link>https://codewithandrea.com/tips/themedata-platform/</link><pubDate>Tue, 6 May 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>By overriding <code>ThemeData.platform</code> inside <code>MaterialApp</code>, you can quickly test all your adaptive UI code, such as:</p><ul><li>all adaptive widgets</li><li>any conditional code that checks the platform</li></ul><p>Much faster than choosing a different emulator/device and running from scratch 👍</p><figure><picture><source srcset="images/249.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Test your UI with ThemeData.platform" srcset="images/249.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/articles/flutter-app-analytics/</guid><title>Flutter App Analytics: Scalable Architecture &amp; Firebase Setup</title><description>A complete guide to adding analytics to Flutter apps using Firebase. Track custom events with a maintainable architecture.</description><link>https://codewithandrea.com/articles/flutter-app-analytics/</link><pubDate>Fri, 2 May 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Picture this: You've told your friends and family about your amazing new app (😄). Maybe you’ve even built a waitlist of eager users. You’re excited—and ready to hit “Publish.”</p><p>But wait…</p><p><strong>How will you know what’s working?</strong></p><ul><li>How many users complete onboarding?</li><li>How many sign up and create an account?</li><li>How many reach the paywall—and convert?</li></ul><p><strong>Without analytics, you’re flying blind.</strong></p><p>You’re making product decisions based on vibes, not data. That’s a fast way to stall growth, waste dev time, or worse—lose your users.</p><h2><a id="the-importance-of-analytics" href="#the-importance-of-analytics">The Importance of Analytics</a></h2><p>Analytics give you the data you need to grow. They help you:</p><ul><li><strong>Measure engagement</strong>: Who’s using your app, how often, and for how long?</li><li><strong>Understand retention</strong>: Where are users dropping off—and why?</li><li><strong>Track feature usage</strong>: What’s popular? What’s ignored?</li><li><strong>Optimize revenue</strong>: Monitor purchases, subscriptions, and churn.</li><li><strong>Know your users</strong>: Demographics, devices, platforms.</li><li><strong>Follow the journey</strong>: Map user flows across the app and spot friction points.</li></ul><p>With this info, you can stop guessing and start making product decisions that actually improve your app.</p><figure><picture><source srcset="images/mixpanel-example-dashboard.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Example Mixpanel dashboard showing primary user metrics for my Flutter Tips app" srcset="images/mixpanel-example-dashboard.png 2x"/></picture><figcaption><center><i>Example Mixpanel dashboard showing primary user metrics for my Flutter Tips app</i></center></figcaption></figure><h2><a id="what-youll-learn" href="#what-youll-learn">What You'll Learn</a></h2><p>In this article, I’ll show you how to track analytics in your Flutter app—from basic event logging to a scalable architecture that works with multiple providers like Firebase and Mixpanel.</p><p>Here’s what we’ll cover:</p><ul><li><strong>What to track</strong>: Choosing the right events—and why they matter.</li><li><strong>How to structure it</strong>: Simple and scalable architectures for event tracking.</li><li><strong>Firebase Analytics setup</strong>: How to wire everything up in a real app.</li></ul><p>Let’s dive in. 👇</p><blockquote><p>This article will guide you through the fundamentals. If you want to go deeper, check out my <a href="https://pro.codewithandrea.com/p/flutter-in-production">Flutter in Production course</a>, which contains an entire module about app analytics.</p></blockquote><h2><a id="introduction-to-event-tracking" href="#introduction-to-event-tracking">Introduction to Event Tracking</a></h2><p>Ask five developers how they implemented analytics, and you’ll likely get five different answers (I know because <a href="https://x.com/biz84/status/1816104313782993112">I did</a>).</p><p>As usual with software engineering, there’s no one-size-fits-all solution—just tradeoffs.</p><p>So before we dive into the code, let’s zoom out and answer a few key questions:</p><ul><li>What events should we track?</li><li>How should we track them?</li><li>What requirements should our solution meet?</li></ul><p>Once we’ve nailed that down, we’ll pick a suitable architecture.</p><h3><a id="what-events-do-we-need-to-track?" href="#what-events-do-we-need-to-track?">What Events Do We Need to Track?</a></h3><p>In simple terms, events represent <strong>interactions between the user and your app</strong>.</p><p>Take my <a href="https://bizz84.github.io/flutter_ship_app_web/">Flutter Ship app</a> as an example:</p><div class="spacer-20px"></div><div class="video-player"><iframe src="https://customer-cj9d17w2zbobjd7j.cloudflarestream.com/82fbe9bab710da621896b0e17afb2341/iframe" style="border: none; position: absolute; top: 0; height: 100%; width: 100%" allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;" allowfullscreen="true"></iframe></div><div class="spacer-12px"></div><figcaption><span>Flutter Ship App Demo</span></figcaption><p>The app helps you "tick all the boxes" before releasing your Flutter apps. It’s basically a pre-filled TODO list.</p><div class="spacer-20px"></div><div class="iframe-small-iphone">
<iframe class="iframe-responsive" src="https://bizz84.github.io/flutter_ship_app_web/" title="Flutter Ship App" width="700" height="500" frameborder="0"></iframe> 
</div><div class="spacer-12px"></div><figcaption>Flutter Ship App web demo. <a href="https://bizz84.github.io/flutter_ship_app_web/" target="_blank">Open in a separate window</a></figcaption><div class="spacer-20px"></div><p>If you wanted to add analytics to this app, what would you track?</p><p>Focus on events that:</p><ul><li><strong>Help the user succeed</strong> (e.g. completing tasks)</li><li><strong>Help your business succeed</strong> (e.g. user signups, retention, monetization)</li></ul><p>For this app, meaningful events are:</p><ol><li>Create a new app</li><li>Edit an existing app</li><li>Delete an app</li><li>Complete a task</li></ol><p>Everything else is optional. Don’t track for the sake of tracking.</p><hr><h3><a id="tracking-events-with-firebase-analytics-and-mixpanel" href="#tracking-events-with-firebase-analytics-and-mixpanel">Tracking Events with Firebase Analytics and Mixpanel</a></h3><p>Now, suppose you want to track these events in your Flutter app.</p><p>If you're using the <a href="https://pub.dev/packages/firebase_analytics"><code>firebase_analytics</code></a> package, you can do it like this:</p><pre><code><div class="highlight"><span></span><span class="kd">final</span><span class="w"> </span><span class="n">analytics</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">FirebaseAnalytics</span><span class="p">.</span><span class="n">instance</span><span class="p">;</span>
<span class="n">analytics</span><span class="p">.</span><span class="n">logEvent</span><span class="p">(</span><span class="s1">&#39;app_created&#39;</span><span class="p">);</span>
<span class="n">analytics</span><span class="p">.</span><span class="n">logEvent</span><span class="p">(</span><span class="s1">&#39;app_updated&#39;</span><span class="p">);</span>
<span class="n">analytics</span><span class="p">.</span><span class="n">logEvent</span><span class="p">(</span><span class="s1">&#39;app_deleted&#39;</span><span class="p">);</span>
<span class="n">analytics</span><span class="p">.</span><span class="n">logEvent</span><span class="p">(</span><span class="s1">&#39;task_completed&#39;</span><span class="p">,</span><span class="w"> </span><span class="nl">parameters:</span><span class="w"> </span><span class="p">{</span><span class="s1">&#39;count&#39;</span><span class="o">:</span><span class="w"> </span><span class="n">count</span><span class="p">});</span>
</div></code></pre><p>Or, if you're using the <a href="https://pub.dev/packages/mixpanel_flutter"><code>mixpanel_flutter</code></a> package, this is the way:</p><pre><code><div class="highlight"><span></span><span class="kd">final</span><span class="w"> </span><span class="n">mixpanel</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">await</span><span class="w"> </span><span class="n">Mixpanel</span><span class="p">.</span><span class="n">init</span><span class="p">(</span>
<span class="w">  </span><span class="n">Env</span><span class="p">.</span><span class="n">mixpanelProjectToken</span><span class="p">,</span>
<span class="w">  </span><span class="nl">trackAutomaticEvents:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="p">);</span>
<span class="n">mixpanel</span><span class="p">.</span><span class="n">track</span><span class="p">(</span><span class="s1">&#39;App Created&#39;</span><span class="p">);</span>
<span class="n">mixpanel</span><span class="p">.</span><span class="n">track</span><span class="p">(</span><span class="s1">&#39;App Updated&#39;</span><span class="p">);</span>
<span class="n">mixpanel</span><span class="p">.</span><span class="n">track</span><span class="p">(</span><span class="s1">&#39;App Deleted&#39;</span><span class="p">);</span>
<span class="n">mixpanel</span><span class="p">.</span><span class="n">track</span><span class="p">(</span><span class="s1">&#39;Task Completed&#39;</span><span class="p">,</span><span class="w"> </span><span class="nl">properties:</span><span class="w"> </span><span class="p">{</span><span class="s1">&#39;count&#39;</span><span class="o">:</span><span class="w"> </span><span class="n">count</span><span class="p">});</span>
</div></code></pre><p>You can already spot the problem: <strong>different APIs, different syntax.</strong></p><p>❌ Scattering these calls all over your codebase is a bad idea. ❌</p><p>Here's why:</p><ul><li><strong>You lock yourself into specific SDKs</strong></li><li><strong>You duplicate event names everywhere</strong> (easy to mistype, hard to refactor)</li><li><strong>You lose type safety and IDE support</strong></li><li><strong>You can’t reuse logic or dispatch to multiple vendors</strong></li><li><strong>You mix business logic with implementation details</strong></li></ul><p>We can do much better by defining a clear interface—one that separates event tracking from everything else.</p><p>Let’s formalize some requirements next.</p><h2><a id="app-analytics-requirements" href="#app-analytics-requirements">App Analytics: Requirements</a></h2><p>Before jumping into implementation, let’s define what we actually need from an analytics system.</p><p>Here are the key requirements:</p><ol><li><strong>Separation of concerns</strong>: App code should never call vendor SDKs (like Firebase or Mixpanel) directly. Instead, we’ll route all event tracking through a clear interface.</li></ol><ol start="2"><li><strong>Support for multiple clients</strong>: We should be able to send events to more than one provider. For example, you might want to log to both Firebase and Mixpanel at the same time.</li></ol><ol start="3"><li><strong>Debug vs Release mode</strong>: In development, we only want to log events to the console. In release mode, real analytics providers should be used—with no changes to app logic.</li></ol><p>Other things worth thinking about:</p><ul><li><strong>Screen view tracking</strong></li><li><strong>Opt-in/out toggles</strong> (e.g. for privacy/GDPR)</li><li><strong>User identification</strong> (after login)</li><li><strong>Multiple build flavors</strong> (dev/staging/prod)</li></ul><p>But for now, we’ll focus on the two most important ones:</p><ul><li>Keeping analytics isolated from app logic</li><li>Supporting multiple analytics providers</li></ul><p>Next, let's walk through two possible architectures and break down their tradeoffs. 👇</p><h2><a id="super-simple-analytics-architecture" href="#super-simple-analytics-architecture">Super-Simple Analytics Architecture</a></h2><p>This setup is adapted from a suggestion I received <a href="https://x.com/luke_pighetti/status/1816217314255396988">on X (Twitter)</a>:</p><pre><code><div class="highlight"><span></span><span class="k">import</span><span class="w"> </span><span class="s1">&#39;dart:developer&#39;</span><span class="p">;</span>

<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter/foundation.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:mixpanel_flutter/mixpanel_flutter.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:firebase_analytics/firebase_analytics.dart&#39;</span><span class="p">;</span>

<span class="kd">class</span><span class="w"> </span><span class="nc">AnalyticsClient</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">const</span><span class="w"> </span><span class="n">AnalyticsClient</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">_analytics</span><span class="p">,</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="n">_mixpanel</span><span class="p">);</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="n">FirebaseAnalytics</span><span class="w"> </span><span class="n">_analytics</span><span class="p">;</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="n">Mixpanel</span><span class="w"> </span><span class="n">_mixpanel</span><span class="p">;</span>

<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">track</span><span class="p">(</span>
<span class="w">    </span><span class="kt">String</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">Map</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span><span class="w"> </span><span class="kt">dynamic</span><span class="o">&gt;</span><span class="w"> </span><span class="n">params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="p">{},</span>
<span class="w">  </span><span class="p">})</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">kReleaseMode</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="kd">await</span><span class="w"> </span><span class="n">_analytics</span><span class="p">.</span><span class="n">logEvent</span><span class="p">(</span><span class="nl">name:</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="nl">parameters:</span><span class="w"> </span><span class="n">params</span><span class="p">);</span>
<span class="w">      </span><span class="kd">await</span><span class="w"> </span><span class="n">_mixpanel</span><span class="p">.</span><span class="n">track</span><span class="p">(</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="nl">properties:</span><span class="w"> </span><span class="n">params</span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="n">log</span><span class="p">(</span><span class="s1">&#39;</span><span class="si">$</span><span class="n">name</span><span class="s1"> </span><span class="si">$</span><span class="n">params</span><span class="s1">&#39;</span><span class="p">,</span><span class="w"> </span><span class="nl">name:</span><span class="w"> </span><span class="s1">&#39;Event&#39;</span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</div></code></pre><p>This is about as simple as it gets. It gives you a shared interface for tracking events:</p><ul><li>In <strong>release mode</strong>, it logs to both Firebase and Mixpanel</li><li>In <strong>debug mode</strong>, it just prints to the console</li></ul><p>If you're using Riverpod, tracking events looks like this:</p><pre><code><div class="highlight"><span></span><span class="kd">final</span><span class="w"> </span><span class="n">analytics</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">analyticsClientProvider</span><span class="p">);</span>
<span class="n">analytics</span><span class="p">.</span><span class="n">track</span><span class="p">(</span><span class="s1">&#39;app_created&#39;</span><span class="p">);</span>
<span class="n">analytics</span><span class="p">.</span><span class="n">track</span><span class="p">(</span><span class="s1">&#39;app_updated&#39;</span><span class="p">);</span>
<span class="n">analytics</span><span class="p">.</span><span class="n">track</span><span class="p">(</span><span class="s1">&#39;app_deleted&#39;</span><span class="p">);</span>
<span class="n">analytics</span><span class="p">.</span><span class="n">track</span><span class="p">(</span><span class="s1">&#39;task_completed&#39;</span><span class="p">,</span><span class="w"> </span><span class="nl">parameters:</span><span class="w"> </span><span class="p">{</span><span class="s1">&#39;count&#39;</span><span class="o">:</span><span class="w"> </span><span class="n">count</span><span class="p">});</span>
</div></code></pre><p>As long as your event names follow the <a href="https://firebase.google.com/docs/reference/android/com/google/firebase/analytics/FirebaseAnalytics.Event">Firebase Analytics guidelines</a> (1–40 alphanumeric characters or underscores), you’re good to go.</p><p>This approach is surprisingly flexible for how little code it requires. You can swap out analytics providers by updating a single class, and the rest of your app doesn’t care. 👍</p><h3><a id="but-it-has-drawbacks" href="#but-it-has-drawbacks">But It Has Drawbacks</a></h3><p>This architecture starts to break down as your app grows:</p><ul><li><strong>No conditional tracking</strong>: You can’t log some events to Mixpanel but not Firebase. This is a problem if you're watching your event volume (and costs).<ul></ul></li></ul><ul><li><strong>Hardcoded strings everywhere</strong>: Event names end up duplicated across the codebase. Easy to mistype. Hard to refactor. A big footgun on teams.<ul></ul></li></ul><ul><li><strong>No single source of truth</strong>:There’s no centralized list of events or their expected parameters.<ul></ul></li></ul><ul><li><strong>No autocomplete</strong>: You lose IDE support. If you had a type-safe API, you'd get helpful autocompletion:<ul></ul></li></ul><figure><picture><source srcset="images/analytics-auto-completion.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Analytics methods auto-completion in VSCode" srcset="images/analytics-auto-completion.png 2x"/></picture><figcaption><center><i>Analytics methods auto-completion in VSCode</i></center></figcaption></figure><p>This setup may be <strong>good enough</strong> for solo devs or small apps where you want minimal friction and full control.</p><p>But if you’re working in a larger codebase or with a team, it’s worth investing in something a bit more robust. Let’s look at a more scalable alternative. 👇</p><h2><a id="more-complex-analytics-architecture" href="#more-complex-analytics-architecture">More Complex Analytics Architecture</a></h2><p>If you want a <strong>type-safe</strong> analytics API with autocomplete and clear separation of concerns, here’s the approach I recommend.</p><p>Start by defining an abstract <code>AnalyticsClient</code> interface. For the <a href="https://bizz84.github.io/flutter_ship_app_web/">Flutter Ship app</a>, it might look like this:</p><pre><code><div class="highlight"><span></span><span class="kd">abstract</span><span class="w"> </span><span class="kd">class</span><span class="w"> </span><span class="nc">AnalyticsClient</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppCreated</span><span class="p">();</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppUpdated</span><span class="p">();</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppDeleted</span><span class="p">();</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackTaskCompleted</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">completedCount</span><span class="p">);</span>
<span class="p">}</span>
</div></code></pre><p>This way, all the events are defined in a single place, avoiding duplication and potential mistakes.</p><h3><a id="but-how-do-we-support-multiple-clients?" href="#but-how-do-we-support-multiple-clients?">But how do we support multiple clients?</a></h3><p>Here’s one way to structure it:</p><figure><picture><source srcset="images/diagram-app-analytics-architecture.webp 2x" type="image/webp"/><img class="bottom-12px" alt="App Analytics Architecture" srcset="images/diagram-app-analytics-architecture.png 2x"/></picture><figcaption><center><i>App Analytics Architecture</i></center></figcaption></figure><p>How this works:</p><ul><li>You define all your events in an abstract <code>AnalyticsClient</code> interface.</li><li>Each concrete implementation (e.g. <code>LoggerAnalyticsClient</code>, <code>FirebaseAnalyticsClient</code>, etc.) handles how those events are logged.</li><li>An <code>AnalyticsFacade</code> class implements <code>AnalyticsClient</code> too—but just dispatches to all registered clients.</li><li>Finally, the app uses a <code>analyticsFacadeProvider</code> to access the facade and call the appropriate tracking methods.</li></ul><p>Now, from your app code, tracking an event is as simple as:</p><pre><code><div class="highlight"><span></span><span class="kd">final</span><span class="w"> </span><span class="n">analytics</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">analyticsFacadeProvider</span><span class="p">);</span>
<span class="n">analytics</span><span class="p">.</span><span class="n">trackEditApp</span><span class="p">();</span>
</div></code></pre><p>And since each event is a dedicated method, you get autocomplete and type safety out of the box:</p><figure><picture><source srcset="images/analytics-auto-completion.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Analytics methods auto-completion in VSCode" srcset="images/analytics-auto-completion.png 2x"/></picture><figcaption><center><i>Analytics methods auto-completion in VSCode</i></center></figcaption></figure><h3><a id="how-do-we-implement-this?" href="#how-do-we-implement-this?">How Do We Implement This?</a></h3><p>This is just the high-level architecture. To make it work, we’ll need to answer:</p><ul><li>How do we register multiple clients in <code>AnalyticsFacade</code>?</li><li>How are events dispatched to each client?</li><li>What do concrete <code>AnalyticsClient</code> subclasses look like?</li></ul><p>Let’s walk through the implementation details next. 👇</p><h2><a id="app-analytics-implementation-details" href="#app-analytics-implementation-details">App Analytics: Implementation Details</a></h2><p>To implement the architecture we just discussed, we’ll need a few key building blocks:</p><ol><li>An <code>AnalyticsClient</code> interface that defines all the events we want to track.</li><li>An <code>AnalyticsFacade</code> that implements <code>AnalyticsClient</code>, and delegates calls to multiple clients.</li><li>A <code>LoggerAnalyticsClient</code> for local dev—logs events to the console.</li><li>Additional clients like <code>FirebaseAnalyticsClient</code>, <code>MixpanelAnalyticsClient</code>, etc., that wrap vendor SDKs.</li></ol><p>To keep things organized, I recommend putting all of this in a <code>monitoring</code> folder:</p><pre><code><div class="highlight"><span></span>‣ lib
  ‣ src
    ‣ monitoring
      ‣ analytics_client.dart
      ‣ analytics_facade.dart
      ‣ logger_analytics_client.dart
      ‣ firebase_analytics_client.dart
</div></code></pre><p>Here’s how each part works:</p><h3><a id="1-the analyticsclient cnterface" href="#1-the analyticsclient cnterface">1. The AnalyticsClient Cnterface</a></h3><p>This is the contract. It defines all the events your app supports.</p><pre><code><div class="highlight"><span></span><span class="kd">abstract</span><span class="w"> </span><span class="kd">class</span><span class="w"> </span><span class="nc">AnalyticsClient</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="c1">// Custom events for the Flutter Ship app.</span>
<span class="w">  </span><span class="c1">// TODO: Replace with your own events.</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackNewAppOnboarding</span><span class="p">();</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackNewAppHome</span><span class="p">();</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppCreated</span><span class="p">();</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppUpdated</span><span class="p">();</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppDeleted</span><span class="p">();</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackTaskCompleted</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">completedCount</span><span class="p">);</span>
<span class="p">}</span>
</div></code></pre><blockquote><p>Since this class is abstract, all methods are declarations only—no implementations.</p></blockquote><h3><a id="2-the loggeranalyticsclient class" href="#2-the loggeranalyticsclient class">2. The LoggerAnalyticsClient Class</a></h3><p>This one’s simple: it logs events to the console. Useful for local dev and tests before wiring up a real analytics backend:</p><pre><code><div class="highlight"><span></span><span class="k">import</span><span class="w"> </span><span class="s1">&#39;dart:async&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;dart:developer&#39;</span><span class="p">;</span>

<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/src/monitoring/analytics_client.dart&#39;</span><span class="p">;</span>

<span class="kd">class</span><span class="w"> </span><span class="nc">LoggerAnalyticsClient</span><span class="w"> </span><span class="kd">implements</span><span class="w"> </span><span class="n">AnalyticsClient</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">const</span><span class="w"> </span><span class="n">LoggerAnalyticsClient</span><span class="p">();</span>

<span class="w">  </span><span class="kd">static</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="n">_name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Event&#39;</span><span class="p">;</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackNewAppHome</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">log</span><span class="p">(</span><span class="s1">&#39;trackNewAppHome&#39;</span><span class="p">,</span><span class="w"> </span><span class="nl">name:</span><span class="w"> </span><span class="n">_name</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackNewAppOnboarding</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">log</span><span class="p">(</span><span class="s1">&#39;trackNewAppOnboarding&#39;</span><span class="p">,</span><span class="w"> </span><span class="nl">name:</span><span class="w"> </span><span class="n">_name</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppCreated</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">log</span><span class="p">(</span><span class="s1">&#39;trackAppCreated&#39;</span><span class="p">,</span><span class="w"> </span><span class="nl">name:</span><span class="w"> </span><span class="n">_name</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppUpdated</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">log</span><span class="p">(</span><span class="s1">&#39;trackAppUpdated&#39;</span><span class="p">,</span><span class="w"> </span><span class="nl">name:</span><span class="w"> </span><span class="n">_name</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppDeleted</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">log</span><span class="p">(</span><span class="s1">&#39;trackAppDeleted&#39;</span><span class="p">,</span><span class="w"> </span><span class="nl">name:</span><span class="w"> </span><span class="n">_name</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackTaskCompleted</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">completedCount</span><span class="p">)</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">log</span><span class="p">(</span><span class="s1">&#39;trackTaskCompleted(completedCount: </span><span class="si">$</span><span class="n">completedCount</span><span class="s1">)&#39;</span><span class="p">,</span><span class="w"> </span><span class="nl">name:</span><span class="w"> </span><span class="n">_name</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</div></code></pre><blockquote><p>We’ll add a <code>FirebaseAnalyticsClient</code> later. For now, focus on what you want to track—not how it’s sent.</p></blockquote><h3><a id="is-there-a-better-way-to-define-events?" href="#is-there-a-better-way-to-define-events?">Is There a Better Way to Define Events?</a></h3><p>This approach works—but it’s not DRY. You’re copy-pasting a lot when adding new events:</p><pre><code><div class="highlight"><span></span><span class="c1">// copy-paste when creating new events</span>
<span class="nd">@override</span>
<span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppCreated</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="n">log</span><span class="p">(</span><span class="s1">&#39;trackAppCreated&#39;</span><span class="p">,</span><span class="w"> </span><span class="nl">name:</span><span class="w"> </span><span class="n">_name</span><span class="p">);</span>
<span class="p">}</span>
</div></code></pre><p>Alternatives worth considering:</p><ul><li><strong>Enums</strong>: Define an <code>AppEvent</code> enum and list all the possible events as values. This doesn't work if different events need different arguments.</li><li><strong>Sealed classes</strong>: Use a base <code>AppEvent</code> sealed class and create a subclass every time we need a new event. More flexible, but verbose.</li><li><strong>Freezed union types</strong>: Terse syntax and pattern matching—but requires codegen.</li></ul><p>Each option has tradeoffs. For now, we'll stick with our initial approach.</p><h3><a id="3-the analyticsfacade class" href="#3-the analyticsfacade class">3. The AnalyticsFacade Class</a></h3><p>The <code>AnalyticsFacade</code> implements the <code>AnalyticsClient</code> interface and forwards each method call to all registered clients.</p><pre><code><div class="highlight"><span></span><span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter/foundation.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/src/monitoring/analytics_client.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/src/monitoring/logger_analytics_client.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:riverpod_annotation/riverpod_annotation.dart&#39;</span><span class="p">;</span>

<span class="k">part</span><span class="w"> </span><span class="s1">&#39;analytics_facade.g.dart&#39;</span><span class="p">;</span>

<span class="c1">// https://refactoring.guru/design-patterns/facade</span>
<span class="kd">class</span><span class="w"> </span><span class="nc">AnalyticsFacade</span><span class="w"> </span><span class="kd">implements</span><span class="w"> </span><span class="n">AnalyticsClient</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">const</span><span class="w"> </span><span class="n">AnalyticsFacade</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">clients</span><span class="p">);</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="n">List</span><span class="o">&lt;</span><span class="n">AnalyticsClient</span><span class="o">&gt;</span><span class="w"> </span><span class="n">clients</span><span class="p">;</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppOpened</span><span class="p">()</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">_dispatch</span><span class="p">(</span>
<span class="w">        </span><span class="p">(</span><span class="n">c</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">c</span><span class="p">.</span><span class="n">trackAppOpened</span><span class="p">(),</span>
<span class="w">      </span><span class="p">);</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackNewAppHome</span><span class="p">()</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">_dispatch</span><span class="p">(</span>
<span class="w">        </span><span class="p">(</span><span class="n">c</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">c</span><span class="p">.</span><span class="n">trackNewAppHome</span><span class="p">(),</span>
<span class="w">      </span><span class="p">);</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackNewAppOnboarding</span><span class="p">()</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">_dispatch</span><span class="p">(</span>
<span class="w">        </span><span class="p">(</span><span class="n">c</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">c</span><span class="p">.</span><span class="n">trackNewAppOnboarding</span><span class="p">(),</span>
<span class="w">      </span><span class="p">);</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppCreated</span><span class="p">()</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">_dispatch</span><span class="p">(</span>
<span class="w">        </span><span class="p">(</span><span class="n">c</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">c</span><span class="p">.</span><span class="n">trackAppCreated</span><span class="p">(),</span>
<span class="w">      </span><span class="p">);</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppUpdated</span><span class="p">()</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">_dispatch</span><span class="p">(</span>
<span class="w">        </span><span class="p">(</span><span class="n">c</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">c</span><span class="p">.</span><span class="n">trackAppUpdated</span><span class="p">(),</span>
<span class="w">      </span><span class="p">);</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppDeleted</span><span class="p">()</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">_dispatch</span><span class="p">(</span>
<span class="w">        </span><span class="p">(</span><span class="n">c</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">c</span><span class="p">.</span><span class="n">trackAppDeleted</span><span class="p">(),</span>
<span class="w">      </span><span class="p">);</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackTaskCompleted</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">completedCount</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">_dispatch</span><span class="p">(</span>
<span class="w">        </span><span class="p">(</span><span class="n">c</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">c</span><span class="p">.</span><span class="n">trackTaskCompleted</span><span class="p">(</span><span class="n">completedCount</span><span class="p">),</span>
<span class="w">      </span><span class="p">);</span>

<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">_dispatch</span><span class="p">(</span>
<span class="w">    </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="kt">Function</span><span class="p">(</span><span class="n">AnalyticsClient</span><span class="w"> </span><span class="n">client</span><span class="p">)</span><span class="w"> </span><span class="n">work</span><span class="p">,</span>
<span class="w">  </span><span class="p">)</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">final</span><span class="w"> </span><span class="n">client</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="n">clients</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="kd">await</span><span class="w"> </span><span class="n">work</span><span class="p">(</span><span class="n">client</span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</div></code></pre><blockquote><p>The <code>_dispatch</code> method removes duplication and broadcasts each event to all the registered clients.</p></blockquote><h3><a id="setting-up-the-provider" href="#setting-up-the-provider">Setting Up the Provider</a></h3><p>Here’s how to expose the <code>AnalyticsFacade</code> via Riverpod:</p><pre><code><div class="highlight"><span></span><span class="nd">@Riverpod</span><span class="p">(</span><span class="nl">keepAlive:</span><span class="w"> </span><span class="kc">true</span><span class="p">)</span>
<span class="n">AnalyticsFacade</span><span class="w"> </span><span class="n">analyticsFacade</span><span class="p">(</span><span class="n">Ref</span><span class="w"> </span><span class="n">ref</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="n">AnalyticsFacade</span><span class="p">([</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">kReleaseMode</span><span class="p">)</span><span class="w"> </span><span class="n">LoggerAnalyticsClient</span><span class="p">(),</span>
<span class="w">  </span><span class="p">]);</span>
<span class="p">}</span>
</div></code></pre><blockquote><p>In debug builds, you get console logging. In release builds, you can add real clients like <code>FirebaseAnalyticsClient</code> (more on this later).</p></blockquote><p>This setup gives you a clear, modular, and scalable way to track analytics events—without coupling your app logic to any vendor-specific SDK.</p><h2><a id="which-architecture-is-better?" href="#which-architecture-is-better?">Which Architecture Is Better?</a></h2><p>The architecture we just built gives you:</p><ul><li>A type-safe event tracking API</li><li>Autocomplete and IDE support</li><li>Clear separation of concerns</li><li>Support for multiple analytics backends</li></ul><p>Here’s a visual recap:</p><figure><picture><source srcset="images/diagram-app-analytics-architecture.webp 2x" type="image/webp"/><img class="bottom-12px" alt="App Analytics Architecture" srcset="images/diagram-app-analytics-architecture.png 2x"/></picture><figcaption><center><i>App Analytics Architecture</i></center></figcaption></figure><p>It’s flexible, scalable, and production-ready.</p><p>But it comes at a cost.</p><p>Every time you add a new event, you need to:</p><ul><li>Add a method to the <code>AnalyticsClient</code> interface</li><li>Implement it in the <code>AnalyticsFacade</code></li><li>Implement it in every concrete client (e.g. Logger, Firebase, Mixpanel)</li></ul><h3><a id="when-the-simple-approach-is-enough" href="#when-the-simple-approach-is-enough">When the Simple Approach Is Enough</a></h3><p>For small apps or quick MVPs, the simple approach may be enough:</p><pre><code><div class="highlight"><span></span><span class="k">import</span><span class="w"> </span><span class="s1">&#39;dart:developer&#39;</span><span class="p">;</span>

<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter/foundation.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:mixpanel_flutter/mixpanel_flutter.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:firebase_analytics/firebase_analytics.dart&#39;</span><span class="p">;</span>

<span class="kd">class</span><span class="w"> </span><span class="nc">AnalyticsClient</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">const</span><span class="w"> </span><span class="n">AnalyticsClient</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">_analytics</span><span class="p">,</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="n">_mixpanel</span><span class="p">);</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="n">FirebaseAnalytics</span><span class="w"> </span><span class="n">_analytics</span><span class="p">;</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="n">Mixpanel</span><span class="w"> </span><span class="n">_mixpanel</span><span class="p">;</span>

<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">track</span><span class="p">(</span>
<span class="w">    </span><span class="kt">String</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">Map</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span><span class="w"> </span><span class="kt">dynamic</span><span class="o">&gt;</span><span class="w"> </span><span class="n">params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="p">{},</span>
<span class="w">  </span><span class="p">})</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">kReleaseMode</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="kd">await</span><span class="w"> </span><span class="n">_analytics</span><span class="p">.</span><span class="n">logEvent</span><span class="p">(</span><span class="nl">name:</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="nl">parameters:</span><span class="w"> </span><span class="n">params</span><span class="p">);</span>
<span class="w">      </span><span class="kd">await</span><span class="w"> </span><span class="n">_mixpanel</span><span class="p">.</span><span class="n">track</span><span class="p">(</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="nl">properties:</span><span class="w"> </span><span class="n">params</span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="n">log</span><span class="p">(</span><span class="s1">&#39;</span><span class="si">$</span><span class="n">name</span><span class="s1"> </span><span class="si">$</span><span class="n">params</span><span class="s1">&#39;</span><span class="p">,</span><span class="w"> </span><span class="nl">name:</span><span class="w"> </span><span class="s1">&#39;Event&#39;</span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</div></code></pre><p>This gets the job done—fast. But you lose:</p><ul><li>Autocomplete</li><li>Type safety</li><li>Centralized event definitions</li><li>Vendor-level control and filtering</li></ul><figure><picture><source srcset="images/analytics-auto-completion.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Analytics methods auto-completion in VSCode" srcset="images/analytics-auto-completion.png 2x"/></picture><figcaption><center><i>Analytics methods auto-completion in VSCode</i></center></figcaption></figure><blockquote><p>Bottom line: For anything beyond a throwaway prototype, go with the more structured architecture. It’s more verbose up front, but it pays off in maintainability, testability, and developer experience.</p></blockquote><p>Now let’s see how to actually call the <code>analyticsFacadeProvider</code> to track events in real app code.</p><h2><a id="tracking-custom-events" href="#tracking-custom-events">Tracking Custom Events</a></h2><p>We’ve defined an interface like this:</p><pre><code><div class="highlight"><span></span><span class="kd">abstract</span><span class="w"> </span><span class="kd">class</span><span class="w"> </span><span class="nc">AnalyticsClient</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackNewAppOnboarding</span><span class="p">();</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackNewAppHome</span><span class="p">();</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppCreated</span><span class="p">();</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppUpdated</span><span class="p">();</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppDeleted</span><span class="p">();</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackTaskCompleted</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">completedCount</span><span class="p">);</span>
<span class="p">}</span>
</div></code></pre><p>But unless you actually call these methods, nothing gets tracked.</p><p>So where should you put these calls?</p><ul><li>In widgets?</li><li>In controllers?</li><li>Somewhere else?</li></ul><p>Let’s look at both options.</p><h3><a id="tracking-events-in-widgets-and-controllers" href="#tracking-events-in-widgets-and-controllers">Tracking Events in Widgets and Controllers</a></h3><p>Here’s a simple example—a <code>+</code> button on the home page:</p><figure><picture><source srcset="images/home-page-new-app-button.webp 2x" type="image/webp"/><img class="bottom-12px" alt="New app buttons on the home page" srcset="images/home-page-new-app-button.png 2x"/></picture><figcaption><center><i>New app buttons on the home page</i></center></figcaption></figure><p>The event tracking code looks like this:</p><pre><code><div class="highlight"><span></span><span class="n">IconButton</span><span class="p">(</span>
<span class="w">  </span><span class="nl">onPressed:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="c1">// event tracking code</span>
<span class="w">    </span><span class="n">unawaited</span><span class="p">(</span><span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">analyticsFacadeProvider</span><span class="p">).</span><span class="n">trackNewAppHome</span><span class="p">());</span>
<span class="w">    </span><span class="c1">// navigation code</span>
<span class="w">    </span><span class="n">Navigator</span><span class="p">.</span><span class="n">of</span><span class="p">(</span><span class="n">context</span><span class="p">).</span><span class="n">pushNamed</span><span class="p">(</span><span class="n">AppRoutes</span><span class="p">.</span><span class="n">createApp</span><span class="p">);</span>
<span class="w">  </span><span class="p">},</span>
<span class="w">  </span><span class="nl">icon:</span><span class="w"> </span><span class="n">Icon</span><span class="p">(</span><span class="n">Icons</span><span class="p">.</span><span class="n">add</span><span class="p">),</span>
<span class="p">)</span>
</div></code></pre><p>In this case, the analytics call is placed directly in the <code>onPressed</code> callback.</p><blockquote><p>💡 The <code>unawaited</code> function is used here to fire the event without blocking. These are fire-and-forget calls—you don’t need to wait for them. More on that here: <a href="https://codewithandrea.com/tips/use-unawaited-analytics-calls/">Use unawaited for your analytics calls</a>.</p></blockquote><p>But if your logic is more complex, it’s better to move analytics calls into a controller.</p><p>Here’s an example from a custom controller class:</p><pre><code><div class="highlight"><span></span><span class="c1">/// This class holds the business logic for creating, editing, and deleting apps</span>
<span class="c1">/// using the underlying AppDatabase class for data persistence.</span>
<span class="c1">/// More info here: https://codewithandrea.com/articles/flutter-presentation-layer/</span>
<span class="nd">@riverpod</span>
<span class="kd">class</span><span class="w"> </span><span class="nc">CreateEditAppController</span><span class="w"> </span><span class="kd">extends</span><span class="w"> </span><span class="n">_$CreateEditAppController</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="kt">void</span><span class="w"> </span><span class="n">build</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="c1">// no-op</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">createOrEditApp</span><span class="p">(</span><span class="n">App</span><span class="o">?</span><span class="w"> </span><span class="n">existingApp</span><span class="p">,</span><span class="w"> </span><span class="kt">String</span><span class="w"> </span><span class="n">newName</span><span class="p">)</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">final</span><span class="w"> </span><span class="n">db</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">appDatabaseProvider</span><span class="p">);</span>
<span class="w">    </span><span class="c1">// * Update the DB</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">existingApp</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="kd">await</span><span class="w"> </span><span class="n">db</span><span class="p">.</span><span class="n">editAppName</span><span class="p">(</span><span class="nl">appId:</span><span class="w"> </span><span class="n">existingApp</span><span class="p">.</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="nl">newName:</span><span class="w"> </span><span class="n">newName</span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="kd">await</span><span class="w"> </span><span class="n">db</span><span class="p">.</span><span class="n">createNewApp</span><span class="p">(</span><span class="nl">name:</span><span class="w"> </span><span class="n">newName</span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="c1">// * Analytics code</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">existingApp</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="n">unawaited</span><span class="p">(</span><span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">analyticsFacadeProvider</span><span class="p">).</span><span class="n">trackAppUpdated</span><span class="p">());</span>
<span class="w">    </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="n">unawaited</span><span class="p">(</span><span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">analyticsFacadeProvider</span><span class="p">).</span><span class="n">trackAppCreated</span><span class="p">());</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">deleteAppById</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">appId</span><span class="p">)</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">await</span><span class="w"> </span><span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">appDatabaseProvider</span><span class="p">).</span><span class="n">deleteAppById</span><span class="p">(</span><span class="n">appId</span><span class="p">);</span>
<span class="w">    </span><span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">analyticsFacadeProvider</span><span class="p">).</span><span class="n">trackAppDeleted</span><span class="p">();</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</div></code></pre><p>This controller handles mutations and database interactions—so it’s the perfect place to also track related analytics events.</p><h3><a id="key-guidelines-for-tracking-events" href="#key-guidelines-for-tracking-events">Key Guidelines for Tracking Events</a></h3><ul><li>❌ <strong>Never track events in <code>build()</code></strong>. It can be called dozens of times per second during animations. Same goes for <code>initState()</code> and other lifecycle methods.</li><li>✅ <strong>Track in widget callbacks</strong> like <code>onPressed</code>. If the logic is complex, move it out into a controller or service class.</li><li>✅ <strong>Tracking in controllers is ideal</strong>. They’re UI-free, easier to test, and already house the logic that triggers the events.</li><li>❌ <strong>Avoid tracking events in the data/networking layer</strong>. Events should be tracked at the source (UI layer) for better accuracy.</li><li>✅ <strong>Use <code>unawaited()</code></strong> for all analytics calls. Don’t block UI or care about results—just fire and forget.</li></ul><h2><a id="firebase-analytics-integration" href="#firebase-analytics-integration">Firebase Analytics Integration</a></h2><p>So far, we’ve been using the <code>LoggerAnalyticsClient</code> to print events to the console.</p><p>But for production apps, you’ll want to plug in a real analytics backend—like Firebase Analytics, Mixpanel, or PostHog.</p><p>Thanks to our <code>AnalyticsFacade</code>, you can do this without changing your app code. Just add a new client, register it, and you’re done. That’s the power of separation of concerns.</p><p>Let’s walk through integrating Firebase Analytics.</p><h3><a id="adding-firebase-to-your-flutter-app" href="#adding-firebase-to-your-flutter-app">Adding Firebase to your Flutter app</a></h3><p>Follow the official guide to <a href="https://firebase.google.com/docs/flutter/setup">add Firebase to your Flutter app</a>.</p><p>If your app uses multiple flavors (e.g. dev, staging, prod), the setup is more involved. I cover that in detail here:</p><ul><li><a href="https://codewithandrea.com/articles/flutter-firebase-multiple-flavors-flutterfire-cli/">How to Setup Flutter &amp; Firebase with Multiple Flavors using the FlutterFire CLI</a></li></ul><h3><a id="creating-the firebaseanalyticsclient class" href="#creating-the firebaseanalyticsclient class">Creating the FirebaseAnalyticsClient Class</a></h3><p>This is a straightforward implementation of the <code>AnalyticsClient</code> interface using the Firebase SDK:</p><pre><code><div class="highlight"><span></span><span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:firebase_analytics/firebase_analytics.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/src/monitoring/analytics_client.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:riverpod_annotation/riverpod_annotation.dart&#39;</span><span class="p">;</span>

<span class="k">part</span><span class="w"> </span><span class="s1">&#39;firebase_analytics_client.g.dart&#39;</span><span class="p">;</span>

<span class="kd">class</span><span class="w"> </span><span class="nc">FirebaseAnalyticsClient</span><span class="w"> </span><span class="kd">implements</span><span class="w"> </span><span class="n">AnalyticsClient</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">const</span><span class="w"> </span><span class="n">FirebaseAnalyticsClient</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">_analytics</span><span class="p">);</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="n">FirebaseAnalytics</span><span class="w"> </span><span class="n">_analytics</span><span class="p">;</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackNewAppHome</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">await</span><span class="w"> </span><span class="n">_analytics</span><span class="p">.</span><span class="n">logEvent</span><span class="p">(</span><span class="nl">name:</span><span class="w"> </span><span class="s1">&#39;new_app_home&#39;</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackNewAppOnboarding</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">await</span><span class="w"> </span><span class="n">_analytics</span><span class="p">.</span><span class="n">logEvent</span><span class="p">(</span><span class="nl">name:</span><span class="w"> </span><span class="s1">&#39;new_app_onboarding&#39;</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppCreated</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">await</span><span class="w"> </span><span class="n">_analytics</span><span class="p">.</span><span class="n">logEvent</span><span class="p">(</span><span class="nl">name:</span><span class="w"> </span><span class="s1">&#39;app_created&#39;</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppUpdated</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">await</span><span class="w"> </span><span class="n">_analytics</span><span class="p">.</span><span class="n">logEvent</span><span class="p">(</span><span class="nl">name:</span><span class="w"> </span><span class="s1">&#39;app_updated&#39;</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackAppDeleted</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">await</span><span class="w"> </span><span class="n">_analytics</span><span class="p">.</span><span class="n">logEvent</span><span class="p">(</span><span class="nl">name:</span><span class="w"> </span><span class="s1">&#39;app_deleted&#39;</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">trackTaskCompleted</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">completedCount</span><span class="p">)</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">await</span><span class="w"> </span><span class="n">_analytics</span><span class="p">.</span><span class="n">logEvent</span><span class="p">(</span>
<span class="w">      </span><span class="nl">name:</span><span class="w"> </span><span class="s1">&#39;task_completed&#39;</span><span class="p">,</span>
<span class="w">      </span><span class="nl">parameters:</span><span class="w"> </span><span class="p">{</span><span class="s1">&#39;count&#39;</span><span class="o">:</span><span class="w"> </span><span class="n">completedCount</span><span class="p">},</span>
<span class="w">    </span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>

<span class="nd">@Riverpod</span><span class="p">(</span><span class="nl">keepAlive:</span><span class="w"> </span><span class="kc">true</span><span class="p">)</span>
<span class="n">FirebaseAnalyticsClient</span><span class="w"> </span><span class="n">firebaseAnalyticsClient</span><span class="p">(</span><span class="n">Ref</span><span class="w"> </span><span class="n">ref</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="n">FirebaseAnalyticsClient</span><span class="p">(</span><span class="n">FirebaseAnalytics</span><span class="p">.</span><span class="n">instance</span><span class="p">);</span>
<span class="p">}</span>
</div></code></pre><h3><a id="registering-the-client-with-the-facade" href="#registering-the-client-with-the-facade">Registering the Client with the Facade</a></h3><p>Update your <code>analyticsFacadeProvider</code> to include the Firebase client:</p><pre><code><div class="highlight"><span></span><span class="nd">@Riverpod</span><span class="p">(</span><span class="nl">keepAlive:</span><span class="w"> </span><span class="kc">true</span><span class="p">)</span>
<span class="n">AnalyticsFacade</span><span class="w"> </span><span class="n">analyticsFacade</span><span class="p">(</span><span class="n">Ref</span><span class="w"> </span><span class="n">ref</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="n">firebaseAnalyticsClient</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ref</span><span class="p">.</span><span class="n">watch</span><span class="p">(</span><span class="n">firebaseAnalyticsClientProvider</span><span class="p">);</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="n">AnalyticsFacade</span><span class="p">([</span>
<span class="w">    </span><span class="n">firebaseAnalyticsClient</span><span class="p">,</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">kReleaseMode</span><span class="p">)</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="n">LoggerAnalyticsClient</span><span class="p">(),</span>
<span class="w">  </span><span class="p">]);</span>
<span class="p">}</span>
</div></code></pre><p>And just like that—no changes to the rest of your app—your events are now being sent to Firebase Analytics.</p><p>You’ll see them show up in the <a href="https://console.firebase.google.com/">Firebase Console</a>:</p><figure><picture><source srcset="images/firebase-dash-01-active-user.webp 2x" type="image/webp"/><img class="bottom-12px" alt="First active user showing in the Firebase Analytics dashboard" srcset="images/firebase-dash-01-active-user.png 2x"/></picture><figcaption><center><i>First active user showing in the Firebase Analytics dashboard</i></center></figcaption></figure><p>Here’s what it looks like in my <a href="https://fluttertips.dev/">Flutter Tips app</a>:</p><figure><picture><source srcset="images/firebase-dash-overview.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Firebase Analytics dashboard for the Flutter Tips app" srcset="images/firebase-dash-overview.png 2x"/></picture><figcaption><center><i>Firebase Analytics dashboard for the Flutter Tips app</i></center></figcaption></figure><h2><a id="flutter-app-analytics-summary" href="#flutter-app-analytics-summary">Flutter App Analytics: Summary</a></h2><p>In this article, we covered everything you need to get started with production-grade analytics in your Flutter app:</p><ul><li>✅ Why analytics is essential for shipping and growing with confidence</li><li>✅ How to think about and choose the right events to track</li><li>✅ A simple architecture for quick wins</li><li>✅ A scalable, type-safe architecture for larger apps and teams</li><li>✅ How to track events from widgets and controllers</li><li>✅ How to integrate Firebase Analytics with zero impact on app logic</li></ul><p>But hold on—there’s more to shipping analytics in real-world apps.</p><p>Before you hit publish, consider:</p><ul><li><strong>Tracking screen views</strong>: Use a navigation observer to capture screen transitions automatically.</li><li><strong>Opt-in/out analytics</strong>: Let users disable tracking if required (e.g. for GDPR compliance).</li><li><strong>User identification</strong>: Link events to logged-in users for cross-device tracking and funnel analysis.</li><li><strong>Mixpanel or PostHog integration</strong>: Firebase is free, but tools like Mixpanel offer powerful filtering and reporting—and are much more privacy-friendly.</li></ul><p>To go deeper into these topics—and way beyond analytics—check out my latest course. 👇</p><h2><a id="flutter-in-production" href="#flutter-in-production">Flutter in Production</a></h2><p>When it comes to <strong>shipping</strong> and <strong>maintaining</strong> apps in production, there are many important aspects to consider:</p><ul><li><strong>Preparing for release</strong>: splash screens, flavors, environments, error reporting, analytics, force update, privacy, T&amp;Cs.</li><li><strong>App Submissions</strong>: app store metadata &amp; screenshots, compliance, testing vs distribution tracks, dealing with rejections.</li><li><strong>Release automation:</strong> CI workflows, environment variables, custom build steps, code signing, uploading to the stores.</li><li><strong>Post-release</strong>: error monitoring, bug fixes, addressing user feedback, over-the-air updates, feature flags &amp; A/B testing.</li></ul><p>My latest course will help you get your app to the stores faster and with fewer headaches.</p><p>If you’re interested, you can learn more and enroll here. 👇</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/adaptive-alert-dialog/</guid><title>Adaptive Alert Dialog (Material, Cupertino)</title><description>A simple alert dialog that supports adaptive mode, default and cancel actions, destructive style, and dismissible mode.</description><link>https://codewithandrea.com/tips/adaptive-alert-dialog/</link><pubDate>Thu, 1 May 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>In many of my projects, I use a simple alert dialog that supports:</p><ul><li>Adaptive mode (Material / Cupertino)</li><li>Default and cancel actions</li><li>Destructive UI style</li><li>Dismissible mode</li></ul><p>Super useful for Yes/No, Cancel/Delete scenarios. 👍</p><figure><picture><source srcset="images/248.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Adaptive Alert Dialog" srcset="images/248.png 2x"/></picture></figure><p>Here's a gist with the source code:</p><ul><li><a href="https://gist.github.com/bizz84/e23dd316ec601791f60b43325a31ab93">Helper function for showing an adaptive alert dialog (Material, Cupertino)</a></li></ul><p>Feel free to tweak and reuse it in your projects. 🙂</p><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/articles/flutter-android-gradle-kts/</guid><title>Kotlin DSL in Flutter 3.29: How to Update Your Android Gradle Files</title><description>New Kotlin DSL in Flutter 3.29? Learn how to update your Gradle files, configure code signing, flavors, and more, in minutes.</description><link>https://codewithandrea.com/articles/flutter-android-gradle-kts/</link><pubDate>Wed, 30 Apr 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>The recent <a href="https://medium.com/flutter/whats-new-in-flutter-3-29-f90c380c2317">Flutter 3.29</a> release introduced many new updates to Impeller, Cupertino widgets, DevTools and more. But one big change flew under the radar: <strong>new Flutter projects now use the Kotlin DSL for Gradle files by default</strong>.</p><p>This has some implications for projects that rely on custom Gradle configurations, such as flavors, code signing, and more.</p><p>This article breaks down what changed, how it affects you, and how to avoid common pitfalls.</p><h2><a id="whats-changed?" href="#whats-changed?">What's changed?</a></h2><p>New projects generated with Flutter 3.29 now have <code>.kts</code> files instead of <code>.gradle</code>:</p><ul><li><code>android/build.gradle</code> → <code>android/build.gradle.kts</code></li><li><code>android/settings.gradle</code> → <code>android/settings.gradle.kts</code></li><li><code>android/app/build.gradle</code> → <code>android/app/build.gradle.kts</code></li></ul><figure><picture><source srcset="images/android-built-gradle-comparison.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Android project folder generated with Flutter 3.27 (left) and Flutter 3.29 (right)" srcset="images/android-built-gradle-comparison.png 2x"/></picture><figcaption><center><i>Android project folder generated with Flutter 3.27 (left) and Flutter 3.29 (right)</i></center></figcaption></figure><p>Before you panic: <strong>both Groovy (old) and Kotlin (new) files are fully supported</strong>.</p><p>If your project is using Groovy <code>.gradle</code> files, <strong>you don't need to migrate</strong>. Everything will still work with Flutter 3.29.</p><p><strong>Only brand-new projects</strong> start with <code>.kts</code> files.</p><p>That said, if you <em>are</em> working with <code>.kts</code>, there are a few things you’ll want to know. 👇</p><h2><a id="the-new-kotlin-dsl-syntax" href="#the-new-kotlin-dsl-syntax">The New Kotlin DSL Syntax</a></h2><p>Let’s play spot-the-difference between these two snippets:</p><pre><code><div class="highlight"><span></span><span class="c1">// Example 1 - is this Groovy or Kotlin?</span>
<span class="n">pluginManagement</span><span class="w"> </span><span class="o">{</span>
<span class="w">    </span><span class="kt">def</span><span class="w"> </span><span class="n">flutterSdkPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">{</span>
<span class="w">        </span><span class="kt">def</span><span class="w"> </span><span class="n">properties</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Properties</span><span class="o">()</span>
<span class="w">        </span><span class="n">file</span><span class="o">(</span><span class="s2">&quot;local.properties&quot;</span><span class="o">).</span><span class="na">withInputStream</span><span class="w"> </span><span class="o">{</span><span class="w"> </span><span class="n">properties</span><span class="o">.</span><span class="na">load</span><span class="o">(</span><span class="n">it</span><span class="o">)</span><span class="w"> </span><span class="o">}</span>
<span class="w">        </span><span class="kt">def</span><span class="w"> </span><span class="n">flutterSdkPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">properties</span><span class="o">.</span><span class="na">getProperty</span><span class="o">(</span><span class="s2">&quot;flutter.sdk&quot;</span><span class="o">)</span>
<span class="w">        </span><span class="k">assert</span><span class="w"> </span><span class="n">flutterSdkPath</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="o">,</span><span class="w"> </span><span class="s2">&quot;flutter.sdk not set in local.properties&quot;</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">flutterSdkPath</span>
<span class="w">    </span><span class="o">}()</span>

<span class="w">    </span><span class="n">includeBuild</span><span class="o">(</span><span class="s2">&quot;$flutterSdkPath/packages/flutter_tools/gradle&quot;</span><span class="o">)</span>

<span class="w">    </span><span class="n">repositories</span><span class="w"> </span><span class="o">{</span>
<span class="w">        </span><span class="n">google</span><span class="o">()</span>
<span class="w">        </span><span class="n">mavenCentral</span><span class="o">()</span>
<span class="w">        </span><span class="n">gradlePluginPortal</span><span class="o">()</span>
<span class="w">    </span><span class="o">}</span>
<span class="o">}</span>

<span class="n">plugins</span><span class="w"> </span><span class="o">{</span>
<span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="s2">&quot;dev.flutter.flutter-plugin-loader&quot;</span><span class="w"> </span><span class="n">version</span><span class="w"> </span><span class="s2">&quot;1.0.0&quot;</span>
<span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="s2">&quot;com.android.application&quot;</span><span class="w"> </span><span class="n">version</span><span class="w"> </span><span class="s2">&quot;8.1.0&quot;</span><span class="w"> </span><span class="n">apply</span><span class="w"> </span><span class="kc">false</span>
<span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="s2">&quot;org.jetbrains.kotlin.android&quot;</span><span class="w"> </span><span class="n">version</span><span class="w"> </span><span class="s2">&quot;1.8.22&quot;</span><span class="w"> </span><span class="n">apply</span><span class="w"> </span><span class="kc">false</span>
<span class="o">}</span>

<span class="n">include</span><span class="w"> </span><span class="s2">&quot;:app&quot;</span>
</div></code></pre><pre><code><div class="highlight"><span></span><span class="c1">// Example 2 - is this Groovy or Kotlin?</span>
<span class="n">pluginManagement</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">val</span><span class="w"> </span><span class="nv">flutterSdkPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">run</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="kd">val</span><span class="w"> </span><span class="nv">properties</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">java</span><span class="p">.</span><span class="na">util</span><span class="p">.</span><span class="na">Properties</span><span class="p">()</span>
<span class="w">        </span><span class="n">file</span><span class="p">(</span><span class="s">&quot;local.properties&quot;</span><span class="p">).</span><span class="na">inputStream</span><span class="p">().</span><span class="na">use</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">properties</span><span class="p">.</span><span class="na">load</span><span class="p">(</span><span class="nb">it</span><span class="p">)</span><span class="w"> </span><span class="p">}</span>
<span class="w">        </span><span class="kd">val</span><span class="w"> </span><span class="nv">flutterSdkPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">properties</span><span class="p">.</span><span class="na">getProperty</span><span class="p">(</span><span class="s">&quot;flutter.sdk&quot;</span><span class="p">)</span>
<span class="w">        </span><span class="n">require</span><span class="p">(</span><span class="n">flutterSdkPath</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s">&quot;flutter.sdk not set in local.properties&quot;</span><span class="w"> </span><span class="p">}</span>
<span class="w">        </span><span class="n">flutterSdkPath</span>
<span class="w">    </span><span class="p">}</span>

<span class="w">    </span><span class="n">includeBuild</span><span class="p">(</span><span class="s">&quot;</span><span class="si">$</span><span class="n">flutterSdkPath</span><span class="s">/packages/flutter_tools/gradle&quot;</span><span class="p">)</span>

<span class="w">    </span><span class="n">repositories</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="n">google</span><span class="p">()</span>
<span class="w">        </span><span class="n">mavenCentral</span><span class="p">()</span>
<span class="w">        </span><span class="n">gradlePluginPortal</span><span class="p">()</span>
<span class="w">    </span><span class="p">}</span>
<span class="p">}</span>

<span class="n">plugins</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;dev.flutter.flutter-plugin-loader&quot;</span><span class="p">)</span><span class="w"> </span><span class="n">version</span><span class="w"> </span><span class="s">&quot;1.0.0&quot;</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;com.android.application&quot;</span><span class="p">)</span><span class="w"> </span><span class="n">version</span><span class="w"> </span><span class="s">&quot;8.7.0&quot;</span><span class="w"> </span><span class="n">apply</span><span class="w"> </span><span class="kc">false</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;org.jetbrains.kotlin.android&quot;</span><span class="p">)</span><span class="w"> </span><span class="n">version</span><span class="w"> </span><span class="s">&quot;1.8.22&quot;</span><span class="w"> </span><span class="n">apply</span><span class="w"> </span><span class="kc">false</span>
<span class="p">}</span>

<span class="n">include</span><span class="p">(</span><span class="s">&quot;:app&quot;</span><span class="p">)</span>
</div></code></pre><p>Both snippets do the same thing, but there are key differences:</p><ul><li>Groovy uses <code>def</code> for variables; Kotlin uses <code>val</code>.</li><li>Groovy lets you skip parentheses in method calls; Kotlin requires them.</li><li>Groovy assertions use <code>assert</code>; Kotlin uses <code>require</code>.</li></ul><p>There are plenty more differences, all neatly documented here:</p><ul><li><a href="https://developer.android.com/build/migrate-to-kotlin-dsl">Migrate your build configuration from Groovy to Kotlin</a></li></ul><p>But what does this actually mean for your project?</p><h2><a id="common-gradle-setup-tasks" href="#common-gradle-setup-tasks">Common Gradle Setup Tasks</a></h2><p>Here are the typical things you’ll need to adjust:</p><ol><li>Set Android NDK, minSdk, targetSdk, Java version</li><li>Configure code signing</li><li>Add the <code>com.google.gms.google-services</code> plugin for apps using Firebase</li><li>Set up multiple flavors</li></ol><p>Old tutorials and Stack Overflow answers? <strong>Mostly outdated now</strong>. Here's how to handle it.</p><h3><a id="1-setting-the-android-ndk-minsdk-targetsdk-java-version" href="#1-setting-the-android-ndk-minsdk-targetsdk-java-version">1. Setting the Android NDK, minSdk, targetSdk, Java Version</a></h3><p>Same settings as before—just with the correct Kotlin syntax (assignment operator, double quotes for string literals):</p><pre><code><div class="highlight"><span></span><span class="p">...</span>

<span class="n">android</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">namespace</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;com.codewithandrea.flutter_ship_app&quot;</span>
<span class="w">    </span><span class="n">compileSdk</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">flutter</span><span class="p">.</span><span class="na">compileSdkVersion</span>
<span class="w">    </span><span class="n">ndkVersion</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;27.0.12077973&quot;</span><span class="w"> </span><span class="c1">// Updated</span>

<span class="w">    </span><span class="n">compileOptions</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="n">sourceCompatibility</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">JavaVersion</span><span class="p">.</span><span class="na">VERSION_17</span><span class="w"> </span><span class="c1">// Updated</span>
<span class="w">        </span><span class="n">targetCompatibility</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">JavaVersion</span><span class="p">.</span><span class="na">VERSION_17</span><span class="w"> </span><span class="c1">// Updated</span>
<span class="w">    </span><span class="p">}</span>

<span class="w">    </span><span class="n">kotlinOptions</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="n">jvmTarget</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">JavaVersion</span><span class="p">.</span><span class="na">VERSION_17</span><span class="p">.</span><span class="na">toString</span><span class="p">()</span><span class="w"> </span><span class="c1">// Updated</span>
<span class="w">    </span><span class="p">}</span>

<span class="w">    </span><span class="n">defaultConfig</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="c1">// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).</span>
<span class="w">        </span><span class="n">applicationId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;com.codewithandrea.flutter_ship_app&quot;</span>
<span class="w">        </span><span class="n">minSdk</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">21</span><span class="w"> </span><span class="c1">// Updated        </span>
<span class="w">        </span><span class="n">targetSdk</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">34</span><span class="w"> </span><span class="c1">// Updated</span>
<span class="w">        </span><span class="n">versionCode</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">flutter</span><span class="p">.</span><span class="na">versionCode</span>
<span class="w">        </span><span class="n">versionName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">flutter</span><span class="p">.</span><span class="na">versionName</span>
<span class="w">    </span><span class="p">}</span>

<span class="w">    </span><span class="p">...</span>
<span class="p">}</span>

<span class="p">...</span>
</div></code></pre><p><strong>Pro Tip:</strong> updating all these values manually is a pain, and I do it so often that I created a script to automate the process. Here's how it works:</p><ul><li><a href="https://codewithandrea.com/tips/update-android-project-script/">Script to Update the Android Project Settings</a></li></ul><p>Download it here and add it to your project:</p><ul><li><a href="https://gist.github.com/bizz84/605e2ca2088cb4acb7a076ca993f41cd">update-android-project.sh</a></li></ul><h3><a id="2-configuring-code-signing" href="#2-configuring-code-signing">2. Configuring Code Signing</a></h3><p>The official Flutter docs about <a href="https://docs.flutter.dev/deployment/android">building and releasing an Android app</a> are already updated for the new Kotlin DSL.</p><p>Key steps:</p><ul><li><a href="https://docs.flutter.dev/deployment/android#create-an-upload-keystore">Create an upload keystore</a></li><li><a href="https://docs.flutter.dev/deployment/android#reference-the-keystore-from-the-app">Reference the keystore</a></li><li><a href="https://docs.flutter.dev/deployment/android#configure-signing-in-gradle">Configure signing in Gradle</a></li></ul><p>Here's a sample code showing the required changes to the <code>android/app/build.gradle.kts</code> file:</p><pre><code><div class="highlight"><span></span><span class="k">import</span><span class="w"> </span><span class="nn">java.io.FileInputStream</span>
<span class="k">import</span><span class="w"> </span><span class="nn">java.util.Properties</span>

<span class="n">plugins</span><span class="w"> </span><span class="p">{</span>
<span class="w">   </span><span class="p">...</span>
<span class="p">}</span>

<span class="kd">val</span><span class="w"> </span><span class="nv">keystoreProperties</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Properties</span><span class="p">()</span>
<span class="kd">val</span><span class="w"> </span><span class="nv">keystorePropertiesFile</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rootProject</span><span class="p">.</span><span class="na">file</span><span class="p">(</span><span class="s">&quot;key.properties&quot;</span><span class="p">)</span>
<span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">keystorePropertiesFile</span><span class="p">.</span><span class="na">exists</span><span class="p">())</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">keystoreProperties</span><span class="p">.</span><span class="na">load</span><span class="p">(</span><span class="n">FileInputStream</span><span class="p">(</span><span class="n">keystorePropertiesFile</span><span class="p">))</span>
<span class="p">}</span>

<span class="n">android</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="c1">// ...</span>

<span class="w">    </span><span class="n">signingConfigs</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="n">create</span><span class="p">(</span><span class="s">&quot;release&quot;</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="n">keyAlias</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">keystoreProperties</span><span class="o">[</span><span class="s">&quot;keyAlias&quot;</span><span class="o">]</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="kt">String</span>
<span class="w">            </span><span class="n">keyPassword</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">keystoreProperties</span><span class="o">[</span><span class="s">&quot;keyPassword&quot;</span><span class="o">]</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="kt">String</span>
<span class="w">            </span><span class="n">storeFile</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">keystoreProperties</span><span class="o">[</span><span class="s">&quot;storeFile&quot;</span><span class="o">]?.</span><span class="na">let</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">file</span><span class="p">(</span><span class="nb">it</span><span class="p">)</span><span class="w"> </span><span class="p">}</span>
<span class="w">            </span><span class="n">storePassword</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">keystoreProperties</span><span class="o">[</span><span class="s">&quot;storePassword&quot;</span><span class="o">]</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="kt">String</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="n">buildTypes</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="n">release</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="c1">// TODO: Add your own signing config for the release build.</span>
<span class="w">            </span><span class="c1">// Signing with the debug keys for now,</span>
<span class="w">            </span><span class="c1">// so `flutter run --release` works.</span>
<span class="w">            </span><span class="n">signingConfig</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">signingConfigs</span><span class="p">.</span><span class="na">getByName</span><span class="p">(</span><span class="s">&quot;debug&quot;</span><span class="p">)</span>
<span class="w">            </span><span class="n">signingConfig</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">signingConfigs</span><span class="p">.</span><span class="na">getByName</span><span class="p">(</span><span class="s">&quot;release&quot;</span><span class="p">)</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">    </span><span class="p">}</span>
<span class="p">...</span>
<span class="p">}</span>
</div></code></pre><h3><a id="3-adding-the-comgooglegmsgoogle-services-plugin-firebase" href="#3-adding-the-comgooglegmsgoogle-services-plugin-firebase">3. Adding the com.google.gms.google-services plugin (Firebase)</a></h3><p>This is handled automatically when you <a href="https://codewithandrea.com/articles/flutter-firebase-multiple-flavors-flutterfire-cli/">add Firebase to your app with the FlutterFire CLI</a>.</p><p>Resulting <code>settings.gradle.kts</code>:</p><pre><code><div class="highlight"><span></span><span class="n">plugins</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;dev.flutter.flutter-plugin-loader&quot;</span><span class="p">)</span><span class="w"> </span><span class="n">version</span><span class="w"> </span><span class="s">&quot;1.0.0&quot;</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;com.android.application&quot;</span><span class="p">)</span><span class="w"> </span><span class="n">version</span><span class="w"> </span><span class="s">&quot;8.7.0&quot;</span><span class="w"> </span><span class="n">apply</span><span class="w"> </span><span class="kc">false</span>
<span class="w">    </span><span class="c1">// START: FlutterFire Configuration</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;com.google.gms.google-services&quot;</span><span class="p">)</span><span class="w"> </span><span class="n">version</span><span class="p">(</span><span class="s">&quot;4.3.15&quot;</span><span class="p">)</span><span class="w"> </span><span class="n">apply</span><span class="w"> </span><span class="kc">false</span>
<span class="w">    </span><span class="c1">// END: FlutterFire Configuration</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;org.jetbrains.kotlin.android&quot;</span><span class="p">)</span><span class="w"> </span><span class="n">version</span><span class="w"> </span><span class="s">&quot;1.8.22&quot;</span><span class="w"> </span><span class="n">apply</span><span class="w"> </span><span class="kc">false</span>
<span class="p">}</span>
</div></code></pre><p>The same plugin will also be listed in <code>android/app/build.gradle.kts</code>:</p><pre><code><div class="highlight"><span></span><span class="n">plugins</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;com.android.application&quot;</span><span class="p">)</span>
<span class="w">    </span><span class="c1">// START: FlutterFire Configuration</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;com.google.gms.google-services&quot;</span><span class="p">)</span>
<span class="w">    </span><span class="c1">// END: FlutterFire Configuration</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;kotlin-android&quot;</span><span class="p">)</span>
<span class="w">    </span><span class="c1">// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;dev.flutter.flutter-gradle-plugin&quot;</span><span class="p">)</span>
<span class="p">}</span>
</div></code></pre><h3><a id="4-adding-support-for-multiple-flavors" href="#4-adding-support-for-multiple-flavors">4. Adding Support for Multiple Flavors</a></h3><p>This one is a bit tricky as it requires defining separate product flavors for your app.</p><p>The <a href="https://pub.dev/packages/flutter_flavorizr">Flutter Flavorizr plugin</a> already handles this for you when you run these processors:</p><pre><code><div class="highlight"><span></span>dart<span class="w"> </span>run<span class="w"> </span>flutter_flavorizr<span class="w"> </span>-p<span class="w"> </span>android:buildGradle,android:flavorizrGradle,android:androidManifest
</div></code></pre><p>If you're doing it manually, open <code>android/app/build.gradle.kts</code> and add this line at the very bottom:</p><pre><code><div class="highlight"><span></span><span class="n">apply</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">from</span><span class="p">(</span><span class="s">&quot;flavors.gradle.kts&quot;</span><span class="p">)</span><span class="w"> </span><span class="p">}</span>
</div></code></pre><p>Then, create a <code>android/app/flavors.gradle.kts</code> file with these contents:</p><pre><code><div class="highlight"><span></span><span class="k">import</span><span class="w"> </span><span class="nn">com.android.build.gradle.AppExtension</span>

<span class="kd">val</span><span class="w"> </span><span class="nv">android</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">project</span><span class="p">.</span><span class="na">extensions</span><span class="p">.</span><span class="na">getByType</span><span class="p">(</span><span class="n">AppExtension</span><span class="o">::</span><span class="n">class</span><span class="p">.</span><span class="na">java</span><span class="p">)</span>

<span class="n">android</span><span class="p">.</span><span class="na">apply</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">flavorDimensions</span><span class="p">(</span><span class="s">&quot;flavor-type&quot;</span><span class="p">)</span>

<span class="w">    </span><span class="n">productFlavors</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="n">create</span><span class="p">(</span><span class="s">&quot;dev&quot;</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="n">dimension</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;flavor-type&quot;</span>
<span class="w">            </span><span class="n">applicationId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;com.codewithandrea.flutter_ship_app.dev&quot;</span>
<span class="w">            </span><span class="n">resValue</span><span class="p">(</span><span class="n">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;string&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;app_name&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;Flutter Ship Dev&quot;</span><span class="p">)</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">        </span><span class="n">create</span><span class="p">(</span><span class="s">&quot;stg&quot;</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="n">dimension</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;flavor-type&quot;</span>
<span class="w">            </span><span class="n">applicationId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;com.codewithandrea.flutter_ship_app.stg&quot;</span>
<span class="w">            </span><span class="n">resValue</span><span class="p">(</span><span class="n">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;string&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;app_name&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;Flutter Ship Stg&quot;</span><span class="p">)</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">        </span><span class="n">create</span><span class="p">(</span><span class="s">&quot;prod&quot;</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="n">dimension</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;flavor-type&quot;</span>
<span class="w">            </span><span class="n">applicationId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;com.codewithandrea.flutter_ship_app&quot;</span>
<span class="w">            </span><span class="n">resValue</span><span class="p">(</span><span class="n">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;string&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;app_name&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;Flutter Ship&quot;</span><span class="p">)</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">    </span><span class="p">}</span>
<span class="p">}</span>
</div></code></pre><p>Notes:</p><ul><li>Make sure the flavor names match your <code>flutter run --flavor [name]</code> command.</li><li>Update the <code>applicationId</code> and app name for each flavor.</li><li>In <code>AndroidManifest.xml</code>, set the app label dynamically in the <code>android:label</code> attribute:</li></ul><pre><code><div class="highlight"><span></span><span class="nt">&lt;manifest</span><span class="w"> </span><span class="na">xmlns:android=</span><span class="s">&quot;http://schemas.android.com/apk/res/android&quot;</span><span class="nt">&gt;</span>
<span class="w">  </span><span class="nt">&lt;application</span>
<span class="w">    </span><span class="na">android:label=</span><span class="s">&quot;@string/app_name&quot;</span><span class="w"> </span><span class="err">&lt;--</span><span class="w"> </span><span class="err">updated</span>
<span class="w">    </span><span class="na">android:name=</span><span class="s">&quot;${applicationName}&quot;</span>
<span class="w">    </span><span class="na">android:icon=</span><span class="s">&quot;@mipmap/ic_launcher&quot;</span><span class="nt">&gt;</span>
<span class="w">    </span>...
<span class="w">  </span><span class="nt">&lt;/application&gt;</span>
<span class="w">  </span>...
<span class="nt">&lt;/manifest&gt;</span>
</div></code></pre><blockquote><p>There's much more to adding flavors than what I've shown here. For a full guide, check out my <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production course</a>.</p></blockquote><h2><a id="what-if-you-need-to-recreate-your-android-project?" href="#what-if-you-need-to-recreate-your-android-project?">What If You Need to Recreate Your Android Project?</a></h2><p>Old Flutter apps sometimes break on new Android tooling. If you’re stuck, it’s often easier to <a href="https://codewithandrea.com/tips/fixing-build-issues-nuclear-option/">nuke and recreate the Android folder</a>:</p><pre><code><div class="highlight"><span></span><span class="c1"># Commit to git before making any changes</span>
git<span class="w"> </span>add<span class="w"> </span>.<span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span>git<span class="w"> </span>commit<span class="w"> </span>-m<span class="w"> </span><span class="s2">&quot;Working copy&quot;</span>
<span class="c1"># Delete android folder</span>
rm<span class="w"> </span>-rf<span class="w"> </span>android
<span class="c1"># Create it again with the Flutter CLI</span>
flutter<span class="w"> </span>create<span class="w"> </span>.<span class="w"> </span>--platforms<span class="w"> </span>android<span class="w"> </span>--org<span class="w"> </span>com.yourorgname
<span class="c1"># See what&#39;s changed</span>
git<span class="w"> </span>diff<span class="w"> </span>android
<span class="c1"># Reapply previous settings, following steps above</span>
<span class="c1"># Run again</span>
flutter<span class="w"> </span>run
<span class="c1"># All good? Commit to git</span>
git<span class="w"> </span>add<span class="w"> </span>.<span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span>git<span class="w"> </span>commit<span class="w"> </span>-m<span class="w"> </span><span class="s2">&quot;Updated Android project&quot;</span>
</div></code></pre><blockquote><p>When you recreate the Android project from scratch, <strong>any old settings you had applied will also be lost</strong>. Always run <code>git diff android</code> to see what's changed, and selectively restore any custom settings as needed.</p></blockquote><h2><a id="conclusion" href="#conclusion">Conclusion</a></h2><p>Starting with Flutter 3.29, new projects use the Kotlin DSL for Gradle.</p><p>Honestly? <strong>I see little real benefit</strong> over Groovy. Yet, our tech overlords have decided this is the way, and we have to keep up and learn a new syntax. 🤷‍♂️</p><p><strong>My advice:</strong></p><ul><li>If you're not forced to migrate, <strong>stick with Groovy</strong>.</li><li>If you start fresh or need Kotlin DSL, <strong>this guide has you covered</strong>.</li></ul><p>Need more help shipping your app? Keep reading.</p><h2><a id="flutter-in-production" href="#flutter-in-production">Flutter in Production</a></h2><p>Shipping and maintaining apps in production takes more than just writing code:</p><ul><li><strong>Preparing for release</strong>: splash screens, flavors, environments, error reporting, analytics, force update, privacy, T&amp;Cs.</li><li><strong>App Submissions</strong>: metadata, screenshots, compliance, testing vs distribution tracks, dealing with rejections.</li><li><strong>Release automation:</strong> CI workflows, environment variables, custom build steps, code signing, uploading to the stores.</li><li><strong>Post-release</strong>: monitoring crashes, fixing bugs, OTA updates, feature flags &amp; A/B testing.</li></ul><p>My latest course will help you ship faster and with fewer headaches. Learn more here. 👇</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/firebase-remote-config-init/</guid><title>How to Initialize Firebase Remote Config</title><description>A battle tested solution for loading Firebase Remote Config in Flutter, avoiding common pitfalls.</description><link>https://codewithandrea.com/tips/firebase-remote-config-init/</link><pubDate>Wed, 30 Apr 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Firebase Remote Config is great, but it's a bit tricky to configure it correctly.</p><p>Here's my battle-tested loading strategy:</p><ol><li>setup fetch interval (flavor-dependent)</li><li>set default values</li><li>activate previous values</li><li>fetch new values (unawaited)</li><li>add a realtime listener (which is not subject to the <code>minimumFetchInterval</code>)</li></ol><figure><picture><source srcset="images/247.webp 2x" type="image/webp"/><img class="bottom-40px" alt="How to Initialize Firebase Remote Config" srcset="images/247.png 2x"/></picture></figure><p>Want to use this in your apps? Grab it from this gist:</p><ul><li><a href="https://gist.github.com/bizz84/5bdbc7685564e43c9cdfe5b658248f4b">Flutter Remote Config boilerplate code (using Riverpod)</a></li></ul><p>For more info about common loading strategies, read:</p><ul><li><a href="https://firebase.google.com/docs/remote-config/loading">Firebase Remote Config loading strategies</a></li></ul><h3><a id="flutter-in-production" href="#flutter-in-production">Flutter in Production</a></h3><p>My latest course shows how to use Firebase Remote Config in useful scenarios such as force updates and A/B tests.</p><p>Learn more here 👇</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/disable-impeller-android/</guid><title>How to Disable Impeller on Android</title><description>debugRepaintRainbowEnabled helps you discover widgets/areas that unexpectedly repaint in your app. Here's how to use it.</description><link>https://codewithandrea.com/tips/disable-impeller-android/</link><pubDate>Tue, 29 Apr 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Since Flutter 3.29, Impeller is enabled by default on Android, but it can cause some rendering issues on some devices.</p><p>If needed, here's how to disable it and revert to the old Skia renderer. 👇</p><figure><picture><source srcset="images/246.webp 2x" type="image/webp"/><img class="bottom-40px" alt="How to disable Impeller on Android" srcset="images/246.png 2x"/></picture></figure><p>See this changelog for some Impeller issues that were recently fixed:</p><ul><li><a href="https://github.com/flutter/flutter/blob/stable/CHANGELOG.md#flutter-329-changes">Flutter 3.29 Changes</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/april-2025/</guid><title>April 2025: Flutter Roadmap Update, New Beta Release, Latest Community Articles</title><description>Also included: upcoming changes to the formatter in Dart 3.8, common mistakes in Flutter and Dart development, OWASP Mobile Top 10 series.</description><link>https://codewithandrea.com/newsletter/april-2025/</link><pubDate>Thu, 24 Apr 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Spring has officially arrived 🌸—and while we’re likely waiting until <a href="https://io.google/2025/">Google I/O</a> for the next stable Flutter release, there’s already plenty happening in the ecosystem.</p><p>This month’s newsletter covers:</p><ul><li>Flutter’s updated 2025 roadmap</li><li>What’s new in the latest beta release</li><li>Upcoming formatter changes and new IDE assists in Dart 3.8</li><li>Hand-picked articles about common Flutter mistakes, app security, and more</li></ul><p>Let’s dive in! 🚀</p><h2><a id="🧭-flutter-2025-roadmap-update" href="#🧭-flutter-2025-roadmap-update">🧭 Flutter 2025 Roadmap Update</a></h2><p>The Flutter team has shared their <a href="https://github.com/flutter/flutter/blob/master/docs/roadmap/Roadmap.md">official 2025 roadmap</a>, outlining key focus areas for the year.</p><p>Highlights include:</p><ul><li><strong>Interoperability on iOS &amp; Android</strong>: Continue experimental work to support direct calls from Dart to Swift/Obj-C (iOS) and Kotlin/Java (Android). This includes calling APIs that can only be invoked on the main OS/platform thread.</li><li><strong>Web Platform</strong>: Hot reload support, better performance, improved accessibility, and the removal of legacy HTML/JS libraries.</li><li><strong>Core Framework</strong>: Investigate changes to reduce verbosity in Flutter widget code and streamline development.</li><li><strong>Dart Language Improvements</strong>: Enhancements to code generation, new language features, and a refactored analyzer for better performance and tooling.</li></ul><p>Personally, I’m most excited about <a href="https://codewithandrea.com/tips/hot-reload-flutter-web-beta/">hot reload for Flutter web</a> (finally!), as well as new Dart language features and build_runner improvements.</p><p>Learn more here:</p><ul><li><a href="https://github.com/flutter/flutter/blob/master/docs/roadmap/Roadmap.md">2025 Roadmap</a></li></ul><h2><a id="🆕-wanna-help-flutter?-try-out-the-beta!" href="#🆕-wanna-help-flutter?-try-out-the-beta!">🆕 Wanna help Flutter? Try out the beta!</a></h2><p>The last stable release—<strong>Flutter 3.29</strong>—introduced some big changes (like Impeller on Android) and left many developers frustrated with regressions and waiting for <a href="https://github.com/flutter/flutter/blob/stable/CHANGELOG.md#3293">hotfixes</a>.</p><p>The Flutter team is now encouraging developers to test the <strong>latest beta</strong> and report bugs early. If you want to help shape the next stable release, now’s a great time to get involved:</p><ul><li><a href="https://www.reddit.com/r/FlutterDev/comments/1k35goq/wanna_help_flutter_try_out_the_beta/">Wanna help Flutter? Try out the beta!</a></li></ul><p>I think this will help iron out potential issues before the next stable release. It also means we can already try out some of the upcoming changes. 👇</p><h3><a id="⌨️-dart-38-formatter-improvements" href="#⌨️-dart-38-formatter-improvements">⌨️ Dart 3.8: Formatter Improvements</a></h3><p>The new <a href="https://codewithandrea.com/articles/new-formatting-style-dart-3-7/">formatting style introduced in Dart 3.7</a> was quite controversial, with many people <a href="https://www.reddit.com/r/FlutterDev/comments/1k2s4zy/new_dart_formatting_is_hurting_productivity/">against the decision to automatically remove trailing commas</a>.</p><p>The good news? Dart 3.8 will introduce an opt-in formatter config to <strong>preserve trailing commas</strong>.</p><p>To try it out, upgrade to the <a href="https://docs.flutter.dev/release/archive#beta-channel">latest beta</a> and add the following to your <code>analysis_options.yaml</code>:</p><pre><code><div class="highlight"><span></span><span class="nt">formatter</span><span class="p">:</span>
<span class="w">  </span><span class="nt">trailing_commas</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">preserve</span>
</div></code></pre><p>More info:</p><ul><li><a href="https://www.reddit.com/r/FlutterDev/comments/1jyu2si/dart_38_will_contain_an_updated_formatter_that/">Dart 3.8 will contain an updated formatter that can preserve commas</a></li></ul><h3><a id="⚡️-new-ide-assists-coming-to-dart-38" href="#⚡️-new-ide-assists-coming-to-dart-38">⚡️ New IDE Assists Coming to Dart 3.8</a></h3><p>Dart 3.8 also brings a bunch of new <strong>code assists and quality-of-life improvements</strong>:</p><ul><li>Smarter code completion</li><li>Quick wraps with <code>FutureBuilder</code> and <code>ValueListenableBuilder</code></li><li>Rename closure parameters</li><li>Better <code>show</code>/<code>hide</code> combinator assists for imports</li></ul><p>For the full list, check the changelog:</p><ul><li><a href="https://github.com/dart-lang/sdk/blob/main/CHANGELOG.md#analyzer">Dart 3.8 Analyzer Changelog</a></li></ul><blockquote><p>Shoutout to <a href="https://github.com/FMorschel">@FMorschel</a> for driving many of these improvements!</p></blockquote><h3><a id="⚠️-hot-restart-bug-on-ios-flutter-329" href="#⚠️-hot-restart-bug-on-ios-flutter-329">⚠️ Hot Restart Bug on iOS (Flutter 3.29)</a></h3><p>Some devs (myself included) have hit an annoying issue where <strong>hot restart hangs on iOS</strong>.</p><p>Thankfully, <a href="https://x.com/luke_pighetti">Luke Pighetti</a> filed an <a href="https://github.com/flutter/flutter/issues/165656">issue</a>, and the Flutter team <a href="https://github.com/flutter/flutter/issues/165656#issuecomment-2746241011">already confirmed</a> that it's fixed on master. If you're affected, <a href="https://github.com/flutter/flutter/issues/165656#issuecomment-2766546922">read this comment</a> for a temporary workaround.</p><p>For more details, here's the full issue:</p><ul><li><a href="https://github.com/flutter/flutter/issues/165656">hot restart locks up on iOS</a></li></ul><h2><a id="📚-articles-from-the-community" href="#📚-articles-from-the-community">📚 Articles from the Community</a></h2><p>Here are some standout reads from the past month:</p><h3><a id="📝-15-common-mistakes-in-flutter-and-dart-development-and-how-to-avoid-them" href="#📝-15-common-mistakes-in-flutter-and-dart-development-and-how-to-avoid-them">📝 15 Common Mistakes in Flutter and Dart Development (and How to Avoid Them)</a></h3><p>From memory leaks and rebuild traps to architectural missteps, <a href="https://bsky.app/profile/mhadaily.bsky.social">Majid Hajian</a> shares hard-earned lessons from 7 years of Flutter development.</p><p>This article covers:</p><ul><li>Real-world mistakes in large Flutter apps</li><li>How to catch issues with tools like <a href="https://dcm.dev/">DCM</a></li><li>Strategies for better maintainability and performance</li></ul><p>Read on to explore common pitfalls and learn how to avoid them:</p><ul><li><a href="https://dcm.dev/blog/2025/03/24/fifteen-common-mistakes-flutter-dart-development/">15 Common Mistakes in Flutter and Dart Development (and How to Avoid Them)</a></li></ul><h3><a id="🛡️-owasp-top-10-for-flutter-security-series" href="#🛡️-owasp-top-10-for-flutter-security-series">🛡️ OWASP Top 10 for Flutter (Security Series)</a></h3><p>Majid is also behind this new series on <strong>Flutter security</strong>, applying the <a href="https://owasp.org/www-project-mobile-top-10/">OWASP Mobile Top 10</a> to real-world Flutter apps.</p><p>So far, three parts are out:</p><ul><li><a href="https://docs.talsec.app/appsec-articles/articles/owasp-top-10-for-flutter-m1-mastering-credential-security-in-flutter">M1: Mastering Credential Security in Flutter</a></li><li><a href="https://docs.talsec.app/appsec-articles/articles/owasp-top-10-for-flutter-m2-inadequate-supply-chain-security-in-flutter">M2: Inadequate Supply Chain Security in Flutter</a></li><li><a href="https://docs.talsec.app/appsec-articles/articles/owasp-top-10-for-flutter-m3-insecure-authentication-and-authorization-in-flutter">M3: Insecure Authentication and Authorization in Flutter</a></li></ul><p>If you’re building apps that handle sensitive data, this is essential reading.</p><h3><a id="📝-typeset-whatsapp-style-text-formatting-in-flutter" href="#📝-typeset-whatsapp-style-text-formatting-in-flutter">📝 TypeSet: WhatsApp-Style Text Formatting in Flutter</a></h3><p><a href="https://pub.dev/packages/typeset">TypeSet</a> is a small but powerful package that lets you style inline text with a WhatsApp-style syntax:</p><pre><code><div class="highlight"><span></span><span class="n">TypeSet</span><span class="p">(</span><span class="s1">&#39;Hello *bold* _italic_ ~strikethrough~ #underlined# `monospace` text&#39;</span><span class="p">);</span>
</div></code></pre><p>Perfect for chat apps or UIs that need rich text formatting without complexity.</p><p>To learn more, check out the <a href="https://pub.dev/packages/typeset">package on pub.dev</a> or read the full article:</p><ul><li><a href="https://rohanjsh.medium.com/typeset-bringing-whatsapp-style-text-formatting-to-your-flutter-apps-9690c5979c35">TypeSet: Bringing WhatsApp-Style Text Formatting to Your Flutter Apps</a></li></ul><h2><a id="📢-latest-from-code-with-andrea" href="#📢-latest-from-code-with-andrea">📢 Latest from Code with Andrea</a></h2><p>No new articles from me this month—I’ve been focused on finishing my <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> course.</p><p>The good news? <strong>It’s now complete</strong> with over 200 lessons! 🎉</p><p>To celebrate this, I’m running a <strong>Spring Sale</strong> next week. If you’ve been thinking about enrolling, here are all the details about the upcoming sale:</p><ul><li><a href="https://codewithandrea.com/emails/2025-spring-01-announcement/">Flutter in Production is Complete – and 50% Off Next Week! 🎉</a></li></ul><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>I recently stumbled across a new cool site called <a href="https://www.isthistechdead.com/flutter">Is Flutter dead?</a>—glad to see it still says <strong>no</strong>! 😅</p><p>As the Flutter ecosystem continues to grow, I’m already brainstorming new content and ideas for future courses!</p><p>So stay tuned, and as always, happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/debug-repaint-rainbow-enabled/</guid><title>How to use debugRepaintRainbowEnabled</title><description>debugRepaintRainbowEnabled helps you discover widgets/areas that unexpectedly repaint in your app. Here's how to use it.</description><link>https://codewithandrea.com/tips/debug-repaint-rainbow-enabled/</link><pubDate>Thu, 24 Apr 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p><code>debugRepaintRainbowEnabled</code> helps you discover widgets/areas that unexpectedly repaint in your app.</p><p>Here's how to set it 👇</p><figure><picture><img class="bottom-40px" alt="How to use debugRepaintRainbowEnabled" srcset="images/245.gif 1x"/></picture></figure><p>If you discover widgets that repaint when they shouldn't, wrap them with <code>RepaintBoundary</code>.</p><p>This can improve rendering performance, but only use it when necessary (<code>RepaintBoundary</code> has some extra CPU/memory cost).</p><p>Learn more here:</p><ul><li><a href="https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html">RepaintBoundary class</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/text-form-field-numeric-inputs/</guid><title>TextFormField Setup for Numeric Inputs</title><description>When working with forms in Flutter, numeric inputs need special attention. To improve the user experience, set the appropriate keyboardType and inputFormatters.</description><link>https://codewithandrea.com/tips/text-form-field-numeric-inputs/</link><pubDate>Tue, 15 Apr 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>When working with forms in Flutter, numeric inputs need special attention.</p><p>To improve the user experience:</p><ol><li>Add <code>keyboardType: TextInputType.number</code></li><li>Add <code>FilteringTextInputFormatter.digitsOnly</code> as an inputFormatter</li></ol><p>Your users will thank you!</p><figure><picture><source srcset="images/244.webp 2x" type="image/webp"/><img class="bottom-40px" alt="TextFormField Setup for Numeric Inputs" srcset="images/244.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/show-flutter-web-url-text-span/</guid><title>Showing URLs on Flutter web with TextSpan</title><description>The TextSpan class lets you set a custom mouse cursor style, along with a tap gesture recognizer for opening your URL links on Flutter web.</description><link>https://codewithandrea.com/tips/show-flutter-web-url-text-span/</link><pubDate>Wed, 9 Apr 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>You can show native-looking URL links on Flutter web using <code>TextSpan</code>:</p><ul><li>Create a <code>Text.rich</code> or <code>SelectableText.rich</code> widget</li><li>Make it look like a web link with a custom text style</li><li>Add a gesture recognizer to open the web link</li><li>Customize the mouse cursor style</li></ul><figure><picture><img class="bottom-40px" alt="Showing URLs on Flutter web with TextSpan" srcset="images/243.gif 1x"/></picture></figure><p>As an alternative, wrap the whole thing with a <a href="https://pub.dev/documentation/url_launcher/latest/link/Link-class.html"><code>Link</code></a> widget (from the <a href="https://pub.dev/packages/url_launcher"><code>url_launcher</code></a> package):</p><figure><picture><img class="bottom-40px" alt="Alternative with Link widget (url_launcher)" srcset="images/243.2.png 2x"/></picture></figure><p>Here's a complete <code>URLTextWidget</code> that implements the whole thing, in 40 lines of code:</p><ul><li><a href="https://gist.github.com/bizz84/accc69b941a6903cfe4e312f68779ba9">URLTextWidget (GitHub Gist)</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/ab-testing-flutter/</guid><title>A/B Testing in Flutter</title><description>A/B tests help you make data-driven decisions and increase conversions in your app. Here's how they work.</description><link>https://codewithandrea.com/tips/ab-testing-flutter/</link><pubDate>Tue, 8 Apr 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>A/B tests help you make data-driven decisions and increase conversions in your app.</p><p>To run an A/B test: 1. Choose a metric to improve 2. Formulate a hypothesis 3. Split the audience into cohorts 4. Run an experiment, analyze the results, and roll out the winner 🥇</p><figure><picture><img class="bottom-40px" alt="A/B Testing in Flutter" srcset="images/242.1.png 2x"/></picture></figure><p>Tools like <a href="https://firebase.google.com/products/remote-config">Firebase Remote Config</a> and <a href="https://posthog.com/experiments">PostHog</a> make it easy to run A/B tests in your apps.</p><p>For example, here's a report from an A/B test I ran on my <a href="https://fluttertips.dev/">Flutter Tips app</a>:</p><figure><picture><img class="bottom-40px" alt="A/B Testing with Firebase Remote Config" srcset="images/242.2.png 2x"/></picture></figure><p>But how do you implement this in practice?</p><p>My latest course includes an entire module about Feature Toggles and A/B Tests.</p><p>Learn more here:</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/release-toggles-dart-define/</guid><title>Release Toggles with Dart Defines</title><description>Static release toggles let you release unfinished code without activating it in production. Here's how to use --dart-define to manage them.</description><link>https://codewithandrea.com/tips/release-toggles-dart-define/</link><pubDate>Wed, 2 Apr 2025 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Static release toggles let you release unfinished code without activating it in production.</p><p>This is common with large projects that practice trunk-based deployment and continuous delivery.</p><p>For more flexibility, you can implement this with <code>--dart-define</code>. 👇</p><figure><picture><source srcset="images/241.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Release Toggles with Dart Defines" srcset="images/241.png 2x"/></picture></figure><p>Static release toggles are easy to use, but some use cases require more flexibility:</p><ul><li>Experiment by showing different variants to separate user cohorts (A/B and multivariate testing)</li><li>Gradually roll out a feature to users, rather than releasing it to everyone at once</li></ul><p>Complex use cases require dynamic toggles and some dedicated infrastructure to manage them, and my latest course covers this topic in much more detail.</p><p>Learn more here. 👇</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/int-bool-from-environment/</guid><title>int.fromEnvironment and bool.fromEnvironment</title><description>When reading variables from .env files, you can use int.fromEnvironment and bool.fromEnvironment to read integers and booleans.</description><link>https://codewithandrea.com/tips/int-bool-from-environment/</link><pubDate>Fri, 28 Mar 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>In addition to <a href="https://api.dart.dev/dart-core/String/String.fromEnvironment.html"><code>String.fromEnvironment</code></a>, Dart also supports:</p><ul><li><a href="https://api.dart.dev/dart-core/int/int.fromEnvironment.html"><code>int.fromEnvironment</code></a></li><li><a href="https://api.dart.dev/dart-core/bool/bool.fromEnvironment.html"><code>bool.fromEnvironment</code></a></li></ul><p>Very handy when reading environment variables from your <code>.env</code> files:</p><figure><picture><source srcset="images/240.webp 2x" type="image/webp"/><img class="bottom-40px" alt="int.fromEnvironment and bool.fromEnvironment" srcset="images/240.png 2x"/></picture></figure><p>To learn more about environment variables and best practices for storing API keys, read:</p><ul><li><a href="https://codewithandrea.com/articles/flutter-api-keys-dart-define-env-files/">How to Store API Keys in Flutter: --dart-define vs .env files</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/march-2025/</guid><title>March 2025: Hot-reload on Flutter web, Practical Architecture, Unified Riverpod Syntax</title><description>Also included: Lesser-known Dart and Flutter functionalities, latest from Code with Andrea, and some thoughts on vibe coding with AI.</description><link>https://codewithandrea.com/newsletter/march-2025/</link><pubDate>Tue, 25 Mar 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>After the <strong>Flutter 3.29</strong> release (don’t forget to update to the latest <a href="https://github.com/flutter/flutter/blob/stable/CHANGELOG.md#3292">hotfix</a>), things have been relatively quiet this month.</p><p>Still, there are some exciting updates worth sharing:</p><ul><li>Hot reload finally comes to Flutter web (3.31 beta)</li><li>Practical Flutter Architecture</li><li>Lesser-known Dart &amp; Flutter features you should be using</li><li>Preview of the unified provider syntax for the next Riverpod release</li><li>Latest from Code with Andrea</li><li>Some thoughts on the "vibe coding" trend with AI</li></ul><p>Let’s dive in! 🚀</p><h2><a id="flutter-news-and-articles" href="#flutter-news-and-articles">Flutter News and Articles</a></h2><h3><a id="⚡️ hot-reload-on-flutter-web-beta" href="#⚡️ hot-reload-on-flutter-web-beta">⚡️ Hot Reload on Flutter Web (Beta)</a></h3><p>Hot reload is one of Flutter’s <strong>biggest strengths</strong>, making development incredibly fast. But until now, <strong>Flutter Web didn’t support it</strong>—despite being the <a href="https://github.com/flutter/flutter/issues/53041">#2 most upvoted feature request</a>.</p><p>That’s finally changing with <strong>Flutter 3.31 beta</strong>! 🎉 You can now <strong>try hot reload on Flutter Web</strong> before it hits stable.</p><p>To enable it, follow these steps:</p><ul><li><a href="https://codewithandrea.com/tips/hot-reload-flutter-web-beta/">Hot Reload on Flutter Web (Beta)</a></li></ul><p>For all the details, check out the original thread on Reddit:</p><ul><li><a href="https://www.reddit.com/r/FlutterDev/comments/1jedakr/try_out_hot_reload_on_the_web_with_the_latest/">Try Out Hot Reload on Flutter Web</a></li></ul><h3><a id="📝-practical-flutter-architecture" href="#📝-practical-flutter-architecture">📝 Practical Flutter Architecture</a></h3><p>App architecture remains one of the <strong>hottest topics in Flutter development</strong>—a good structure can make or break your app in the long run.</p><p>Existing resources include:</p><ul><li>The official <a href="https://docs.flutter.dev/app-architecture/guide">Flutter architecture guide</a></li><li>My own articles: <a href="https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/">Riverpod Architecture</a> and <a href="https://codewithandrea.com/articles/comparison-flutter-app-architectures/">Comparing Flutter Architectures</a></li></ul><p>Most recently, <strong>Thomas Burkhart</strong> also decided to share his own take on the topic.</p><p>His article reiterates the benefits of <strong>layered architecture</strong>, offers a summary of approaches such as <strong>MVVM</strong>, <strong>MVC</strong>, <strong>MVU</strong> (none of which are particularly suited for Flutter), and introduces a pragmatic approach that has worked well for him.</p><p>Whether you're a seasoned Flutter developer or just starting out, this is well worth a read:</p><ul><li><a href="https://blog.burkharts.net/practical-flutter-architecture">Practical Flutter Architecture</a></li></ul><p><strong>Bonus:</strong> He also forked the <a href="https://github.com/flutter/samples/tree/main/compass_app">Flutter compass app</a> and refactored it using his approach:</p><ul><li><a href="https://github.com/escamoteur/compass_fork">Compass Fork (Refactored)</a></li></ul><blockquote><p>Like Thomas, I also wasn’t fully satisfied with the official compass app and I've been meaning to refactor it for some time. Hopefully, I'll be able to revisit this soon!</p></blockquote><h3><a id="📝 10-lesser-known-dart-&-flutter-features-you-should-use" href="#📝 10-lesser-known-dart-&-flutter-features-you-should-use">📝 10 Lesser-Known Dart & Flutter Features You Should Use</a></h3><p>As you advance in Flutter development, you'll want to make the most of the language and framework features.</p><p>In this article, <strong>Majid Hajian</strong> shares <strong>10 advanced APIs</strong> that help various aspects of your code, including:</p><ul><li><strong>Better async handling</strong></li><li><strong>Improved error handling and debugging</strong></li><li><strong>Attaching metadata to objects that can’t be subclassed</strong></li></ul><p>Each technique is backed by <strong>real-world use cases</strong>, making it easy to apply in your projects:</p><ul><li><a href="https://dcm.dev/blog/2025/02/27/ten-lesser-known-dart-flutter-functionalities/">10 Lesser-Known Dart and Flutter Functionalities You Should Start Using</a></li></ul><h3><a id="💡-new-unified-provider-syntax-for-riverpod" href="#💡-new-unified-provider-syntax-for-riverpod">💡 New Unified Provider Syntax for Riverpod</a></h3><p>While <a href="https://pub.dev/packages/flutter_riverpod">Riverpod</a> is a very popular package, not everyone is happy with the current API:</p><ul><li>❌ <strong>Too many provider types</strong></li><li>❌ <strong>Code generation is slow for large apps</strong></li></ul><p>Since <strong>Dart macros were canceled</strong> (<a href="https://medium.com/dartlang/an-update-on-dart-macros-data-serialization-06d3037d4f12">details here</a>), <strong>Rémi Rousselet</strong> went back to the drawing board and proposed a <strong>new, simplified provider syntax that eliminates code-gen</strong>.</p><p>My first impression? This is a very positive step forward.</p><p>If you use Riverpod, you can find the proposal here and share your feedback:</p><ul><li><a href="https://github.com/rrousselGit/riverpod/issues/4008">Unified syntax for providers, without code-generation</a></li></ul><blockquote><p>While the proposal is still in the early stages, it is very detailed and gives us a glimpse of what Riverpod 3.0 might look like.</p></blockquote><h2><a id="latest-from-code-with-andrea" href="#latest-from-code-with-andrea">Latest from Code with Andrea</a></h2><p>Since last month, I have published:</p><ul><li><strong>New Course Modules</strong> → <a href="https://pro.codewithandrea.com/flutter-in-production/13-shorebird/01-intro">Shorebird</a> &amp; <a href="https://pro.codewithandrea.com/flutter-in-production/14-screenshots/01-intro">Screenshot Automation</a></li><li><strong>10 New Flutter Tips</strong> → <a href="https://codewithandrea.com/tips/">Check them out</a></li><li><strong>New Article about Refactoring</strong> 👇</li></ul><h3><a id="📝 why-you-should-refactor-before-adding-new-features" href="#📝 why-you-should-refactor-before-adding-new-features">📝 Why You Should Refactor Before Adding New Features</a></h3><p>Refactoring is about <strong>making future changes easier</strong>. When done right:</p><ul><li><strong>New features integrate smoothly</strong> instead of feeling bolted on.</li><li><strong>Tech debt is reduced</strong>, making maintenance less painful.</li><li><strong>Code reviews are faster</strong>, since changes are smaller and more focused.</li></ul><p>In this article, I share a real-world example of how refactoring has helped me ship a new feature while keeping my codebase flexible and maintainable:</p><ul><li><a href="https://codewithandrea.com/articles/why-refactor-before-new-features/">Why You Should Refactor Before Adding New Features</a></li></ul><h2><a id="some-thoughts-on-vibe-coding-with-ai" href="#some-thoughts-on-vibe-coding-with-ai">Some Thoughts on Vibe Coding with AI</a></h2><p>The term <strong>vibe coding</strong> was recently <a href="https://x.com/karpathy/status/1886192184808149383">coined by Andrej Karpathy</a> (a leading AI researcher and engineer), and it now keeps popping up in my feeds.</p><p>The idea?</p><p><strong>AI-driven development</strong> where LLM-based IDEs (like <a href="https://www.cursor.com/en">Cursor</a>) do all the work for you, to an extent where:</p><blockquote><p>"You fully give in to the vibes, embrace exponentials, and forget that the code even exists." — Andrej Karpathy</p></blockquote><p>At first, I assumed that in the hands of inexperienced devs, <strong>this would lead to catastrophic security flaws</strong>. But I also see the appeal—especially for <strong>rapid prototyping and non-production apps</strong>.</p><p>If you’re curious, here are some thought-provoking takes on vibe coding:</p><ul><li><a href="https://simonwillison.net/2025/Mar/19/vibe-coding/">Not all AI-assisted programming is vibe coding (but vibe coding rocks)</a></li><li><a href="https://cendyne.dev/posts/2025-03-19-vibe-coding-vs-reality.html">"Vibe Coding" vs Reality</a></li><li><a href="https://nmn.gl/blog/vibe-coding-fantasy">Vibe Coding is a Dangerous Fantasy</a></li></ul><p>Have you tried vibe coding with Flutter? I’d love to hear your thoughts!</p><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>With my <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> course nearing completion, I’m excited to experiment with <strong>new ideas and projects</strong>.</p><p>Who knows—maybe I’ll even try <strong>vibe coding</strong> myself and see how far I can take it before everything falls apart. 😅</p><p>Stay tuned—and as always, happy coding! 🎉</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/hot-reload-flutter-web/</guid><title>Hot Reload on Flutter web (3.32)</title><description>To enable this, switch to Flutter 3.32 and run your app with --web-experimental-hot-reload</description><link>https://codewithandrea.com/tips/hot-reload-flutter-web/</link><pubDate>Thu, 20 Mar 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Hot-reload is available on Flutter web! ⚡️</p><p>To enable it:</p><ul><li>Upgrade to Flutter 3.32 stable</li><li>Run your app with <code>--web-experimental-hot-reload</code></li></ul><p>For the best experience in VSCode:</p><ul><li>Enable hot reload on save</li><li>Add a web launch configuration 👇</li></ul><figure><picture><img class="bottom-40px" alt="Hot Reload on Flutter web" srcset="images/239.gif 2x"/></picture></figure><p>Check this post for all the details:</p><ul><li><a href="https://www.reddit.com/r/FlutterDev/comments/1jedakr/try_out_hot_reload_on_the_web_with_the_latest/">Try out hot reload on the web with the latest Flutter beta</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/uploading-screenshots-fastlane/</guid><title>Uploading Screenshots with Fastlane</title><description>Instead of taking screenshots manually for each device &amp; language, you can automate it with Maestro! Here's how.</description><link>https://codewithandrea.com/tips/uploading-screenshots-fastlane/</link><pubDate>Wed, 19 Mar 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Rather than uploading your app store screenshots manually, you can automate the process with Fastlane!</p><p>Here's how 👇</p><figure><picture><source srcset="images/238.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Uploading Screenshots with Fastlane" srcset="images/238.png 2x"/></picture></figure><p>Before uploading to the App Store, fastlane will show a preview so you can check if everything looks good.</p><figure><picture><img class="bottom-40px" alt="Fastlane App Store preview showing the metadata and screenshots" srcset="images/238.2.png 2x"/></picture></figure><p>You can skip this by passing <code>force: true</code> to the <code>upload_to_app_store</code> lane (useful for non-interactive CI workflows).</p><hr><h3><a id="video-preview" href="#video-preview">Video Preview</a></h3><p>Here's a preview showing how to export screenshots from Figma and upload them with Fastlane, in less than one minute:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="NpWp33_uakg"></div></div><hr><h3><a id="flutter-in-production" href="#flutter-in-production">Flutter in Production</a></h3><p>My latest course includes a whole module about screenshot automation, covering:</p><ul><li>✅ Tips for better screenshots</li><li>✅ Capturing screenshots with Maestro</li><li>✅ Editing them with Figma</li><li>✅ Uploading them with Fastlane (locally &amp; on CI)</li></ul><p>Learn more here:</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/semantics-identifiers-ui-testing/</guid><title>Using Semantics Identifiers for UI Testing</title><description>Instead of taking screenshots manually for each device &amp; language, you can automate it with Maestro! Here's how.</description><link>https://codewithandrea.com/tips/semantics-identifiers-ui-testing/</link><pubDate>Tue, 18 Mar 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>UI testing frameworks like Maestro and Appium rely on Flutter’s <strong>semantics tree</strong> to interact with your app UI.</p><p>If your app is localized, you can make your UI tests more robust by adding a <strong>semantic identifier</strong> to your widgets:</p><p>Here's how. 👇</p><figure><picture><source srcset="images/237.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Using Semantics Identifiers for UI Testing" srcset="images/237.png 2x"/></picture></figure><p>If you want to dig deeper, here's an excellent article explaining how Flutter's semantics tree maps with the native accessibility tree:</p><ul><li><a href="https://www.maestro.dev/blog/the-power-of-open-source-making-maestro-work-better-with-flutter">The power of open-source. Making Maestro work better with Flutter</a></li></ul><p>To learn more about UI testing, screenshot generation, and more, check the latest module in my Flutter in Production course:</p><ul><li><a href="https://pro.codewithandrea.com/flutter-in-production/14-screenshots/01-intro">Introduction to Automated Screenshot Generation</a></li></ul>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/automated-screenshot-generation-maestro/</guid><title>Automated Screenshot Generation with Maestro</title><description>Instead of taking screenshots manually for each device &amp; language, you can automate it with Maestro! Here's how.</description><link>https://codewithandrea.com/tips/automated-screenshot-generation-maestro/</link><pubDate>Mon, 17 Mar 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Instead of taking screenshots manually for each device &amp; language, you can automate it with Maestro!</p><p>Here's a video preview:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="oUWf9tYc4kQ"></div></div><p>How to use this in practice?</p><p>Simply write a YAML file to define how Maestro should interact with your app UI and call <code>takeScreenshot</code> as needed.</p><p>Super easy—no test harness required! 🚀</p><figure><picture><source srcset="images/236.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Automated Screenshot Generation with Maestro" srcset="images/236.png 2x"/></picture></figure><h3><a id="getting-started" href="#getting-started">Getting Started</a></h3><p>To get started, check the official docs:</p><ul><li><a href="https://docs.maestro.dev/">What is Maestro?</a></li></ul><h3><a id="flutter-in-production-course" href="#flutter-in-production-course">Flutter in Production course</a></h3><p>My latest course includes a whole module about screenshot automation, covering:</p><ul><li>✅ Tips for better screenshots</li><li>✅ Capturing screenshots with Maestro</li><li>✅ Editing them with Figma</li><li>✅ Uploading to the stores with Fastlane (locally &amp; on CI)</li></ul><p>Learn more here:</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/android-demo-mode-for-screenshots/</guid><title>Android Demo Mode for Better Screenshots</title><description>Here's a handy command to override the Android status bar UI before taking screenshots for your Play Store listings.</description><link>https://codewithandrea.com/tips/android-demo-mode-for-screenshots/</link><pubDate>Fri, 14 Mar 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If you want to take better screenshots for your Play Store listings, you can enable Demo Mode in the Android settings.</p><p>Here's how to enable this. 👇</p><figure><picture><source srcset="images/235.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Android Demo Mode for Better Screenshots" srcset="images/235.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/ios-status-bar-for-screenshots/</guid><title>iOS Status Bar Tip for Better Screenshots</title><description>Here's a handy command to override the iOS status bar UI before taking screenshots.</description><link>https://codewithandrea.com/tips/ios-status-bar-for-screenshots/</link><pubDate>Thu, 13 Mar 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>You can use this command to clean up the iOS status bar before taking screenshots:</p><pre><code><div class="highlight"><span></span>xcrun simctl status_bar booted override --time 09:41 --batteryState charged --batteryLevel 100 --cellularBars 4
</div></code></pre><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="iOS Status Bar tip for Better Screenshots" srcset="images/twitter-card.png 2x"/></picture></figure><p>Then, you can take screenshots manually (<code>CMD+S</code> on macOS), or automate the process with <a href="https://docs.maestro.dev/">Maestro</a>.</p><p>Once you're done, reset the status bar like this:</p><pre><code><div class="highlight"><span></span>xcrun simctl status_bar booted clear
</div></code></pre><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/gradle-kotlin-dsl/</guid><title>Gradle Kotlin DSL (Flutter 3.29)</title><description>Apps created with Flutter 3.29 use the new Gradle Kotlin DSL on Android. Some CLI tools don't support the new syntax, yet.</description><link>https://codewithandrea.com/tips/gradle-kotlin-dsl/</link><pubDate>Mon, 10 Mar 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>New apps created with Flutter 3.29 use the new Gradle Kotlin DSL on Android.</p><p>This may affect you if you rely on CLI tools that don't support the new syntax.</p><p>Here's what you need to know. 👇</p><figure><picture><source srcset="images/233.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Gradle Kotlin DSL (Flutter 3.29)" srcset="images/233.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/flutter-run-route/</guid><title>The flutter run --route argument</title><description>Pub.dev has a new chart that lets you see package download counts by version (major, minor, patch). Here's where to find it.</description><link>https://codewithandrea.com/tips/flutter-run-route/</link><pubDate>Tue, 4 Mar 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>You can run <code>flutter run --route /path/to/route</code> to start your Flutter app from a specific route.</p><p>This works with named routes (nav 1.0) and the router APIs (nav 2.0).</p><p>Super useful for debugging nested routes when you need to hot-restart.</p><figure><picture><source srcset="images/232.webp 2x" type="image/webp"/><img class="bottom-40px" alt="The flutter run --route argument" srcset="images/232.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/articles/why-refactor-before-new-features/</guid><title>Why You Should Refactor Before Adding New Features</title><description>A real-world case study about ephemeral vs application state, navigation, scroll controllers, and how refactoring can help you ship new features smoothly.</description><link>https://codewithandrea.com/articles/why-refactor-before-new-features/</link><pubDate>Fri, 28 Feb 2025 02:00:00 +0100</pubDate><content:encoded><![CDATA[<p>We've all been there: you're excited to build a new feature, but as soon as you dive in, you realize the existing code is a mess.</p><p>At this point, you have two choices:</p><ol><li><strong>Charge ahead</strong>—make it work by building on top of the existing code.</li><li><strong>Take a step back</strong>—clean things up first, then implement the feature with confidence.</li></ol><p>Option 1 might feel faster, but it usually leads to disaster:</p><ul><li>Your new code works… kind of. But it doesn't <em>feel</em> right—hello, tech debt.</li><li>You’re unsure if everything plays nicely together, and subtle bugs creep in.</li><li>Your PR balloons in scope, making reviews painful and frustrating.</li></ul><p>Meanwhile, your boss (who is very easily impressed by those wonderful AI agents) is breathing down your neck and questioning your abilities. 😭😱</p><figure><picture><img class="bottom-40px" alt="The boss from Office Space (movie)" srcset="images/office-space.gif 1x"/></picture></figure><p>Before you quit tech and become a farmer (no shame in that), let me share a story about refactoring—one that helped me ship a feature smoothly instead of wrestling with broken code.</p><p>As part of this, we'll explore a real-world example of:</p><ul><li>ephemeral vs application state in Flutter</li><li>how to manage navigation and communication across screens</li><li>how to blend reactive updates with imperative APIs (as needed by ScrollControllers)</li></ul><h2><a id="a-tale-about-refactoring" href="#a-tale-about-refactoring">A Tale About Refactoring</a></h2><p>Recently, I decided to add wide-screen support to my <a href="https://fluttertips.dev/">Flutter tips app</a>:</p><figure><picture><img class="bottom-12px" alt="Flutter tips app preview" srcset="images/flutter-tips-page-view.gif 1x"/></picture><figcaption><center><i>Flutter tips app preview</i></center></figcaption></figure><p>That is, given these two screens that were initially designed for mobile:</p><figure><picture><source srcset="images/iphone-navigation.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Flutter tips app on mobile, with two screens for content and tips navigation" srcset="images/iphone-navigation.png 2x"/></picture><figcaption><center><i>Flutter tips app on mobile, with two screens for content and tips navigation</i></center></figcaption></figure><p>I wanted to show them side by side and enable seamless navigation on iPad:</p><figure><picture><source srcset="images/ipad-navigation.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Flutter tips app on iPad, with content and navigation side by side" srcset="images/ipad-navigation.png 2x"/></picture><figcaption><center><i>Flutter tips app on iPad, with content and navigation side by side</i></center></figcaption></figure><p>Making this UI adaptive is quite easy. All you need is:</p><ul><li>A responsive layout that uses a <a href="https://codewithandrea.com/articles/flutter-responsive-layouts-split-view-drawer-navigation/">split-view</a> on wide screens, and regular push/pop navigation on mobile.</li><li>Some conditional logic based on <code>MediaQuery.sizeOf(context)</code> or <code>LayoutBuilder</code> to choose between the split-view and single-pane layout depending on a layout breakpoint.</li></ul><p>When given a task like this, it's tempting to just add the new feature as a single PR.</p><p>But, as we're about to find out, there's more than meets the eye.</p><h2><a id="ephemeral-state-application-state-and-navigation" href="#ephemeral-state-application-state-and-navigation">Ephemeral State, Application State, and Navigation</a></h2><p>To set the stage, let's revisit the mobile-only version of the app:</p><figure><picture><source srcset="images/iphone-navigation.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Flutter tips app on mobile, with two screens for content and tips navigation" srcset="images/iphone-navigation.png 2x"/></picture></figure><p>Based on the screenshots above, can you guess how I was keeping track of the <strong>currently selected tip index</strong>?</p><p>Of all possible options, I decided to go with the good old <code>setState</code> approach.</p><p>That is, I had a <code>_MarkdownTipsPageViewState</code> class that I was using for:</p><ul><li>storing the <code>_currentTipIndex</code> as <a href="https://docs.flutter.dev/data-and-backend/state-mgmt/ephemeral-vs-app#ephemeral-state"><strong>ephemeral state</strong></a></li><li>updating this state via the <code>_updateTipIndex</code> method</li></ul><pre><code><div class="highlight"><span></span><span class="kd">class</span><span class="w"> </span><span class="nc">_MarkdownTipsPageViewState</span><span class="w"> </span><span class="kd">extends</span><span class="w"> </span><span class="n">ConsumerState</span><span class="o">&lt;</span><span class="n">MarkdownTipsPageView</span><span class="o">&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="c1">// both initialized in initState</span>
<span class="w">  </span><span class="kd">late</span><span class="w"> </span><span class="n">PageController</span><span class="w"> </span><span class="n">_pageController</span><span class="p">;</span>
<span class="w">  </span><span class="kd">late</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">_currentTipIndex</span><span class="p">;</span>

<span class="w">  </span><span class="kt">void</span><span class="w"> </span><span class="n">_updateTipIndex</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">tipIndex</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">setState</span><span class="p">(()</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="n">_currentTipIndex</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">tipIndex</span><span class="p">;</span>
<span class="w">      </span><span class="c1">// Used to store the tip index in shared preferences (but not too relevant here)</span>
<span class="w">      </span><span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">currentTipIndexStoreProvider</span><span class="p">.</span><span class="n">notifier</span><span class="p">).</span><span class="n">setTipIndex</span><span class="p">(</span><span class="n">tipIndex</span><span class="p">);</span>
<span class="w">    </span><span class="p">});</span>
<span class="w">  </span><span class="p">}</span>
<span class="w">  </span><span class="p">...</span>
<span class="p">}</span>
</div></code></pre><p>In the <code>build</code> method, I had an <code>IconButton</code> with a callback for pushing the <code>tipsList</code> route, and updating the state if a new tip index was returned:</p><pre><code><div class="highlight"><span></span><span class="n">IconButton</span><span class="p">(</span>
<span class="w">  </span><span class="nl">icon:</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="n">Icon</span><span class="p">(</span><span class="n">Icons</span><span class="p">.</span><span class="n">search</span><span class="p">),</span>
<span class="w">  </span><span class="nl">tooltip:</span><span class="w"> </span><span class="s1">&#39;Search tips&#39;</span><span class="p">,</span>
<span class="w">  </span><span class="nl">onPressed:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="c1">// Push the &quot;tips list&quot; route and wait for the result</span>
<span class="w">    </span><span class="kd">final</span><span class="w"> </span><span class="n">newIndex</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">await</span><span class="w"> </span><span class="n">Navigator</span><span class="p">.</span><span class="n">of</span><span class="p">(</span><span class="n">context</span><span class="p">).</span><span class="n">pushNamed</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">(</span>
<span class="w">      </span><span class="n">AppRoutes</span><span class="p">.</span><span class="n">tipsList</span><span class="p">,</span>
<span class="w">      </span><span class="nl">arguments:</span><span class="w"> </span><span class="n">_currentTipIndex</span><span class="p">,</span>
<span class="w">    </span><span class="p">);</span>
<span class="w">    </span><span class="c1">// If a new tip index is returned</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">newIndex</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="c1">// Update the state and jump to the new page</span>
<span class="w">      </span><span class="n">_updateTipIndex</span><span class="p">(</span><span class="n">newIndex</span><span class="p">);</span>
<span class="w">      </span><span class="n">_pageController</span><span class="p">.</span><span class="n">jumpToPage</span><span class="p">(</span><span class="n">currentPage</span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">},</span>
<span class="p">)</span>
</div></code></pre><p>Accordingly, my <code>TipsListViewScreen</code> had an <code>onSelected</code> callback that was used to return the selected tip index via <code>Navigator.pop</code>:</p><pre><code><div class="highlight"><span></span><span class="n">TipsListViewScreen</span><span class="p">(</span>
<span class="w">  </span><span class="nl">flutterTips:</span><span class="w"> </span><span class="n">tips</span><span class="p">,</span>
<span class="w">  </span><span class="nl">initialTipIndex:</span><span class="w"> </span><span class="n">initialTipIndex</span><span class="p">,</span>
<span class="w">  </span><span class="c1">// Pop the index so the parent route can update the state</span>
<span class="w">  </span><span class="nl">onSelected:</span><span class="w"> </span><span class="p">(</span><span class="n">index</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">Navigator</span><span class="p">.</span><span class="n">of</span><span class="p">(</span><span class="n">context</span><span class="p">).</span><span class="n">pop</span><span class="p">(</span><span class="n">index</span><span class="p">),</span>
<span class="p">)</span>
</div></code></pre><p>Here's a diagram showing the interaction between the two screens:</p><figure><picture><source srcset="images/page-view-list-view-ephemeral.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Interaction between the page view and list view screens" srcset="images/page-view-list-view-ephemeral.png 2x"/></picture><figcaption><center><i>Interaction between the page view and list view screens</i></center></figcaption></figure><p>In summary:</p><ul><li>The <code>_currentTipIndex</code> variable is the <strong>main source of truth</strong> that determines the currently selected tip.</li><li>It is stored in the <code>_MarkdownTipsPageViewState</code> class, but it is also passed to the <code>TipsListViewScreen</code> via the <code>initialTipIndex</code> argument and updated via the <code>onSelected</code> callback.</li></ul><p>This works <em>just fine</em> on mobile, but let's now consider the wide-screen version of the app:</p><figure><picture><source srcset="images/ipad-navigation.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Flutter tips app on iPad, with content and navigation side by side" srcset="images/ipad-navigation.png 2x"/></picture></figure><p>With the setup described above, the app would behave like this:</p><ul><li>When swiping between pages, the <code>_currentTipIndex</code> would be updated in the page view screen, but this change would <strong>not</strong> be reflected in the list view screen.</li><li>Calling <code>Navigator.pop</code> from the list view would break the navigation completely (in split view mode, we're already at the root of the navigation stack).</li></ul><p>Such inferior and broken UX is absolutely unacceptable. So how did I fix it?</p><h2><a id="from-ephemeral-to-application-state" href="#from-ephemeral-to-application-state">From Ephemeral to Application State</a></h2><p>The core issue was that my <code>_currentTipIndex</code> lived as <strong>ephemeral state</strong> inside a widget, making cross-screen updates clunky.</p><p>Here's a better approach:</p><ul><li>Promote the current tip index to <strong>application state</strong></li><li>Use a <code>ValueNotifier</code> or Riverpod's own <code>Notifier</code> class to store it</li></ul><p>This way, both pages can <strong>mutate</strong> the notifier and <strong>update</strong> the UI when needed. Here's an interactive demo of the solution in action:</p><div class="spacer-20px"></div><div class="iframe-small-iphone">
<iframe class="iframe-responsive" src="https://codewithandrea.github.io/c007_page_view_sync_list_view_solution/" title="Page View Sync List View Solution" width="700" height="500" frameborder="0"></iframe> 
</div><div class="spacer-12px"></div><figcaption>Page View - List View syncing demo (looks best on desktop)</figcaption><div class="spacer-20px"></div><p>It works like this:</p><ul><li>When you swipe between pages, the correct item is highlighted and centered in the list.</li><li>When you select an item from the list, the page view immediately jumps to the corresponding tip.</li></ul><p>Much better!</p><blockquote><p>You can make this work by using <code>ValueNotifier</code> or Riverpod's own <code>Notifier</code> class. I won't cover all the details here, but you can take this challenge as an exercise if you want: <a href="https://pro.codewithandrea.com/flutter-ui-challenges/007-page-view-sync-list-view/01-intro">Page View / List View syncing challenge</a></p></blockquote><h2><a id="what-about-navigation?" href="#what-about-navigation?">What About Navigation?</a></h2><p>On mobile, we can use the <strong>search icon</strong> to navigate to the tips list screen:</p><figure><picture><source srcset="images/iphone-navigation.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Flutter tips app on mobile, with two screens for content and tips navigation" srcset="images/iphone-navigation.png 2x"/></picture></figure><p>But on iPad, this icon is hidden since both pages are visible side by side:</p><figure><picture><source srcset="images/ipad-navigation.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Flutter tips app on iPad, with content and navigation side by side" srcset="images/ipad-navigation.png 2x"/></picture></figure><p>To achieve this, I ended up using a small <code>LayoutBreakpoints</code> helper class:</p><pre><code><div class="highlight"><span></span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">LayoutBreakpoints</span><span class="p">.</span><span class="n">isSplitView</span><span class="p">(</span><span class="n">screenSize</span><span class="p">))</span>
<span class="w">  </span><span class="n">IconButton</span><span class="p">(</span>
<span class="w">      </span><span class="nl">icon:</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="n">Icon</span><span class="p">(</span><span class="n">Icons</span><span class="p">.</span><span class="n">search</span><span class="p">),</span>
<span class="w">      </span><span class="nl">tooltip:</span><span class="w"> </span><span class="s1">&#39;Search tips&#39;</span><span class="p">,</span>
<span class="w">      </span><span class="c1">// Push the tips list screen (application state is updated elsewhere)</span>
<span class="w">      </span><span class="nl">onPressed:</span>
<span class="w">          </span><span class="p">()</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">Navigator</span><span class="p">.</span><span class="n">of</span><span class="p">(</span><span class="n">context</span><span class="p">).</span><span class="n">pushNamed</span><span class="p">(</span><span class="n">AppRoutes</span><span class="p">.</span><span class="n">tipsList</span><span class="p">),</span>
<span class="w">    </span><span class="p">),</span><span class="w">            </span>
</div></code></pre><p>Note how the <code>onPressed</code> callback is no longer used to update the tip index.</p><p>In fact, we can now do that directly on the <code>onSelected</code> callback:</p><pre><code><div class="highlight"><span></span><span class="n">TipsListViewScreen</span><span class="p">(</span>
<span class="w">  </span><span class="nl">flutterTips:</span><span class="w"> </span><span class="n">tips</span><span class="p">,</span>
<span class="w">  </span><span class="nl">currentTipIndex:</span><span class="w"> </span><span class="n">currentTipIndex</span><span class="p">,</span>
<span class="w">  </span><span class="nl">onSelected:</span><span class="w"> </span><span class="p">(</span><span class="n">index</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="c1">// Update the tip index</span>
<span class="w">    </span><span class="n">ref</span>
<span class="w">        </span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">tipUpdateEventNotifierProvider</span><span class="p">.</span><span class="n">notifier</span><span class="p">)</span>
<span class="w">        </span><span class="p">.</span><span class="n">updateTipSelectedFromList</span><span class="p">(</span><span class="n">index</span><span class="p">);</span>
<span class="w">    </span><span class="c1">// On mobile, go back to the tips page view</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">LayoutBreakpoints</span><span class="p">.</span><span class="n">isSplitView</span><span class="p">(</span><span class="n">screenSize</span><span class="p">))</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="n">Navigator</span><span class="p">.</span><span class="n">of</span><span class="p">(</span><span class="n">context</span><span class="p">).</span><span class="n">pop</span><span class="p">();</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">},</span>
<span class="p">)</span>
</div></code></pre><p>The fundamental idea is that neither screen <strong>owns</strong> the application state, but both screens can <strong>mutate</strong> it and <strong>rebuild / update their UI</strong> when it changes:</p><figure><picture><source srcset="images/page-view-list-view-application.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Interaction between the page view, list view, and current tip index notifier" srcset="images/page-view-list-view-application.png 2x"/></picture><figcaption><center><i>Interaction between the page view, list view, and current tip index notifier</i></center></figcaption></figure><h2><a id="considerations-about-scrollcontrollers" href="#considerations-about-scrollcontrollers">Considerations About ScrollControllers</a></h2><p>Promoting the tip index to application state is a good step forward, but it's not enough:</p><ul><li>On the page view screen, the currently selected page is controlled by a <code>PageController</code>, so an explicit call to <code>jumpToPage</code> is needed to ensure it stays in sync with the tip index.</li><li>On the list view screen, the currect offset is controlled by a <code>AutoScrollController</code> (from the <a href="https://pub.dev/packages/scroll_to_index"><code>scroll_to_index</code></a> package). This controller needs to be updated explicitly when the tip index changes.</li></ul><p>Implementing state updates and ensuring these were correctly reflected in the UI required some fine-tuning. But eventually, I managed to get the app to work as expected.</p><blockquote><p><strong>Hint</strong>: you can register a <code>ValueNotifier</code> <strong>listener</strong> to update the <code>PageController</code> or <code>AutoScrollController</code> when the tip index changes, as explained here: <a href="https://codewithandrea.com/tips/side-effects-value-notifier/">Side Effects with ValueNotifier</a>.</p></blockquote><p>Time for some advice about refactoring code. 👇</p><h2><a id="refactor-first-then-add-features" href="#refactor-first-then-add-features">Refactor First, Then Add Features</a></h2><p>My initial goal was to add responsive support to my Flutter tips app:</p><figure><picture><source srcset="images/ipad-navigation.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Flutter tips app on iPad, with content and navigation side by side" srcset="images/ipad-navigation.png 2x"/></picture></figure><p>Rather than tackling everything at once, I took a step back and did a <strong>first round of refactoring</strong>:</p><ul><li>moved from ephemeral to application state (via notifier)</li><li>used the notifier to trigger state updates rather than relying on push/pop navigation</li><li>prepared the ground for the new feature</li></ul><p>After verifying that the updates worked as expected, I landed this change.</p><p>Then, <strong>in a second PR</strong>, I implemented the actual feature:</p><ul><li>wide-screen support via split view</li><li>a proper event system for syncing the scroll position between screens</li><li>final tweaks</li></ul><p>By refactoring first, I was able to revisit the initial assumptions and choose a more suitable state management approach. But if I had tried to do everything at once, I would have ended up with workarounds and ugly code.</p><h2><a id="the-case-for-refactoring" href="#the-case-for-refactoring">The Case for Refactoring</a></h2><p>Refactoring is about <strong>making future changes easier</strong>. When done right:</p><ul><li><strong>New features integrate smoothly</strong> instead of feeling bolted on.</li><li><strong>Tech debt is reduced</strong>, making maintenance less painful.</li><li><strong>Code reviews are faster</strong>, since changes are smaller and more focused.</li></ul><p>Some even argue that refactoring should be part of your definition of done for every feature.</p><p>But let's face it: as software evolves, requirements change and your initial assumptions may no longer hold. When that happens, refactoring is a good way to improve the existing foundations and make way for new code.</p><p>Of course, refactoring can be done with more confidence if you have automated tests that help you catch regressions. This is especially important when you work on a shared codebase with other developers.</p><p>But for today, here's the key takeaway:</p><p><strong>→ If you're fighting against existing code when making changes, stop and refactor first.</strong></p><p>You’ll save time, avoid frustration, and ship with confidence.</p><h2><a id="conclusion" href="#conclusion">Conclusion</a></h2><p>Adding new features to a messy codebase is like building a house on a shaky foundation—it might stand for a while, but cracks will show. Refactoring first ensures that your code remains flexible, maintainable, and ready for future improvements.</p><p>So next time you're about to add a feature, ask yourself: <strong>Is my code ready for this?</strong> If not, take a step back, clean it up, and then move forward. Your future self (and your team) will thank you.</p><p>P.S. You can try the updated Flutter Tips app with responsive layouts here:</p><ul><li><a href="https://app.fluttertips.dev/">Flutter Tips App (on web)</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/download-counts-by-version/</guid><title>Downloads Count by Version on Pub.dev</title><description>Pub.dev has a new chart that lets you see package download counts by version (major, minor, patch). Here's where to find it.</description><link>https://codewithandrea.com/tips/download-counts-by-version/</link><pubDate>Fri, 28 Feb 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>An improved weekly downloads chart was recently added on <a href="https://pub.dev/">pub.dev</a>.</p><p>This lets you see package download counts by version (major, minor, patch).</p><p>To view it, click on the <strong>Scores</strong> tab and scroll to the bottom (took me a while to find it).</p><figure><picture><source srcset="images/231.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Downloads Count by Version on Pub.dev" srcset="images/231.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/side-effects-value-notifier/</guid><title>Side Effects with ValueNotifier</title><description>By registering a ValueNotifier listener, you can perform side effects such as updating controllers, showing dialogs, and performing navigation.</description><link>https://codewithandrea.com/tips/side-effects-value-notifier/</link><pubDate>Thu, 27 Feb 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>By registering a <code>ValueNotifier</code> listener, you can perform various side effects such as:</p><ul><li>Updating a <code>ScrollController</code>, <code>PageController</code>, <code>AnimationController</code>, etc.</li><li>Showing a dialog</li><li>Navigating to another route</li></ul><p>Note: This works with <code>ChangeNotifier</code>, too. 💡</p><figure><picture><source srcset="images/230.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Side Effects with ValueNotifier" srcset="images/230.png 2x"/></picture></figure><p><strong>Important</strong>: side effects should <strong>never</strong> be performed from the <code>build</code> method.</p><p>To learn more and avoid common mistakes, check this article:</p><ul><li><a href="https://codewithandrea.com/articles/side-effects-flutter/">Side Effects in Flutter: What They Are and How to Handle Them</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/february-2025/</guid><title>February 2025: Flutter 3.29, Dart 3.7, Shorebird &amp; Jaspr Updates, New Formatting Style, TextFormField Mistakes</title><description>Also included: Flutter rendering changes, discontinued official packages, Dart macros update,  new Flutter UI challenges from Code with Andrea.</description><link>https://codewithandrea.com/newsletter/february-2025/</link><pubDate>Mon, 24 Feb 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>February is a Flutter release month, and there's a lot to cover! In this edition, we'll explore:</p><ul><li>The latest <a href="https://medium.com/flutter/whats-new-in-flutter-3-29-f90c380c2317">Flutter 3.29</a> and <a href="https://medium.com/flutter/whats-new-in-flutter-3-29-f90c380c2317">Dart 3.7</a> releases</li><li><strong>Flutter rendering changes</strong> (moving to the platform's main thread)</li><li>The new <strong>Dart formatting style</strong> and how it affects your projects</li><li>Why <strong>Dart macros</strong> are no longer coming—and what else is in focus</li><li>Official <strong>Flutter packages being discontinued</strong></li><li>Key updates from <strong>Shorebird &amp; Jaspr</strong></li><li>Common <strong>TextFormField mistakes</strong> in Flutter apps</li><li>The latest from <strong>Code with Andrea</strong></li></ul><p>Let's dive in!</p><h2><a id="what’s-new-in-flutter-329" href="#what’s-new-in-flutter-329">What’s new in Flutter 3.29</a></h2><p>Every Flutter release brings many improvements, and 3.29 is no exception. Expect refinements to Cupertino and Material widgets, along with DevTools enhancements and Impeller engine optimizations.</p><p>Read the official announcement for the full details:</p><ul><li><a href="https://medium.com/flutter/whats-new-in-flutter-3-29-f90c380c2317">What’s new in Flutter 3.29</a></li></ul><p>Here are some important highlights. 👇</p><h3><a id="⚠️-flutter-rendering-moves-to-the-platforms-main-thread" href="#⚠️-flutter-rendering-moves-to-the-platforms-main-thread">⚠️ Flutter rendering moves to the platform's main thread</a></h3><p>Previously, Flutter ran Dart code on a separate UI thread, which made it harder to call into platform code. Flutter now runs Dart code on the platform's <strong>main thread</strong> on Android and iOS. This enables more efficient platform interop through direct synchronous calls.</p><p>To learn more about this change, check out this detailed issue about <a href="https://github.com/flutter/flutter/issues/150525">merging the platform and UI thread</a>, along with the most <a href="https://github.com/flutter/flutter/issues/150525#issuecomment-2652547816">recent comments by Eric Seidel</a> and <a href="https://github.com/flutter/flutter/issues/150525#issuecomment-2653005061">Matej Knopp</a>.</p><p>But does this change affect existing apps? An issue about <a href="https://github.com/flutter/flutter/issues/163429">slowdowns and jank during scrolling</a> was already reported and <a href="https://github.com/flutter/flutter/issues/163429#issuecomment-2670386843">fixed</a> on the master channel. To avoid surprises, consider waiting for a hotfix before upgrading to Flutter 3.29 in production.</p><h3><a id="⚠️-breaking-changes-and-deprecations" href="#⚠️-breaking-changes-and-deprecations">⚠️ Breaking changes and deprecations</a></h3><p>In an effort to focus on the core framework, the Flutter team is discontinuing support for several official packages on April 30th, and the community is encouraged to fork and maintain these.</p><p>Details are in this umbrella issue:</p><ul><li><a href="https://github.com/flutter/flutter/issues/162960">☂️ Packages planned to be discontinued</a></li></ul><h2><a id="announcing-dart-37" href="#announcing-dart-37">Announcing Dart 3.7</a></h2><p>Here's what's new in Dart:</p><ul><li><strong>New Formatting Style</strong>: If you opt in to Dart 3.7 in your <code>pubspec.yaml</code> file, a new formatting style will be applied to your code. Since this affects all developers, I've written a <a href="https://codewithandrea.com/articles/new-formatting-style-dart-3-7/">detailed article</a> to help you navigate the changes.</li></ul><ul><li><strong>Wildcard variables</strong>: The <code>_</code> symbol is now a proper wildcard, meaning you can use it multiple times when declaring <em>unused</em> parameters, rather than writing <code>_</code>, <code>__</code>, <code>___</code> to avoid name collisions.</li></ul><ul><li><strong>Deprecated JS legacy libraries</strong>:<ul><li>These libraries are now deprecated: <code>dart:html</code>, <code>dart:indexed_db</code>, <code>dart:js</code>, <code>dart:js_util</code>, <code>dart:web_audio</code>, <code>dart:web_gl</code>.</li><li>Instead, use <code>dart:js_interop</code> and <code>package:web</code> going forward.</li></ul></li></ul><ul><li><strong>New pub.dev features</strong>: <a href="https://pub.dev/">Pub.dev</a> has been updated with dark mode, a new weekly downloads chart (located under the "Scores" tab), and a new <a href="https://pub.dev/topics">topics page</a>.</li></ul><p>For all the details, check out the official announcement:</p><ul><li><a href="https://medium.com/dartlang/announcing-dart-3-7-bf864a1b195c">Announcing Dart 3.7</a></li></ul><h3><a id="update-on-macros" href="#update-on-macros">Update on Macros</a></h3><p>Back in January, the Dart team announced that work on the experimental macros feature has stopped due to <strong>several major technical hurdles</strong>.</p><p>While this is not the news many hoped for, it means the team can focus on shipping <a href="https://github.com/dart-lang/build/issues/3800">build_runner improvements</a> and <a href="https://github.com/dart-lang/language/blob/main/working/augmentation-libraries/feature-specification.md">augmentations</a>, bringing tangible benefits to all Flutter developers.</p><p>For more details, check out this announcement:</p><ul><li><a href="https://medium.com/dartlang/an-update-on-dart-macros-data-serialization-06d3037d4f12">An update on Dart macros &amp; data serialization</a></li></ul><p>I also recommend reading Eric Seidel's take on the topic:</p><ul><li><a href="https://shorebird.dev/blog/dart-macros/">On Focus and Dart Macros</a></li></ul><blockquote><p>Wondering if Flutter is dead already? <a href="https://isflutterdeadalready.com/">Here's the answer</a>. 😉</p></blockquote><h2><a id="flutter-videos-and-articles" href="#flutter-videos-and-articles">Flutter Videos and Articles</a></h2><h3><a id="📹-the-runtime-that-makes-dart-tick---slava-egorov" href="#📹-the-runtime-that-makes-dart-tick---slava-egorov">📹 The runtime that makes Dart tick - Slava Egorov</a></h3><p><a href="https://github.com/mraleph">Slava Egorov</a> labels himself as the "dreamer" in the Dart team, and in this talk he dives into low-level performance issues and how to apply optimisations at the compiler level.</p><p>He also goes through some little-known features of the Dart runtime, including interoperability with other languages through the <code>dart:ffi</code> interface.</p><p>While this talk may not be immediately relevant for app developers, it offers fascinating insights into the Dart runtime:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="jV2Zx3hjHPc"></div></div><h3><a id="📝-common-mistakes-with-textformfields-in-flutter" href="#📝-common-mistakes-with-textformfields-in-flutter">📝 Common mistakes with TextFormFields in Flutter</a></h3><p>This article highlights common mistakes in Flutter form design that can hurt user experience and conversion rates. It covers best practices like setting <code>textInputAction</code> for smooth field navigation, using <code>onFieldSubmitted</code> for efficient form submission, choosing the right <code>keyboardType</code>, applying <code>TextCapitalization</code>, using <code>TextInputFormatter</code> for input validation, and enabling autofill with <code>AutofillHints</code> and <code>AutofillGroup</code>.</p><p>If your forms are not up to par, this article will help you fix them:</p><ul><li><a href="https://medium.com/@pomis172/common-mistakes-with-textformfields-in-flutter-8adc8af1a9af">Common mistakes with TextFormFields in Flutter</a></li></ul><h2><a id="flutter-ecosystem-updates" href="#flutter-ecosystem-updates">Flutter Ecosystem Updates</a></h2><p>As Flutter grows, it's great to see tools in the wider ecosystem are getting better and more capable. This month, Shorebird and Jaspr have caught my attention.</p><h3><a id="🐦-shorebird" href="#🐦-shorebird">🐦 Shorebird</a></h3><p>If you want to bypass the app store review process and deploy app updates instantly, Shorebird is the right tool for you.</p><p>Most recently, the team has shipped two big features:</p><ul><li><a href="https://shorebird.dev/blog/desktop-in-production/">Shorebird works on Desktop (and everywhere Flutter does)</a></li><li><a href="https://shorebird.dev/blog/shorebird-codemagic/">Shorebird &amp; Codemagic Integration</a></li></ul><p>With the new integration, you can deploy your Shorebird-enabled Flutter app to Codemagic in 10 minutes:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="stWph9Mthts"></div></div><blockquote><p>While I only started using Shorebird recently, I'm already a big fan and I'll be adding a new Shorebird module to my [Flutter in Production] (courses/flutter-in-production/) course this week! 🚀</p></blockquote><h3><a id="🐶-jaspr" href="#🐶-jaspr">🐶 Jaspr</a></h3><p>Have you heard of Jaspr yet? It's a <strong>Dart web framework</strong> that renders <strong>actual</strong> HTML and CSS, making it ideal for websites that need to be SEO-friendly and load fast!</p><p>As of today, Jaspr supports both <strong>static sites</strong> and <strong>server-side rendering</strong>. It can also integrate with your favorite state management solution and custom backend, and it even supports Tailwind CSS!</p><p>To learn more, visit the official website:</p><ul><li><a href="https://jaspr.site/">Jaspr (The Web Framework for Dart Developers)</a></li></ul><blockquote><p>Personally, I prefer <a href="https://astro.build/">Astro</a> for my web development needs, and I don't mind working directly with HTML and JS/TS (there, I said it! 😅). But if your heart is set on the Dart ecosystem, Jaspr might be the right choice for you.</p></blockquote><h2><a id="latest-from-code-with-andrea" href="#latest-from-code-with-andrea">Latest from Code with Andrea</a></h2><p>Since the last edition, I've been busy:</p><ul><li>Added a <a href="https://pro.codewithandrea.com/flutter-in-production/12-github-actions/01-intro">new module about GitHub Actions</a> to my <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> course. 📕</li><li>Published a new article about the <a href="https://codewithandrea.com/articles/new-formatting-style-dart-3-7/">new formatting style in Dart 3.7</a>. 📝</li><li>Made some cool improvements to my <a href="https://fluttertips.dev/">Flutter Tips</a> app. 🙌</li><li>Shared two new <a href="https://pro.codewithandrea.com/flutter-ui-challenges">Flutter UI challenges</a>. 🚀</li></ul><h3><a id="📝-new-formatting-style-in-dart-37" href="#📝-new-formatting-style-in-dart-37">📝 New Formatting Style in Dart 3.7</a></h3><p>This is worth repeating: there's a <strong>new formatting style</strong> in Dart 3.7. If you opt-in, the formatter will <strong>automatically decide</strong> whether to add or remove trailing commas (no more arguing about formatting during code reviews. 🎯)</p><p>This change will affect a lot of existing code. If you want to migrate while keeping your sanity, I've got you covered:</p><ul><li><a href="https://codewithandrea.com/articles/new-formatting-style-dart-3-7/">There's a New Formatting Style in Dart 3.7 (Here's What It Means for You)</a></li></ul><h3><a id="updated-flutter-tips-app" href="#updated-flutter-tips-app">Updated Flutter Tips App</a></h3><p>This app is my little side project and I like to polish it from time to time. With the latest changes, I've added proper responsive support and the app now looks great on all devices:</p><figure><picture><source srcset="images/flutter-tips-app-responsive.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Preview of the Flutter Tips app on Flutter web" srcset="images/flutter-tips-app-responsive.png 2x"/></picture><figcaption><center><i>Preview of the Flutter Tips app on Flutter web</i></center></figcaption></figure><p>The update is already available on the <a href="https://fluttertips.dev/">app stores</a>, and the <a href="https://app.fluttertips.dev/">web version</a> even supports left/right arrow navigation. 🎯</p><h3><a id="new-ui-challenges" href="#new-ui-challenges">New UI Challenges</a></h3><p>Building UIs in Flutter is fun, but it can be challenging, too!</p><p>So I created this collection of UI challenges to help you practice:</p><figure><picture><source srcset="images/flutter-ui-challenges.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Preview of the Flutter UI Challenges" srcset="images/flutter-ui-challenges.png 2x"/></picture><figcaption><center><i>Preview of the Flutter UI Challenges</i></center></figcaption></figure><p>Each challenge contains a web demo, a starter project, some goals, and the complete solution.</p><p>I've shared 8 challenges to date, including new ones about <a href="https://pro.codewithandrea.com/flutter-ui-challenges/007-page-view-sync-list-view/01-intro">PageView/ListView synchronization</a>, and building a <a href="https://pro.codewithandrea.com/flutter-ui-challenges/008-quiz-app/01-intro">simple quiz app</a>. Try them for free here:</p><ul><li><a href="https://pro.codewithandrea.com/flutter-ui-challenges">Flutter UI Challenges</a></li></ul><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>After <strong>10 months of work</strong>, I'm getting closer to finishing my <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> course, and I hope to wrap up the remaining modules over the next month or two.</p><p>Some new stuff is on the pipeline, too, and I can't wait to share more details soon!</p><p>So stay tuned—and as always, happy coding! 🎉</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/new-formatting-style-dart-3-7/</guid><title>New Formatting Style in Dart 3.7</title><description>Dart 3.7 introduces a new formatter that automatically adds or removes trailing commas, based on the max line length.</description><link>https://codewithandrea.com/tips/new-formatting-style-dart-3-7/</link><pubDate>Thu, 13 Feb 2025 02:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Dart 3.7 introduces a new formatter that <strong>automatically</strong> adds or removes trailing commas, based on the max line length.</p><p>This means you no longer decide how to format your code! The tool now does it for you.</p><figure><picture><source srcset="images/229.webp 2x" type="image/webp"/><img class="bottom-40px" alt="New Formatting Style in Dart 3.7" srcset="images/229.png 2x"/></picture></figure><p>The new change affects all Flutter developers.</p><p>So I created this guide to help you handle it smoothly in your projects. 👇</p><ul><li><a href="https://codewithandrea.com/articles/new-formatting-style-dart-3-7/">There's a New Formatting Style in Dart 3.7 (Here's What It Means for You)</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/articles/updated-formatter-dart-3-8/</guid><title>How to Configure the Updated Code Formatter in Dart 3.8</title><description>Dart 3.8 introduced an updated formatter with various configuration options. Learn what’s new and how to prepare your codebase.</description><link>https://codewithandrea.com/articles/updated-formatter-dart-3-8/</link><pubDate>Thu, 13 Feb 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p><a href="https://medium.com/flutter/whats-new-in-flutter-3-29-f90c380c2317">Flutter 3.29</a> and <a href="https://medium.com/flutter/whats-new-in-flutter-3-29-f90c380c2317">Dart 3.7</a> dropped in February 2025, bringing a bunch of new features and improvements.</p><p>While some changes might not impact you immediately, <strong>one update will affect every Flutter developer</strong>: <strong>the new Dart formatter</strong>.</p><p>This change is <strong>big</strong> and unless you explicitly opt-out, you'll get <strong>a ton</strong> of formatting changes in your codebase. This article guides you through the new changes and all the configuration options, so you can handle things smoothly.</p><p>Let's dive in!</p><blockquote><p><strong>Update</strong>: Dart 3.8 (part of Flutter 3.32) adds a new formatter option to <strong>preserve trailing commas</strong>. This is significant because it means you can upgrade your SDK but still opt-out if you don't like the new style. More on this below.</p></blockquote><h2><a id="how-did-the-old-formatter-work?" href="#how-did-the-old-formatter-work?">How Did The Old Formatter Work?</a></h2><p>Before Dart 3.7, <strong>you</strong> had control over trailing commas. Consider this widget constructor:</p><figure><picture><img class="bottom-12px" alt="Manual formatting with or without trailing commas (applied on save)" srcset="images/manual-formatting.gif 1x"/></picture><figcaption><center><i>Manual formatting with or without trailing commas (applied on save)</i></center></figcaption></figure><p>Here's how the old formatter behaved:</p><ul><li><strong>Add</strong> a trailing comma → formatter <strong>splits</strong> it into multiple lines.</li><li><strong>Remove</strong> the trailing comma → formatter <strong>inlines</strong> it (if it fits within the page width).</li></ul><p>While consistent, this approach required you to decide whether to add or remove trailing commas <strong>for all parameter lists and widgets in your codebase</strong> (and argue about it during code reviews 😞).</p><p>To fix this, <a href="https://medium.com/dartlang/announcing-dart-3-7-bf864a1b195c">Dart 3.7</a> introduces a <a href="https://github.com/dart-lang/dart_style/issues/1253">new formatting style</a>.</p><h2><a id="how-does-the-new-formatter-work?" href="#how-does-the-new-formatter-work?">How Does The New Formatter Work?</a></h2><p>Now, the formatter <strong>automatically decides</strong> whether to add or remove trailing commas.</p><ul><li>If a parameter list <strong>fits within the max page width</strong>, the formatter <strong>removes</strong> trailing commas and keeps it inline:</li></ul><figure><picture><img class="bottom-12px" alt="New formatter removes trailing commas when we don't exceed the max page width" srcset="images/auto-formatting-inline.gif 1x"/></picture><figcaption><center><i>New formatter removes trailing commas when we don't exceed the max page width</i></center></figcaption></figure><ul><li>If a parameter list <strong>exceeds</strong> the max page width, the formatter <strong>adds</strong> a trailing comma and splits it into multiple lines:<ul></ul></li></ul><figure><picture><img class="bottom-12px" alt="New formatter adds trailing commas when the code exceeds the page width" srcset="images/auto-formatting-multiple-lines.gif 1x"/></picture><figcaption><center><i>New formatter adds trailing commas when the code exceeds the page width</i></center></figcaption></figure><hr><h3><a id="summary-old-vs-new-formatter" href="#summary-old-vs-new-formatter">Summary: Old vs New Formatter</a></h3><ul><li>Old formatter<ul><li>Add trailing comma → split into multiple lines</li><li>Remove trailing comma → inline code</li></ul></li></ul><ul><li>New formatter<ul><li>Code fits on one line → remove trailing comma and inline code</li><li>Code exceeds page width → add trailing comma and split into multiple lines</li></ul></li></ul><h2><a id="what-does-this-mean-for-you?" href="#what-does-this-mean-for-you?">What Does This Mean for You?</a></h2><p>If your <code>pubspec.yaml</code> still uses Dart 3.6 or below, <strong>nothing changes</strong> yet:</p><pre><code><div class="highlight"><span></span><span class="nt">environment</span><span class="p">:</span>
<span class="w">  </span><span class="nt">sdk</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">^3.6.0</span>
</div></code></pre><p>But as soon as you explicitly upgrade to Dart 3.7 or above, the new formatter kicks in:</p><pre><code><div class="highlight"><span></span><span class="nt">environment</span><span class="p">:</span>
<span class="w">  </span><span class="nt">sdk</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">^3.7.0</span>
</div></code></pre><p>And when that happens... <strong>expect a massive diff in your codebase</strong>.</p><h3><a id="⚠️-important-formatting-applies-on-save" href="#⚠️-important-formatting-applies-on-save">⚠️ Important: Formatting Applies on Save</a></h3><p>As soon as you save a file, the new formatting style applies, leading to <strong>large diffs</strong>:</p><figure><picture><source srcset="images/single-file-code-diff.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Code diff for a single file after applying the new formatter" srcset="images/single-file-code-diff.png 2x"/></picture><figcaption><center><i>Code diff for a single file after applying the new formatter</i></center></figcaption></figure><p>To avoid chaos, <strong>apply the new formatter across your entire codebase in one go</strong>:</p><pre><code><div class="highlight"><span></span><span class="c1"># Run from the root of your project</span>
dart<span class="w"> </span>format<span class="w"> </span>.
</div></code></pre><p>This ensures all formatting changes are applied consistently. I recommend to <strong>push this as a single PR</strong> and merge it before making other changes:</p><pre><code><div class="highlight"><span></span><span class="c1"># Push all changes at once</span>
git<span class="w"> </span>add<span class="w"> </span>.
git<span class="w"> </span>commit<span class="w"> </span>-m<span class="w"> </span><span class="s2">&quot;Apply the new formatter&quot;</span>
git<span class="w"> </span>push
</div></code></pre><hr><p><em>For reference, when I applied the new formatter to my <a href="https://fluttertips.dev/">Flutter Tips App</a>, I ended up with a PR with <strong>1200+ changes</strong>, which is about 25% of the total line count:</em></p><figure><picture><source srcset="images/flutter-tips-app-changes.webp 2x" type="image/webp"/><img class="bottom-12px" alt="PR with 1200+ changes (25% of the total line count) on my Flutter Tips app" srcset="images/flutter-tips-app-changes.png 2x"/></picture><figcaption><center><i>PR with 1200+ changes (25% of the total line count) on my Flutter Tips app</i></center></figcaption></figure><hr><h2><a id="what-about-page-width?" href="#what-about-page-width?">What About Page Width?</a></h2><p>Let's recall that the new formatter decides how to format the code <strong>based on the maximum page width</strong> (a.k.a line length).</p><p>This is configurable in your <code>analysis_options.yaml</code> file:</p><pre><code><div class="highlight"><span></span><span class="nt">formatter</span><span class="p">:</span>
<span class="w">  </span><span class="nt">page_width</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">120</span>
</div></code></pre><p>Changing this and running <code>dart format</code> again will reformat your entire codebase accordingly.</p><p><strong>Bottom line:</strong> Pick a page width that works for your project and team, and stick with it. 👍</p><h2><a id="what-about-code-generated-files?" href="#what-about-code-generated-files?">What About Code-Generated Files?</a></h2><p>When you run <code>dart format .</code>, it applies to all Dart files in your project, including generated files.</p><p>But there's a catch: <code>build_runner</code> doesn’t respect your <code>page_width</code> setting. Most code generators format output with a fixed 80-character line width, which means generated files may not match your project's formatting.</p><p>What this means for you:</p><ul><li>If your <code>page_width</code> is <strong>80</strong> (default), everything will align without issues.</li><li>If you use a <strong>custom page width</strong>, generated files will revert to <strong>80-character</strong> line breaks whenever you run <code>build_runner</code>.</li></ul><p>This isn’t a major problem, since you only need to run the formatter <strong>once</strong> for the entire project (as suggested above).</p><blockquote><p>Future Improvement: According to <a href="https://github.com/dart-lang/dart_style/issues/864#issuecomment-2492415699">this comment</a>, a future Dart formatter update will <a href="https://github.com/dart-lang/dart_style/issues/864">allow excluding specific files from formatting</a>, which could help with this issue.</p></blockquote><h2><a id="what-about-the-updated-formatter-in-dart-38?" href="#what-about-the-updated-formatter-in-dart-38?">What About the Updated Formatter in Dart 3.8?</a></h2><p>When the new formatter <a href="https://github.com/dart-lang/dart_style/issues/1253">was first discussed in 2023</a>, some raised concerns about <a href="https://github.com/dart-lang/dart_style/issues/1253#issuecomment-1689141432">inconsistent formatting</a> because line breaks don't respect the semantic meaning of the code.</p><p>After more discussions, the Dart team agreed to add a new formatter option in Dart 3.8 to <strong>preserve trailing commas</strong>.</p><p>This means you can upgrade to Dart 3.8:</p><pre><code><div class="highlight"><span></span><span class="c1"># pubspec.yaml</span>
<span class="nt">environment</span><span class="p">:</span>
<span class="w">  </span><span class="nt">sdk</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">^3.8.0</span>
</div></code></pre><p>Then, you have to choose between two options:</p><h3><a id="1-preserve-trailing-commas" href="#1-preserve-trailing-commas">1. Preserve trailing commas</a></h3><p>Enable this option in your <code>analysis_options.yaml</code> file:</p><pre><code><div class="highlight"><span></span><span class="c1"># analysis_options.yaml</span>
<span class="nt">formatter</span><span class="p">:</span>
<span class="w">  </span><span class="nt">trailing_commas</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">preserve</span>
</div></code></pre><p>Effectively, this disables the new formatter behaviour and gives you back control over trailing commas.</p><h3><a id="2-dont-preserve-trailing-commas-default-formatting-style" href="#2-dont-preserve-trailing-commas-default-formatting-style">2. Don't preserve trailing commas (default formatting style)</a></h3><p>As discussed above, the new formatting style will strip trailing commas on code that fits within the page width, so expect a large diff in your codebase.</p><p>To handle this smoothly, follow these steps:</p><ul><li>Set a custom <code>page_width</code> in <code>analysis_options.yaml</code> (optional but recommended):</li></ul><pre><code><div class="highlight"><span></span><span class="c1"># analysis_options.yaml</span>
<span class="nt">formatter</span><span class="p">:</span>
<span class="w">  </span><span class="nt">page_width</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">120</span>
</div></code></pre><ul><li>Apply the formatter to your entire codebase:</li></ul><pre><code><div class="highlight"><span></span><span class="c1"># Run from the root of your project</span>
dart<span class="w"> </span>format<span class="w"> </span>.
</div></code></pre><ul><li>Run <code>build_runner</code> again to update the generated files:</li></ul><pre><code><div class="highlight"><span></span><span class="c1"># Update code-generated files</span>
dart<span class="w"> </span>run<span class="w"> </span>build_runner<span class="w"> </span>watch<span class="w"> </span>-d
</div></code></pre><ul><li>Push the changes <strong>as a single PR</strong>, then merge it before continuing development.</li></ul><pre><code><div class="highlight"><span></span><span class="c1"># Push all changes at once</span>
git<span class="w"> </span>add<span class="w"> </span>.
git<span class="w"> </span>commit<span class="w"> </span>-m<span class="w"> </span><span class="s2">&quot;Apply the new formatter&quot;</span>
git<span class="w"> </span>push
</div></code></pre><blockquote><p>While the new formatter is safe to use, Flutter 3.32 is brand new—bugs may still surface. If you're not in a rush, consider waiting for a hotfix release before upgrading.</p></blockquote><h3><a id="additional-resources" href="#additional-resources">Additional Resources</a></h3><p>All the configuration options are documented here:</p><ul><li><a href="https://github.com/dart-lang/dart_style/wiki/Configuration">Dart formatter configuration options</a></li></ul><p>Here's the original proposal and discussion about the new formatter:</p><ul><li><a href="https://github.com/dart-lang/dart_style/issues/1253">New tall style: Automated trailing commas and other formatting changes</a></li></ul>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/wildcard-variables-dart-3-7/</guid><title>Wildcard Variables in Dart 3.7</title><description>Since Dart 3.7, the _ character is a wildcard variable and you can use it more than once in your code, without causing name collisions.</description><link>https://codewithandrea.com/tips/wildcard-variables-dart-3-7/</link><pubDate>Thu, 13 Feb 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Since Dart 3.7, the <code>_</code> character is a wildcard variable.</p><p>This means that:</p><ul><li>You can use it more than once in your code (e.g. inside parameter lists), without causing name collisions.</li><li>You can't use it as an actual variable (it's only a placeholder).</li></ul><figure><picture><source srcset="images/228.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Wildcard Variables in Dart 3.7" srcset="images/228.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/hotkeys-callback-shortcuts/</guid><title>Hotkeys with CallbackShortcuts</title><description>The CallbackShortcuts widget makes it easy to add keyboard shortcuts to your Flutter app. Here's how to use it.</description><link>https://codewithandrea.com/tips/hotkeys-callback-shortcuts/</link><pubDate>Tue, 11 Feb 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>The <code>CallbackShortcuts</code> widget makes it easy to add keyboard shortcuts to your Flutter app.</p><p>To use it:</p><ol><li>Add one or more key bindings using <code>SingleActivator</code></li><li>Add a <code>Focus</code> widget to capture the desired events</li><li>Use callbacks to perform your desired actions</li></ol><figure><picture><source srcset="images/227.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Hotkeys with CallbackShortcuts" srcset="images/227.png 2x"/></picture></figure><h3><a id="example-code" href="#example-code">Example Code</a></h3><pre><code><div class="highlight"><span></span><span class="c1">// * If another widget captures the focus (e.g. a text field), call</span>
<span class="c1">// * hotkeysFocusNode.requestFocus() in the onEditingComplete callback.</span>
<span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="n">hotkeysFocusNode</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">FocusNode</span><span class="p">();</span>

<span class="c1">// in the build method:</span>
<span class="k">return</span><span class="w"> </span><span class="n">CallbackShortcuts</span><span class="p">(</span>
<span class="w">  </span><span class="nl">bindings:</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">const</span><span class="w"> </span><span class="n">SingleActivator</span><span class="p">(</span><span class="n">LogicalKeyboardKey</span><span class="p">.</span><span class="n">arrowLeft</span><span class="p">)</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="n">onPrevious</span><span class="p">();</span>
<span class="w">    </span><span class="p">},</span>
<span class="w">    </span><span class="k">const</span><span class="w"> </span><span class="n">SingleActivator</span><span class="p">(</span><span class="n">LogicalKeyboardKey</span><span class="p">.</span><span class="n">arrowRight</span><span class="p">)</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="n">onNext</span><span class="p">();</span>
<span class="w">    </span><span class="p">},</span>
<span class="w">  </span><span class="p">},</span>
<span class="w">  </span><span class="nl">child:</span><span class="w"> </span><span class="n">Focus</span><span class="p">(</span>
<span class="w">    </span><span class="nl">focusNode:</span><span class="w"> </span><span class="n">hotkeysFocusNode</span><span class="p">,</span>
<span class="w">    </span><span class="nl">autofocus:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w">    </span><span class="nl">descendantsAreFocusable:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
<span class="w">    </span><span class="nl">descendantsAreTraversable:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
<span class="w">    </span><span class="nl">child:</span><span class="w"> </span><span class="n">Row</span><span class="p">(</span>
<span class="w">      </span><span class="nl">children:</span><span class="w"> </span><span class="p">[</span>
<span class="w">        </span><span class="n">IconButton</span><span class="p">(</span>
<span class="w">          </span><span class="nl">tooltip:</span><span class="w"> </span><span class="s1">&#39;Hotkey: ⬅️&#39;</span><span class="p">,</span>
<span class="w">          </span><span class="nl">icon:</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="n">Icon</span><span class="p">(</span><span class="n">Icons</span><span class="p">.</span><span class="n">arrow_back</span><span class="p">),</span>
<span class="w">          </span><span class="nl">onPressed:</span><span class="w"> </span><span class="n">onPrevious</span><span class="p">,</span>
<span class="w">        </span><span class="p">),</span>
<span class="w">        </span><span class="n">IconButton</span><span class="p">(</span>
<span class="w">          </span><span class="nl">tooltip:</span><span class="w"> </span><span class="s1">&#39;Hotkey: ➡️&#39;</span><span class="p">,</span>
<span class="w">          </span><span class="nl">icon:</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="n">Icon</span><span class="p">(</span><span class="n">Icons</span><span class="p">.</span><span class="n">arrow_forward</span><span class="p">),</span>
<span class="w">          </span><span class="nl">onPressed:</span><span class="w"> </span><span class="n">onNext</span><span class="p">,</span>
<span class="w">        </span><span class="p">),</span>
<span class="w">      </span><span class="p">],</span>
<span class="w">    </span><span class="p">),</span>
<span class="w">  </span><span class="p">),</span>
<span class="p">);</span>
</div></code></pre><p>Note: the activators will only be triggered when there's a <strong>focused</strong> child widget.</p><p>The easiest way to do this is to wrap your child widget in a <code>Focus</code> widget with <code>autofocus: true</code>.</p><p>If other widgets also rely on active focus, you can explicitly request focus with a <code>FocusNode</code>.</p><hr><p>For more details about <code>CallbackShortcuts</code>, check this video:</p><ul><li><a href="https://youtu.be/VcQQ1ns_qNY?si=61hK4ZvPLDoU7Zox">CallbackShortcuts (Widget of the Week)</a></li></ul><hr><p>To dive deeper and learn what to do when things don't work as you expect, watch this three-part series. 👇</p><ul><li><a href="https://youtu.be/JCDfh5bs1xc?si=ExrsMnyUqi1GzEL_">Part 1: Focus (Widget of the Week)</a></li><li><a href="https://youtu.be/6ZcQmdoz9N8?si=2f-IdXFkv43S6taX">Part 2: Shortcuts (Widget of the Week)</a></li><li><a href="https://youtu.be/XawP1i314WM?si=cESt4-bbVVNlLAzp">Part 3: Actions (Widget of the Week)</a></li></ul><hr><p>If you want to see this technique in action, check my Flutter Tips app (web version):</p><ul><li><a href="https://app.fluttertips.dev/">Flutter Tips web app</a></li></ul><p>This supports three different keyboard shortcuts:</p><ul><li><strong>Left arrow</strong>: go to the previous tip</li><li><strong>Space</strong>: favorite/unfavorite a tip</li><li><strong>Right arrow</strong>: go to the next tip</li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/responsive-split-view-flutter/</guid><title>Responsive Split View in Flutter</title><description>Here's how to create a responsive split-view widget that works on mobile, desktop and web.</description><link>https://codewithandrea.com/tips/responsive-split-view-flutter/</link><pubDate>Mon, 10 Feb 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>In Flutter, you can easily create a responsive split-view widget that works on mobile, desktop and web.</p><p>You can do this in 30 lines of code, without any 3rd party packages: 👇</p><figure><picture><source srcset="images/001.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Responsive Split View in Flutter" srcset="images/001.png 2x"/></picture></figure><h3><a id="example-code" href="#example-code">Example Code</a></h3><pre><code><div class="highlight"><span></span><span class="kd">class</span><span class="w"> </span><span class="nc">SplitView</span><span class="w"> </span><span class="kd">extends</span><span class="w"> </span><span class="n">StatelessWidget</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">const</span><span class="w"> </span><span class="n">SplitView</span><span class="p">({</span><span class="k">super</span><span class="p">.</span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="kd">required</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="n">navigationBuilder</span><span class="p">,</span>
<span class="w">      </span><span class="kd">required</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="n">contentBuilder</span><span class="p">,</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="n">breakpoint</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">600</span><span class="p">,</span>
<span class="w">      </span><span class="k">this</span><span class="p">.</span><span class="n">navigationWidth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">300</span><span class="p">});</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="n">WidgetBuilder</span><span class="w"> </span><span class="n">navigationBuilder</span><span class="p">;</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="n">WidgetBuilder</span><span class="w"> </span><span class="n">contentBuilder</span><span class="p">;</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="kt">double</span><span class="w"> </span><span class="n">breakpoint</span><span class="p">;</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="kt">double</span><span class="w"> </span><span class="n">navigationWidth</span><span class="p">;</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="n">Widget</span><span class="w"> </span><span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span><span class="w"> </span><span class="n">context</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">final</span><span class="w"> </span><span class="n">screenWidth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">MediaQuery</span><span class="p">.</span><span class="n">sizeOf</span><span class="p">(</span><span class="n">context</span><span class="p">).</span><span class="n">width</span><span class="p">;</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">screenWidth</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">breakpoint</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="c1">// * wide screen: navigation on the left, content on the right</span>
<span class="w">      </span><span class="k">return</span><span class="w"> </span><span class="n">Row</span><span class="p">(</span>
<span class="w">        </span><span class="nl">children:</span><span class="w"> </span><span class="p">[</span>
<span class="w">          </span><span class="n">SizedBox</span><span class="p">(</span>
<span class="w">            </span><span class="nl">width:</span><span class="w"> </span><span class="n">navigationWidth</span><span class="p">,</span>
<span class="w">            </span><span class="nl">child:</span><span class="w"> </span><span class="n">navigationBuilder</span><span class="p">(</span><span class="n">context</span><span class="p">),</span>
<span class="w">          </span><span class="p">),</span>
<span class="w">          </span><span class="c1">// if you want, add a divider here</span>
<span class="w">          </span><span class="n">Expanded</span><span class="p">(</span><span class="nl">child:</span><span class="w"> </span><span class="n">contentBuilder</span><span class="p">(</span><span class="n">context</span><span class="p">)),</span>
<span class="w">        </span><span class="p">],</span>
<span class="w">      </span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="c1">// * show content only (handle navigation with a drawer or similar)</span>
<span class="w">      </span><span class="k">return</span><span class="w"> </span><span class="n">contentBuilder</span><span class="p">(</span><span class="n">context</span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</div></code></pre><p>For a more complete tutorial, read this article (slightly outdated, but the main principles still apply):</p><ul><li><a href="https://codewithandrea.com/articles/flutter-responsive-layouts-split-view-drawer-navigation/">Responsive layouts in Flutter: Split View and Drawer Navigation</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/github-self-hosted-runners/</guid><title>GitHub Self-Hosted Runners</title><description>If you're using GitHub Actions for your CI/CD needs, you can setup a self-hosted runner to cut your build times and save money.</description><link>https://codewithandrea.com/tips/github-self-hosted-runners/</link><pubDate>Thu, 6 Feb 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If you're using GitHub Actions for your CI/CD needs, you can setup a self-hosted runner to cut your build times and save money. 🎯</p><p>This is very easy to setup, and if you have an organization account, you can share one or more runners across all your repos. 👍</p><figure><picture><source srcset="images/226.webp 2x" type="image/webp"/><img class="bottom-40px" alt="GitHub Self-Hosted Runners" srcset="images/226.png 2x"/></picture></figure><hr><p>My <a href="https://codewithandrea.com/courses/flutter-in-production/">latest course</a> includes a whole module about GitHub Actions, showing how to make your pipelines faster, more reliable, and easier to manage.</p><p>A complete workflow for Android &amp; iOS is also included as a reference.</p><p>To learn more, check the module intro:</p><ul><li><a href="https://pro.codewithandrea.com/flutter-in-production/12-github-actions/01-intro">Introduction to GitHub Actions</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/january-2025/</guid><title>January 2025: Flutter vs React Native, Hard Truths About AI, Pub Workspaces, Less-Known Widgets</title><description>Also included: GoRouter in maintenance mode, Dart Serialization Proposal, localizing an app into 55 languages, Cupertino and Material Updates, and more.</description><link>https://codewithandrea.com/newsletter/january-2025/</link><pubDate>Thu, 23 Jan 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Welcome to the first edition of my Flutter newsletter for 2025! 🎉</p><p>This month, we’re diving into these topics:</p><ul><li>Outlook for Flutter vs React Native in 2025</li><li>Hard truths about AI</li><li>GoRouter entering maintenance mode (and what it means for you)</li><li>A new serialization protocol for Dart (proposal)</li><li>Selected articles from the community</li><li>All the latest from Code With Andrea</li></ul><p>Let’s dive in! 🚀</p><h2><a id="flutter-news" href="#flutter-news">Flutter News</a></h2><p>Let’s kick-off with some trends that will shape Flutter and the mobile app development landscape in 2025.</p><h3><a id="📝-cross-platform-mobile-development" href="#📝-cross-platform-mobile-development">📝 Cross-platform mobile development</a></h3><p><a href="https://bsky.app/profile/gergely.pragmaticengineer.com">Gergely Orosz</a> recently published a deep dive into the most popular frameworks: React Native, Flutter, native-first, and web-based technologies, and how to pick the right approach.</p><p>Key takeaways:</p><ul><li>Flutter and React Native dominate the cross-platform space and <strong>are both here to stay</strong>.</li><li>Developer preference often depends on location.</li><li>While Flutter powers more apps, React Native apps generate slightly more revenue.</li></ul><p>Read-on for the full breakdown:</p><ul><li><a href="https://newsletter.pragmaticengineer.com/p/cross-platform-mobile-development">Cross-platform mobile development</a></li></ul><blockquote><p><strong>Clarification</strong>: During the Flutter In Production livestream, <a href="https://medium.com/flutter/flutter-in-production-f9418261d8e1">it was claimed</a> that 28% of new iOS apps on the App Store use Flutter (source: Apptopia), but this figure was based on <strong>free apps only</strong>. The latest <a href="https://appfigures.com/top-sdks/development/all">app intelligence</a> stats from App Figures (which take into account paid apps, too) show that Flutter sits at 13% on the App Store and 19% on the Play Store.</p></blockquote><h3><a id="📝 hard-truths-about-ai" href="#📝 hard-truths-about-ai">📝 Hard Truths About AI</a></h3><p>AI is revolutionizing how we build software—and Flutter apps are no exception. I find myself using AI tools more and more, and this <a href="https://newsletter.pragmaticengineer.com/p/how-ai-will-change-software-engineering">recent article</a> about AI-assisted coding really resonated with me.</p><p>Highlights include:</p><ul><li><strong>The Knowledge Paradox</strong>: Seniors use AI to speed up tasks they already know, while Juniors use AI to learn new things—with very different results.</li><li><strong>Practical Patterns</strong>: things like "AI first draft", "constant conversation", and "trust but verify" are emerging as effective ways to work with AI.</li><li><strong>Software as a Craft</strong>: While AI makes it easier to build software quickly, creating polished, high-quality experiences is still crucial if you want to build products that stand out.</li></ul><p>Check out the full post for a pragmatic take on how AI is reshaping engineering:</p><ul><li><a href="https://newsletter.pragmaticengineer.com/p/how-ai-will-change-software-engineering">How AI-assisted coding will change software engineering: hard truths</a></li></ul><blockquote><p>Curious about how other Flutter devs feel about AI? Here are <a href="https://www.reddit.com/r/FlutterDev/comments/1hrvyiz/my_experience_using_ai_to_create_an_entire/">some</a> <a href="https://www.reddit.com/r/FlutterDev/comments/1hxyq5n/my_experience_with_building_an_app_with_cursor_ai/">recent</a> <a href="https://www.reddit.com/r/FlutterDev/comments/1i5yqn6/claude_is_fantastic_if_used_right/">posts</a> on Reddit.</p></blockquote><h3><a id="⚠️-gorouter-enters-maintenance-mode" href="#⚠️-gorouter-enters-maintenance-mode">⚠️ GoRouter Enters Maintenance Mode</a></h3><p>The Flutter team has been maintaining <a href="https://pub.dev/packages/go_router">GoRouter</a> for many years, but many long-standing <a href="https://github.com/flutter/flutter/issues?q=is%3Aissue%20state%3Aopen%20%5Bgo_router%5D%20label%3AP1%20%20%20">P1</a> and <a href="https://github.com/flutter/flutter/issues?q=is%3Aissue%20state%3Aopen%20%5Bgo_router%5D%20label%3AP2">P2</a> issues have still not been addressed.</p><p>Most recently, this notice was added at the bottom of the package README:</p><blockquote><p>This package has entered a maintenance phase. The Flutter team's primary focus will be on addressing bug fixes and ensuring stability. While active feature development is not currently planned, we welcome and encourage community contributions to expand the package's functionality.</p></blockquote><p>If this means that the team can focus more on core parts of the Flutter framework, I welcome this. But it’s clear that developers are frustrated with all the GoRouter bugs.</p><p>Here’s my take:</p><ul><li>If your app already uses GoRouter and you can work around its issues, keep using it.</li><li>If you’re starting a new project and the navigator 1.0 APIs are not enough for you, consider moving to alternatives like <a href="https://pub.dev/packages/auto_route">auto_route</a> (fully-featured) or <a href="https://pub.dev/packages/navigation_utils">navigation_utils</a> (lightweight).</li></ul><h3><a id="🧪-rfc-new-serialization-protocol-for-dart" href="#🧪-rfc-new-serialization-protocol-for-dart">🧪 RFC: New Serialization Protocol for Dart</a></h3><p>Serialization is a critical aspect of application development, yet the Dart ecosystem currently lacks a unified, efficient approach to tackle it. Existing solutions like <a href="https://pub.dev/packages/json_serializable"><code>json_serializable</code></a> focus primarily on JSON and rely on intermediate <code>Map</code> objects, which limit performance and flexibility.</p><p>This proposal aims to introduce a modular, universally usable, and performant serialization protocol. It emphasizes format-agnostic functionality, allowing seamless support for JSON, CSV, YAML, MessagePack, and more, while also enabling developers to avoid redundant code when switching between data formats.</p><p>Even if you are happily using <code>json_serializable</code> or <code>jsonEncode/jsonDecode</code> today, I recommend reading the proposal and understanding the issues it aims to solve:</p><ul><li><a href="https://github.com/schultek/codable/blob/main/docs/rfc.md">RFC: New Serialization Protocol for Dart</a></li></ul><h2><a id="flutter-articles-from-the-community" href="#flutter-articles-from-the-community">Flutter Articles from the Community</a></h2><p>Here are my top picks from the Flutterverse this month.</p><h3><a id="📝-how-to-localize-the-app-into-55-languages-and-not-go-crazy?" href="#📝-how-to-localize-the-app-into-55-languages-and-not-go-crazy?">📝 How to localize the app into 55 languages and not go crazy?</a></h3><p>If you’ve ever wondered what it takes to build a language-learning app, this article is for you.</p><p>From internationalization challenges like RTL support, to handling remote localizations, to choosing which flag to show for multi-country languages, it’s all covered here. 👇</p><ul><li><a href="https://dariadroid.substack.com/p/how-to-localize-the-app-into-55-languages">How to localize the app into 55 languages and not go crazy?</a></li></ul><h3><a id="📝-10-flutter-widgets-you-probably-haven’t-heard-of-but-should-be-using!" href="#📝-10-flutter-widgets-you-probably-haven’t-heard-of-but-should-be-using!">📝 10 Flutter Widgets You Probably Haven’t Heard Of (But Should Be Using!)</a></h3><p>In this article, <a href="https://bsky.app/profile/mhadaily.bsky.social">Majid Hajian</a> uncovers hidden gems like <code>AnimatedModalBarrier</code>, <code>RepaintBoundary</code>, and <code>SemanticsDebugger</code>, along with some of their implementation details.</p><ul><li><a href="https://dcm.dev/blog/2025/01/13/ten-flutter-widgets-probably-havent-heard-of-but-should-be-using/">10 Flutter Widgets Probably Haven’t Heard Of (But Should Be Using!)</a></li></ul><h3><a id="📝-exploring-cupertino-and-material-updates-in-flutter-3270" href="#📝-exploring-cupertino-and-material-updates-in-flutter-3270">📝 Exploring Cupertino and Material Updates in Flutter 3.27.0</a></h3><p>Flutter 3.27.0 shipped with a range of updates to the Cupertino and Material widget catalogs.</p><p>This article explores the key updates with code samples and illustrations, so you can more easily use the new APIs in your apps:</p><ul><li><a href="https://canopas.com/exploring-cupertino-and-material-updates-in-flutter-3-27-cb4c76e222e1">Exploring Cupertino and Material Updates in Flutter 3.27.0</a></li></ul><h3><a id="📝-modern-monorepo-management-with-pub-workspaces-and-melos-in-dart" href="#📝-modern-monorepo-management-with-pub-workspaces-and-melos-in-dart">📝 Modern Monorepo Management with Pub Workspaces and Melos in Dart</a></h3><p>Monorepos are great when you want to store multiple apps and/or packages in a single repository. And thanks to the latest <a href="https://dart.dev/tools/pub/workspaces">pub workspace</a> feature in Dart 3.6, it’s easier than ever to manage them.</p><p>Key benefits include:</p><ul><li><strong>Centralized Dependency Management</strong>: All projects in the workspace share a <strong>single version</strong> of each dependency.</li><li><strong>Unified Upgrades</strong>: Running <code>dart pub upgrade</code> applies updates across all packages in the workspace.</li><li><strong>Improved Dart Analyzer Performance</strong>: Previous issues with monorepos causing degraded analyzer performance are now gone.</li></ul><p>Read on to learn how to setup a pub workspace and take things further with <a href="https://melos.zone/">Melos</a>:</p><ul><li><a href="https://lazebny.io/modern-monorepo-management/">Modern Monorepo Management with Pub Workspaces and Melos in Dart</a></li></ul><h3><a id="📝-all-i-know-about-state-management" href="#📝-all-i-know-about-state-management">📝 All I Know about State Management</a></h3><p>If you ever wondered what happens when you call <code>setState</code> in Flutter, this article is for you. Inside, you'll learn:</p><ul><li>How Flutter internally marks certain elements as "dirty" and ensure they're rebuild on the next frame.</li><li>How <code>InheritedWidget</code> and <code>ValueListenableBuilder</code> work.</li><li>How 3rd party solutions like <a href="https://pub.dev/packages/flutter_hooks"><code>flutter_hooks</code></a>, <a href="https://pub.dev/packages/signals"><code>signals</code></a>, and <a href="https://pub.dev/packages/flutter_riverpod"><code>flutter_riverpod</code></a> build upon core Flutter APIs like <a href="https://api.flutter.dev/flutter/widgets/Element/markNeedsBuild.html"><code>markNeedsBuild()</code></a> under the hood.</li></ul><p>Read on for all the details:</p><ul><li><a href="https://chooyan.hashnode.dev/all-i-know-about-state-management">All I Know about State Management</a></li></ul><h2><a id="latest-from-code-with-andrea" href="#latest-from-code-with-andrea">Latest from Code with Andrea</a></h2><p>Since the last edition, I added a <a href="https://pro.codewithandrea.com/flutter-in-production/11-codemagic/01-intro">new module about CI/CD with Codemagic</a> to my <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> course, published my 2024 retro, and shared a new article. Here’s a recap.</p><h3><a id="📝-my-2024-in-review-ups-and-downs" href="#📝-my-2024-in-review-ups-and-downs">📝 My 2024 in Review: Ups and Downs</a></h3><p>Every year, I take a step back to reflect on my journey as a content creator.</p><p>This 2024 retrospective dives into my highlights and achievements from the past year, how AI, SEO, and social media are reshaping my strategy, the launch of latest course, and more:</p><ul><li><a href="https://codewithandrea.com/meta/my-2024-retro/">My 2024 in Review: Ups and Downs</a></li></ul><h3><a id="📝-how-to-release-your-flutter-app-on-the-google-play-store" href="#📝-how-to-release-your-flutter-app-on-the-google-play-store">📝 How to Release Your Flutter App on the Google Play Store</a></h3><p>If you're new to the Google Play Store, there's a lot to consider, from signing up for a developer account (and understanding the strict testing policies for new accounts), to preparing and submitting your app for review.</p><p>This guide covers all the essential steps, so you can publish your app with more confidence:</p><ul><li><a href="https://codewithandrea.com/articles/how-to-release-flutter-google-play-store/">How to Release Your Flutter App on the Google Play Store</a></li></ul><blockquote><p><strong>Site Update</strong>: As my collection of articles continues to grow, I decided to re-arrange them by topic for easier discovery. You can explore them at <a href="https://codewithandrea.com/tutorials/">this link</a>.</p></blockquote><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>This year, I’ll continue to publish new Flutter tips, articles, and courses, and I hope they'll help you succeed as a software engineer.</p><p>Stay tuned—and as always, happy coding! 🎉</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/debug-fill-properties/</guid><title>The debugFillProperties Method</title><description>If you add a debugFillProperties() method to your custom widget classes, all your custom widget properties will show in the DevTools.</description><link>https://codewithandrea.com/tips/debug-fill-properties/</link><pubDate>Thu, 23 Jan 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If you add a <code>debugFillProperties()</code> method to your widget classes, all your custom properties will show in the DevTools.</p><p>This information is useful for debugging purposes, and is stripped from release builds (no performance impact).</p><figure><picture><source srcset="images/225.webp 2x" type="image/webp"/><img class="bottom-40px" alt="The debugFillProperties Method" srcset="images/225.png 2x"/></picture></figure><p>To learn more about all the supported property types and variants, read the official docs:</p><ul><li><a href="https://api.flutter.dev/flutter/widgets/State/debugFillProperties.html">debugFillProperties method</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/upload-source-maps-sentry/</guid><title>Uploading the Source Maps to Sentry</title><description>If you enable code obfuscation in your Flutter release builds, the stack traces will be unreadable in the Sentry crash reports. Here's how to fix it.</description><link>https://codewithandrea.com/tips/upload-source-maps-sentry/</link><pubDate>Mon, 20 Jan 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If you enable code obfuscation in your Flutter release builds, the stack traces will be unreadable in the Sentry crash reports.</p><p>To fix this:</p><ul><li>Generate the source maps with <code>--split-debug-info</code></li><li>Use the <a href="https://pub.dev/packages/sentry_dart_plugin"><code>sentry_dart_plugin</code></a> package to upload them</li><li>Automate the process with CI/CD (optional but highly recommended)</li></ul><figure><picture><source srcset="images/224.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Uploading the Source Maps to Sentry" srcset="images/224.png 2x"/></picture></figure><h3><a id="learn-more" href="#learn-more">Learn more</a></h3><p>To learn more about code obfuscation and how to upload the source maps to Sentry, check these links:</p><ul><li><a href="https://docs.flutter.dev/deployment/obfuscate">Obfuscate Dart code</a></li><li><a href="https://pub.dev/packages/sentry_dart_plugin">sentry_dart_plugin package</a></li></ul><p>For a more complete guide showing how everything fits together (error monitoring, app releases, and CI/CD automation), check my latest course:</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/ssh-access-codemagic/</guid><title>SSH Access on Codemagic Builds</title><description>If your Codemagic builds are failing, you can enable SSH access and login to the build runner to diagnose the issue.</description><link>https://codewithandrea.com/tips/ssh-access-codemagic/</link><pubDate>Thu, 16 Jan 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If your Codemagic builds are failing and you can't figure out why, you can enable SSH access. 🔑</p><p>Use this to login to the build runner, where you can inspect the environment variables and project files, and see if something is missing. 💡</p><figure><picture><source srcset="images/223.webp 2x" type="image/webp"/><img class="bottom-40px" alt="SSH Access on Codemagic Builds" srcset="images/223.png 2x"/></picture></figure><hr><p>My <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> course contains a whole module about CI/CD automation with Codemagic.</p><p>If you want to check it out, here's the intro:</p><ul><li><a href="https://pro.codewithandrea.com/flutter-in-production/11-codemagic/01-intro">Introduction to CI/CD</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/articles/how-to-release-flutter-google-play-store/</guid><title>How to Release Your Flutter App on the Google Play Store</title><description>A reference guide for releasing your Flutter app on the Play Store, including app content, store listing, Android project settings, and code signing.</description><link>https://codewithandrea.com/articles/how-to-release-flutter-google-play-store/</link><pubDate>Mon, 13 Jan 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Your Flutter app is ready, and you’ve completed all the important <a href="https://codewithandrea.com/articles/key-steps-before-releasing-flutter-app/">pre-release steps</a>. Now it’s time to publish it on the <strong>Google Play Store</strong>. But where do you start?</p><p>The Flutter documentation already provides a technical guide for <a href="https://docs.flutter.dev/deployment/android">building and releasing an Android app</a>. But if you're new to the Google Play Store, there's a lot more to consider, from signing up for a developer account (and understanding the strict testing policies for new accounts), to preparing and submitting your app for review.</p><p>If you’re new to this, don’t worry. This article will provide a <strong>high-level roadmap</strong> for releasing your Flutter app on the Google Play Store. Even if you’re familiar with this process, you may discover a trick or two along the way. 👍</p><blockquote><p>This article is about releasing your app on the <strong>Google Play Store</strong>. A companion article about <a href="https://codewithandrea.com/articles/how-to-release-flutter-ios-app-store/">How to Release Your Flutter App on the iOS App Store</a> is also available.</p></blockquote><h3><a id="what-you’ll-learn" href="#what-you’ll-learn">What You’ll Learn:</a></h3><ul><li>How to <strong>sign up for a Google Play Developer Account</strong>.</li><li>How to <strong>create a new app</strong> on the Google Play Console.</li><li>How to <strong>prepare your app for review</strong> by filling all the app content, data safety, and store listing info.</li><li>How to <strong>review the Android project settings</strong>, including code signing.</li><li>How to <strong>build, upload, and submit</strong> your app for review.</li></ul><blockquote><p>Want a more detailed, hands-on guide? My <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> course includes a complete module with 17 lessons that cover each step in depth—from account setup to submission and beyond.</p></blockquote><p>Let’s dive in! 🚀</p><h2><a id="should-you-sell-apps-on-the-google-play-store?" href="#should-you-sell-apps-on-the-google-play-store?">Should You Sell Apps on the Google Play Store?</a></h2><p>It's well known that Apple users are far more willing to pay for apps compared to Android users (it’s <a href="https://9to5mac.com/2023/09/06/iphone-users-spend-apps/">not even close</a>), especially in premium markets like the United States.</p><p>The Play Store comes with some other drawbacks, too:</p><ul><li><strong>Strict testing requirements</strong>: Individual developers face additional friction due to <a href="https://support.google.com/googleplay/android-developer/answer/14151465">mandatory closed testing</a> requirements introduced in 2023.</li><li><strong>Device fragmentation</strong>: Unlike Apple’s tightly controlled ecosystem, Android runs on a vast number of devices with varying screen sizes, hardware capabilities, and operating system versions. This leads to <strong>higher testing effort</strong> and <strong>increased maintenance overhead</strong>.</li><li><strong>Privacy Concerns</strong>: For individual developers, personal information such as your <strong>legal name</strong>, <strong>address</strong>, and <strong>email</strong> may be displayed <strong>publicly</strong> on the Play Store. This raises privacy concerns, especially for solo developers who may not want their personal details exposed.</li></ul><p>With that said, Google's Play Store is still a great platform if:</p><ul><li>Your target audience is located in regions where Android has a larger market share than iOS (e.g., Asia, Africa, Latin America).</li><li>You’re building apps for markets where affordability and accessibility are key factors, as Android dominates mid-range and low-cost device segments.</li></ul><p><strong>Bottom line</strong>:</p><ul><li><strong>Individuals</strong>: If you’re starting out as a solo developer, consider prioritizing the <strong>Apple App Store</strong>. It offers better monetization potential and access to a premium audience, making it a more lucrative starting point.</li><li><strong>Companies</strong>: If your business needs a presence on both platforms, publishing on both the App Store and Play Store is essential.</li></ul><h2><a id="1-sign-up-for-a-google-play-developer-account" href="#1-sign-up-for-a-google-play-developer-account">1. Sign Up for a Google Play Developer Account</a></h2><p>To publish apps on the Google Play Store, you’ll need to create a Play Console developer account.</p><h3><a id="key-info" href="#key-info">Key info:</a></h3><ul><li><strong>Cost</strong>: one time $25 fee.</li><li><strong>Who can enroll</strong>: Individuals or organizations.</li><li><strong>How to enroll</strong>: Start the process at <a href="https://play.google.com/console/signup">play.google.com/console/signup</a>.</li><li><strong>Fees</strong>: Google takes a <strong>15%</strong> cut on your first $1M (USD) of yearly earnings after you enroll in an <a href="https://support.google.com/googleplay/android-developer/answer/10627869">Account Group</a>. Beyond $1M, the fee increases to 30%.</li></ul><h3><a id="invididual-or-organization-account?" href="#invididual-or-organization-account?">Invididual or Organization account?</a></h3><p>The first step on the <a href="https://play.google.com/console/signup">Google Play Console Signup</a> page is to choose between enrolling as an <strong>organization</strong> or as an <strong>individual</strong>.</p><p>Before you choose, beware of the strict <a href="https://support.google.com/googleplay/android-developer/answer/14151465">app testing requirements for new personal developer accounts</a>. Quoting:</p><blockquote><p>If you have a newly created personal developer account, <strong>you must run a closed test</strong> for your app <strong>with a minimum of 12 testers</strong> who have been opted-in <strong>for at least the last 14 days continuously</strong>. When you meet these criteria, you can apply for production access on the <a href="https://play.google.com/console/developers/app/app-dashboard">Dashboard</a> in Play Console so that you can ultimately distribute your app on Google Play.</p></blockquote><p>These requirements <strong>don't</strong> apply if you choose an organization account, or if your personal account was created <strong>before</strong> 13 November 2023.</p><h3><a id="privacy-considerations" href="#privacy-considerations">Privacy considerations</a></h3><p>When creating a developer account, be aware that your <strong>legal name</strong>, <strong>address</strong>, and <strong>contact details</strong> may be displayed <strong>publicly</strong> on Google Play (<a href="https://support.google.com/googleplay/android-developer/answer/13628312">source</a>).</p><ul><li><strong>Individual Accounts:</strong> Google displays your <strong>legal name</strong>, <strong>country</strong>, and <strong>developer email address</strong>. If you monetize your app, your <strong>full address</strong> will also be shown.<ul></ul></li></ul><ul><li><strong>Organization Accounts:</strong> Google displays your organization’s <strong>legal name</strong>, <strong>legal address</strong>, <strong>email</strong>, and <strong>phone number</strong> to improve transparency and user safety.</li></ul><p>For individual developers who want to monetize their apps, this raises significant privacy concerns. To protect your personal information, consider registering a <strong>virtual business address</strong> using a service like <a href="https://ipostal1.com/">iPostal1</a>. However, this option comes with additional costs.</p><h3><a id="how-to-sign-up-for-a-google-play-developer-account" href="#how-to-sign-up-for-a-google-play-developer-account">How to Sign Up for a Google Play Developer Account</a></h3><p>To get started, visit <a href="https://play.google.com/console/signup">this page to sign up for an organization or individual account</a>.</p><p>Then, complete all the steps:</p><ul><li>Enter your Developer Name</li><li>Link a Payments Profile to verify your identity</li><li>Enter your email address and consent for your data to be shown on your public developer profile</li><li>Tell Google about you, your website, and your app publishing and monetization plans</li><li>Share your private contact details so Google can contact you</li></ul><blockquote><p>If you're signing up as an organization, you'll also need to <a href="https://www.dnb.com/en-us/smb/duns/get-a-duns.html">obtain a D-U-N-S Number</a>. This process can take 1 to 30 business days, so apply as early as possible.</p></blockquote><p>Once you've agreed to the terms and purchased your membership, you'll need to complete the verification process and submit some official proof of ID and/or business registration.</p><h2><a id="2-create-a-new-app-on-the-google-play-console" href="#2-create-a-new-app-on-the-google-play-console">2. Create a New App on the Google Play Console</a></h2><p>Once your developer account is ready, you can sign in to the <a href="https://play.google.com/console/u/3/developers/"><strong>Google Play Console</strong></a>. After choosing your developer account, you'll be taken to the main dashboard:</p><figure><picture><source srcset="images/google-play-console-signed-in.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Google Play Console dashboard" srcset="images/google-play-console-signed-in.png 2x"/></picture></figure><h3><a id="how-to-create-a-new-app" href="#how-to-create-a-new-app">How to Create a New App</a></h3><p>Select the <strong>Home</strong> tab on the left panel, then click on <strong>Create app</strong>:</p><figure><picture><source srcset="images/create-app-01-apps-list.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Create a new app" srcset="images/create-app-01-apps-list.png 2x"/></picture></figure><p>On the next page, enter all the required details:</p><ul><li>App name</li><li>Default language for your store listing</li><li>Select <strong>App</strong> or <strong>Game</strong></li><li>Select <strong>Free</strong> or <strong>Paid</strong></li></ul><p>Then, tick the declaration boxes and click <strong>Create app</strong>.</p><h3><a id="the-app-dashboard" href="#the-app-dashboard">The App Dashboard</a></h3><p>After creating the app, you'll be taken to your app's dashboard:</p><figure><picture><source srcset="images/create-app-10-dashboard.webp 2x" type="image/webp"/><img class="bottom-40px" alt="App dashboard" srcset="images/create-app-10-dashboard.png 2x"/></picture></figure><p>The App Dashboard is your central hub for managing your app. It highlights important tasks like:</p><ul><li><strong>Setting up your Store Listing</strong>: Add your app's name, description, and media assets.</li><li><strong>Testing and Releasing</strong>: Manage internal testing, open testing, and production releases.</li><li><strong>Compliance</strong>: Ensure your app meets Google Play policies and regulatory requirements.</li></ul><h2><a id="3-prepare-your-app-for-review-app-content" href="#3-prepare-your-app-for-review-app-content">3. Prepare your App For Review (App Content)</a></h2><p>Preparing an app for review on the Play Store is a long process and you'll need to fill in multiple pages. Let's start with the App Content and Data Safety sections.</p><h3><a id="set-up-your-app-app-content" href="#set-up-your-app-app-content">Set up your app (App Content)</a></h3><p>From your app’s dashboard, scroll down to the <strong>Set up your app</strong> section and expand the tasks:</p><figure><picture><source srcset="images/create-app-11-setup-tasks.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Dashboard - Set up your app" srcset="images/create-app-11-setup-tasks.png 2x"/></picture></figure><p>Here's how to tackle them:</p><ul><li><strong>Privacy Policy</strong>: Enter your privacy policy URL.</li><li><strong>App Access</strong>: If parts of your app are restricted (e.g., authentication required), provide instructions for the Google Play review team to access those areas.</li><li><strong>Ads</strong>: Indicate whether your app contains ads.</li><li><strong>Content Rating</strong>: Complete the content rating questionnaire to generate appropriate ratings for your app in different regions.</li><li><strong>Target Audience</strong>: Specify the target age group for your app. If your app targets children, you’ll need to meet additional compliance requirements.</li><li><strong>Address Specific Use Cases</strong>: Some tasks only apply to apps with specific use cases (e.g. news apps, government apps, financial features, health features). Complete the relevant sections as needed.</li></ul><h3><a id="data-safety" href="#data-safety">Data Safety</a></h3><p>You'll also need to fill the <strong>data safety</strong> section, which includes 5 separate steps:</p><figure><picture><source srcset="images/create-app-50-data-safety-overview.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Data safety" srcset="images/create-app-50-data-safety-overview.png 2x"/></picture></figure><p>Let's tackle them:</p><ol><li><strong>Overview</strong>: The first page provides an overview of what you need to disclose.</li></ol><ol start="2"><li><strong>Data collection and security</strong>: Here, you’ll answer questions about how your app collects and handles user data. Answer these questions accurately based on your app’s functionality and policies.</li></ol><ol start="3"><li><strong>Data Types</strong>: Here, you’ll declare all the data types your app collects or shares. These are grouped into categories such as, <strong>Location</strong>, <strong>Crash logs</strong>, <strong>Device or other IDs</strong>, <strong>App activity</strong>, <strong>Personal info</strong>, <strong>Financial info</strong>, and others.</li></ol><ol start="4"><li><strong>Data usage and handling</strong>: For each data type you selected, you’ll need to answer questions about:<ul><li><strong>Purpose</strong>: Why is the data collected (e.g., analytics, diagnostics)?</li><li><strong>Sharing</strong>: Is the data shared with third parties?</li><li><strong>User Choice</strong>: Can users opt out of data collection?</li><li><strong>Data Retention</strong>: Is the data processed ephemerally, retained temporarily, or stored long-term?</li></ul></li></ol><ol start="5"><li><strong>Preview</strong>: The final step is to review the summary that will appear on your app’s Play Store listing. This summary helps users understand how your app collects and handles their data.</li></ol><h2><a id="4-prepare-your-app-for-review-store-listing" href="#4-prepare-your-app-for-review-store-listing">4. Prepare your App For Review (Store Listing)</a></h2><p>Navigate to your app’s dashboard and locate the <strong>Manage how your app is organized and presented</strong> section under <strong>Set up your app</strong>:</p><figure><picture><source srcset="images/create-app-70.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Set up your app" srcset="images/create-app-70.png 2x"/></picture></figure><p>Click on <strong>Select an app category and provide contact details</strong> to begin.</p><h3><a id="store-settings" href="#store-settings">Store Settings</a></h3><p>In the <strong>Store settings</strong> page, you’ll set up your <strong>App category</strong> and <strong>Store Listing contact details</strong>.</p><ul><li><strong>App Category</strong>:<ul><li>Click <strong>Edit</strong>, then select <strong>App or Game</strong> and the appropriate <strong>category</strong>.</li><li>Click <strong>Manage tags</strong> to add up to five relevant tags that describe your app’s content and functionality. Tags improve your app’s discoverability in search results and recommendations.</li></ul></li></ul><ul><li><strong>Store Listing Contact Details</strong>:<ul><li>Click <strong>Edit</strong> and enter your email address (required), phone number (optional), and website (optional).</li></ul></li></ul><h3><a id="store-listing" href="#store-listing">Store Listing</a></h3><p>The <strong>Store Listing</strong> is the public-facing page where users will discover, download, and interact with your app on the Google Play Store.</p><p>From the app dashboard, click <strong>Set up your Store Listing</strong> and follow the steps below:</p><ul><li><strong>Listing assets</strong><ul><li><strong>App name</strong>: The name displayed on the Play Store.</li><li><strong>Short description</strong>: A brief, engaging snippet that appears in search results.</li><li><strong>Full description</strong>: A detailed explanation of your app’s features and benefits. Use clear, concise, and engaging language.</li></ul></li></ul><ul><li><strong>App Icon and Feature Graphic</strong>: Upload your <strong>app icon</strong> (512x512 px) and <strong>feature graphic</strong> (1024x500 px). These visuals are essential for branding and grabbing users’ attention.</li></ul><ul><li><strong>Phone and Tablet Screenshots</strong>: Upload <strong>2 to 8 screenshots</strong> (16:9 or 9:16 aspect ratio).</li></ul><h3><a id="select-countries-and-regions" href="#select-countries-and-regions">Select Countries and Regions</a></h3><p>Before moving on, let’s complete one more important step so you won’t forget later: selecting <strong>countries and regions</strong> where your app will be available.</p><p>Scroll down to the <strong>Create and publish a release</strong> section at the very bottom of your app’s dashboard:</p><figure><picture><source srcset="images/release-app-02-production.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Select countries and regions" srcset="images/release-app-02-production.png 2x"/></picture></figure><p>Click <strong>Select countries and regions</strong> to choose where your app will be available.</p><h2><a id="5-review-the-android-project-settings" href="#5-review-the-android-project-settings">5. Review the Android Project settings</a></h2><p>Before releasing your app, it’s essential to review your Android project settings to ensure compatibility with Google Play requirements and avoid potential errors.</p><p>Here's a handy checklist:</p><ul><li><strong>App Icons and Launch Screen</strong>: Ensure your app has a launcher icon and a splash screen tailored for Android. The <a href="https://pub.dev/packages/flutter_launcher_icons">flutter_launcher_icons</a> and <a href="https://pub.dev/packages/flutter_native_splash">flutter_native_splash</a> packages can help with this.</li><li><strong>Flavors Configuration (Optional)</strong>: If you’re using flavors to manage multiple app versions (e.g., free vs. paid), verify your Android flavor settings. My <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production course</a> includes an <a href="https://pro.codewithandrea.com/flutter-in-production/03-flavors/01-intro">entire module about flavors</a>.</li><li><strong>App Manifest File</strong>: Review your <code>AndroidManifest.xml</code> file for required permissions and settings. For example, add <code>&lt;uses-permission android:name="android.permission.INTERNET"/&gt;</code> if your app requires internet access, and verify plugin-specific requirements (e.g., <a href="https://pub.dev/packages/url_launcher">url_launcher</a>).</li><li><strong>Gradle Build Configuration</strong>: Update your <code>android/app/build.gradle</code> file to meet Google Play’s API level requirements:<ul><li>Set <code>compileSdkVersion</code> and <code>targetSdkVersion</code> to <strong>34</strong> (Android 14).</li><li>Confirm <code>minSdkVersion</code> is set to <strong>21</strong> to avoid multidex requirements.</li></ul></li></ul><blockquote><p>To more easily update the Gradle settings, use this: <a href="https://codewithandrea.com/tips/update-android-project-script/">Script to Update the Android Project Settings</a>.</p></blockquote><h3><a id="code-signing" href="#code-signing">Code signing</a></h3><p>Before you can release your app on the Google Play Store, it needs to be signed with a digital certificate. Code signing ensures that your app is authentic and hasn’t been tampered with after being published.</p><p>To enable code signing, you need to:</p><ol><li>Create an upload keystore.</li><li>Reference the keystore in your project.</li><li>Configure Gradle for signing release builds.</li></ol><p>These steps are all explained in detail in the official docs:</p><ul><li><a href="https://docs.flutter.dev/deployment/android#sign-the-app">Build and release an Android app &gt; Sign the app</a></li></ul><h2><a id="6-build-upload-and-submit-your-app-to-the-google-play-store" href="#6-build-upload-and-submit-your-app-to-the-google-play-store">6. Build, Upload, and Submit your App to the Google Play Store</a></h2><p>After configuring <strong>code signing</strong>, it’s time to build a release version of your app and upload it to Google Play.</p><blockquote><p>Here, we'll focus on how to publish your app on the <strong>production track</strong>, so that users can download and install from the Play Store. <strong>Internal</strong>, <strong>closed</strong>, and <strong>open-testing tracks</strong> also exist, and you can use them to distribute your app to different groups of users before going live. These are covered in my <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> course.</p></blockquote><h3><a id="step-1-update-the-app’s-version-number" href="#step-1-update-the-app’s-version-number">Step 1: Update the App’s Version Number</a></h3><p>Before building your app, double check the version number in the <code>pubspec.yaml</code> file:</p><pre><code><div class="highlight"><span></span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">0.3.4+18</span>
</div></code></pre><p><strong>What Does This Mean?</strong></p><ul><li><strong>0.3.4</strong>: The version name (major.minor.patch).</li><li><strong>18</strong>: The build number (used for versioning in Google Play).</li></ul><p>When you build the app, these values will update the <code>versionName</code> and <code>versionCode</code> in the <code>local.properties</code> file on Android.</p><p>You can also override the version name and build number during with this command:</p><pre><code><div class="highlight"><span></span>flutter<span class="w"> </span>build<span class="w"> </span>appbundle<span class="w"> </span>--build-name<span class="o">=</span><span class="m">0</span>.3.4<span class="w"> </span>--build-number<span class="o">=</span><span class="m">18</span>
</div></code></pre><p>This is useful if you’re using multiple release tracks (different app builds need to be uploaded for each testing or production track).</p><h3><a id="step-2-build-your-app-for-release" href="#step-2-build-your-app-for-release">Step 2: Build Your App for Release</a></h3><p>Google Play supports two release formats: <strong>App Bundle (AAB)</strong> (preferred) and <strong>APK</strong>.</p><p>To build an app bundle, run:</p><pre><code><div class="highlight"><span></span>flutter<span class="w"> </span>build<span class="w"> </span>appbundle<span class="w"> </span>--release
</div></code></pre><p>If successful, the app bundle will be generated at:</p><pre><code><div class="highlight"><span></span><span class="p">[</span><span class="n">project</span><span class="p">]</span><span class="o">/</span><span class="n">build</span><span class="o">/</span><span class="n">app</span><span class="o">/</span><span class="n">outputs</span><span class="o">/</span><span class="n">bundle</span><span class="o">/</span><span class="n">release</span><span class="o">/</span><span class="n">app</span><span class="p">.</span><span class="n">aab</span>
</div></code></pre><blockquote><p>To protect your Dart code from reverse-engineering, consider adding the <code>--obfuscate</code> and <code>--split-debug-info</code> flags. Learn more in the <a href="https://docs.flutter.dev/deployment/obfuscate">Dart Obfuscation Guide</a>.</p></blockquote><h3><a id="step-3-upload-to-google-play" href="#step-3-upload-to-google-play">Step 3: Upload to Google Play</a></h3><p>Select the <strong>Production</strong> track, then click <strong>Create new release</strong>:</p><figure><picture><source srcset="images/release-app-11-production-new-release.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Production, create new release" srcset="images/release-app-11-production-new-release.png 2x"/></picture></figure><p>Drag and drop your app bundle (<code>app.aab</code>) into the <strong>App bundles</strong> box:</p><figure><picture><source srcset="images/release-app-20-create-production-release.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Create production release" srcset="images/release-app-20-create-production-release.png 2x"/></picture></figure><p>Once uploaded, the bundle will be optimised for distribution (this may take a minute or so).</p><p>Then, it will appear above the <strong>Release details</strong> section, and the <strong>Release name</strong> will be pre-filled with the build version:</p><figure><picture><source srcset="images/release-app-21-uploaded-app-bundle.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Uploaded app bundle" srcset="images/release-app-21-uploaded-app-bundle.png 2x"/></picture></figure><p>Update the <strong>Release notes</strong> (e.g. "Initial release"), then click <strong>Next</strong>.</p><p>After completing all required steps, Google Play will display any errors or warnings. For example, you may see a warning about missing debug symbols:</p><figure><picture><source srcset="images/release-app-31-errors-warning-messages.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Errors, warnings and messages" srcset="images/release-app-31-errors-warning-messages.png 2x"/></picture></figure><blockquote><p>The warning above refers to uploading debug symbols for your <strong>Android native code</strong> (Kotlin or Java) and can be safely ignored. When building a Flutter app, most of your crashes are likely to happen in your Dart code. You only need to upload debug symbols for your Flutter app if you used the <code>--obfuscate</code> flag when building your app bundle.</p></blockquote><p>Once you click <strong>Save</strong>, a popup will appear. Click <strong>Go to overview</strong>:</p><figure><picture><source srcset="images/release-app-32-go-to-overview.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Go to Publishing overview?" srcset="images/release-app-32-go-to-overview.png 2x"/></picture></figure><h3><a id="step-4-submit-for-review" href="#step-4-submit-for-review">Step 4: Submit for Review</a></h3><p>At this stage, Google Play will perform some automated checks on your app bundle:</p><figure><picture><source srcset="images/release-app-33-publishing-overview-checks.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Running quick checks for commonly found issues" srcset="images/release-app-33-publishing-overview-checks.png 2x"/></picture></figure><p>If all checks pass, click on the button to <strong>Send changes for review</strong>:</p><p>Once the checks are done, the page will update and if there are no issues, you'll be able to send all the changes for review:</p><figure><picture><source srcset="images/release-app-34-send-changes-for-review.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Changes not yet sent for review" srcset="images/release-app-34-send-changes-for-review.png 2x"/></picture></figure><p>After submission, your app will enter the review process, which can take several days.</p><figure><picture><source srcset="images/release-app-36-changes-in-review.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Changes in review" srcset="images/release-app-36-changes-in-review.png 2x"/></picture></figure><p>Congratulations! You've now submitted the first version of your app to the Play Store! 🎉</p><blockquote><p>At the top of the Publishing overview, you can find an option about <strong>Managed publishing</strong>. <strong>Turn on</strong> this option if you want control when approved changes are published on Google Play.</p></blockquote><h3><a id="what-happens-next?" href="#what-happens-next?">What Happens Next?</a></h3><p>Once you’ve submitted your app, it enters the <strong>App Review Process</strong>, where Google Play evaluates your app for compliance with its <a href="https://play.google/developer-content-policy/">Developer Policies</a>.</p><p>While you wait, keep an eye on your <strong>Inbox</strong> in the Google Play Console. This is the best place to track important updates and communications from Google Play:</p><figure><picture><source srcset="images/review-01-inbox.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Google Play Console Inbox" srcset="images/review-01-inbox.png 2x"/></picture></figure><p>To ensure you don’t miss critical notifications, turn on <strong>Email notifications</strong> for <strong>Publishing updates</strong>.</p><h3><a id="what-if-your-app-is-rejected?" href="#what-if-your-app-is-rejected?">What if your app is rejected?</a></h3><p>Rejections happen, even to experienced developers. If your app is rejected, don’t panic—follow these steps to resolve the issue:</p><ul><li><strong>Review the Rejection Reasons</strong>: Google Play will send you an email outlining the reasons for the rejection. This email usually includes links to the <a href="https://play.google/developer-content-policy/">Developer Policy Center</a> to help you understand the specific guidelines you violated.</li><li><strong>Fix the Issues</strong>: Make the necessary changes to your app to address the rejection reasons. This might involve updating your app metadata, fixing <strong>technical issues</strong> like crashes or bugs, or adjusting your app’s functionality to comply with policies.</li><li><strong>Resubmit Your App</strong>: Once you’ve resolved the issues, upload a new build of your app, update the metadata if necessary, and resubmit your app for review.</li><li><strong>Appeal If Necessary</strong>: If you believe your app was unfairly rejected, you can <a href="https://support.google.com/googleplay/android-developer/troubleshooter/2993242">submit an appeal</a>. Be sure to provide clear and concise reasoning, along with evidence supporting your case.</li></ul><h2><a id="7-submitting-app-updates-to-the-google-play-store" href="#7-submitting-app-updates-to-the-google-play-store">7. Submitting App Updates to the Google Play Store</a></h2><p>Keeping your app updated is essential for maintaining user satisfaction and staying competitive on the Play Store. Regular updates allow you to:</p><ul><li><strong>Fix bugs</strong> to improve user experience and app stability.</li><li><strong>Introduce new features</strong> to keep your app relevant and engaging.</li><li><strong>Comply with new Google Play policies</strong> or platform changes (e.g., updated API level requirements).</li></ul><p>Once you're ready, follow these steps to submit an app update:</p><ul><li><strong>Update the Version and Build Number</strong>: If you forget to do this, the upload will fail with an error, and you'll have to make a new build.</li><li><strong>Build a New App Bundle</strong>: Run the <code>flutter build appbundle --release</code> command (with any other flags that are needed by your app).</li><li><strong>Create a New Production Release</strong>: In the Google Play Console, go to the <strong>Production</strong> track and click <strong>Create new Release</strong>.</li><li><strong>Upload the App Bundle</strong>: Drag and drop your updated app bundle (<code>app-prod-release.aab</code>) into the <strong>App bundles</strong> box.</li><li><strong>Update the Release Notes</strong>: Scroll down to the <strong>Release notes</strong> section, where you can describe what’s new in this update.</li><li><strong>Review and Save</strong>: Google Play will check for errors or warnings. If there are no errors, click <strong>Save</strong>.</li><li><strong>Submit for Review</strong>: Once everything is ready, go to the <strong>Publishing overview</strong> and click <strong>Send change for review</strong>.</li></ul><p>Your app will enter the review process, just like when you first published it.</p><h2><a id="wrapping-up" href="#wrapping-up">Wrapping Up</a></h2><p>This article presented a clear roadmap for releasing your app on the Play Store. From account setup to submission and updates, you now have a clear guide for navigating the process successfully.</p><p>Note that making releases by hand can be time consuming, especially if you release often. Here's a solution. 👇</p><h3><a id="scaling-with-cicd-pipelines" href="#scaling-with-cicd-pipelines">Scaling with CI/CD Pipelines</a></h3><p>If you work as part of a team or release updates often, consider setting up <strong>CI/CD pipelines</strong> to automate your app builds, testing, and releases. CI/CD enables you to:</p><ul><li>Save time by automating repetitive tasks.</li><li>Run specific workflows (e.g. run tests) when certain <a href="https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#on">events are triggered</a> (e.g. when pushing a branch).</li><li>Distribute apps more efficiently via TestFlight or the App Store.</li></ul><p>To dive deeper into these topics, and many more, you can explore my new course. 👇</p><h2><a id="new-course-flutter-in-production" href="#new-course-flutter-in-production">New Course: Flutter in Production</a></h2><p>When it comes to <strong>shipping</strong> and <strong>maintaining</strong> apps in production, there are many important aspects to consider:</p><ul><li><strong>Preparing for release</strong>: splash screens, flavors, environments, error reporting, analytics, force update, privacy, T&amp;Cs.</li><li><strong>App Submissions</strong>: app store metadata &amp; screenshots, compliance, testing vs distribution tracks, dealing with rejections.</li><li><strong>Release automation:</strong> CI workflows, environment variables, custom build steps, code signing, uploading to the stores.</li><li><strong>Post-release</strong>: error monitoring, bug fixes, addressing user feedback, over-the-air updates, feature flags &amp; A/B testing.</li></ul><p>My latest course will help you get your app to the stores faster and with fewer headaches.</p><p>If you’re interested, you can learn more and enroll here. 👇</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/move-to-file-vscode-assist/</guid><title>Move Declaration to File (VSCode assist)</title><description>With VSCode, you can easily move your Dart classes and functions to a different file (this is very useful when refactoring).</description><link>https://codewithandrea.com/tips/move-to-file-vscode-assist/</link><pubDate>Wed, 8 Jan 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>With VSCode, you can easily move your Dart classes and functions to a different file.</p><p>To use this, select any declaration name and press <code>CMD+.</code>, then use the Move option.</p><p>The desired file will be created with all the required imports. 👍</p><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Move Declaration to File (VSCode assist)" srcset="images/twitter-card.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/stack-fractionally-sized-box/</guid><title>Using Stack and FractionallySizedBox</title><description>Here's how to overlay multiple widgets inside a Stack and constrain their size and position with Positioned and FractionallySizedBox.</description><link>https://codewithandrea.com/tips/stack-fractionally-sized-box/</link><pubDate>Tue, 7 Jan 2025 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Flutter offers many ways to create custom layouts that can’t be expressed with <code>Row</code> and <code>Column</code>. 👍</p><p>For example, here's how to overlay multiple widgets inside a <a href="https://api.flutter.dev/flutter/widgets/Stack-class.html"><code>Stack</code></a> and constrain their size and position with <a href="https://api.flutter.dev/flutter/widgets/Positioned-class.html"><code>Positioned</code></a> and <a href="https://api.flutter.dev/flutter/widgets/FractionallySizedBox-class.html"><code>FractionallySizedBox</code></a>. 👇</p><figure><picture><source srcset="images/221.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Using Stack and FractionallySizedBox" srcset="images/221.png 2x"/></picture></figure><h3><a id="example-code" href="#example-code">Example Code</a></h3><pre><code><div class="highlight"><span></span><span class="k">const</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">4</span><span class="p">;</span>
<span class="c1">// * Aspect ratio = 1 yields a square</span>
<span class="k">return</span><span class="w"> </span><span class="n">AspectRatio</span><span class="p">(</span>
<span class="w">  </span><span class="nl">aspectRatio:</span><span class="w"> </span><span class="m">1</span><span class="p">,</span>
<span class="w">  </span><span class="c1">// * Stack the squares on top of each other</span>
<span class="w">  </span><span class="nl">child:</span><span class="w"> </span><span class="n">Stack</span><span class="p">(</span>
<span class="w">    </span><span class="c1">// * Generate n squares</span>
<span class="w">    </span><span class="nl">children:</span><span class="w"> </span><span class="n">List</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span><span class="n">n</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="n">index</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="c1">// * Material colors have shades from 100 to 900</span>
<span class="w">      </span><span class="kd">final</span><span class="w"> </span><span class="n">color</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Colors</span><span class="p">.</span><span class="n">indigo</span><span class="p">[(</span><span class="n">index</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="m">2</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="m">100</span><span class="p">]</span><span class="o">!</span><span class="p">;</span>
<span class="w">      </span><span class="c1">// * Fill the entire surface</span>
<span class="w">      </span><span class="k">return</span><span class="w"> </span><span class="n">Positioned</span><span class="p">.</span><span class="n">fill</span><span class="p">(</span>
<span class="w">        </span><span class="c1">// * Size child to a fraction of the available space</span>
<span class="w">        </span><span class="nl">child:</span><span class="w"> </span><span class="n">FractionallySizedBox</span><span class="p">(</span>
<span class="w">          </span><span class="c1">// * Pick width and height between 0 and 1</span>
<span class="w">          </span><span class="nl">widthFactor:</span><span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">index</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="n">n</span><span class="p">,</span>
<span class="w">          </span><span class="nl">heightFactor:</span><span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">index</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="n">n</span><span class="p">,</span>
<span class="w">          </span><span class="c1">// * Choose the alignment of the child</span>
<span class="w">          </span><span class="nl">alignment:</span><span class="w"> </span><span class="n">Alignment</span><span class="p">.</span><span class="n">topRight</span><span class="p">,</span>
<span class="w">          </span><span class="c1">// * Just a colored box</span>
<span class="w">          </span><span class="nl">child:</span><span class="w"> </span><span class="n">ColoredBox</span><span class="p">(</span><span class="nl">color:</span><span class="w"> </span><span class="n">color</span><span class="p">),</span>
<span class="w">        </span><span class="p">),</span>
<span class="w">      </span><span class="p">);</span>
<span class="w">    </span><span class="p">}),</span>
<span class="w">  </span><span class="p">),</span>
<span class="p">);</span>
</div></code></pre><h3><a id="challenge" href="#challenge">Challenge</a></h3><p><strong>Bonus</strong>: Try to implement a chess board layout using <code>Stack</code> and <code>FractionallySizedBox</code> by completing this challenge:</p><ul><li><a href="https://pro.codewithandrea.com/flutter-ui-challenges/006-chess-board/01-intro">Challenge: Chess Board</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/december-2024/</guid><title>December 2024: Flutter 3.27 Release, Architecture Case Study, Dart Microbenchmarks, and Union Types</title><description>Also included: Dart 3.6, Flutter AI Toolkit, Flutter in Production livestream, and the latest from Code with Andrea.</description><link>https://codewithandrea.com/newsletter/december-2024/</link><pubDate>Mon, 23 Dec 2024 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Welcome to my final Flutter newsletter of 2024!</p><p>This edition is packed with exciting updates:</p><ul><li>The mighty and much-awaited Flutter 3.27 release</li><li>Flutter's Production Era (livestream recap)</li><li>A new official architecture case study</li><li>Deep-dive articles about benchmarks and union types</li><li>Latest updates from Code with Andrea</li></ul><p>So grab a drink, relax, and enjoy the read! 🍷</p><h2><a id="flutter-327-release" href="#flutter-327-release">Flutter 3.27 Release</a></h2><p>Flutter 3.27 is a <strong>big</strong> release that brings many updates across the framework, engine, and ecosystem. Highlights include:</p><ul><li>High-fidelity updates for Cupertino widgets</li><li>Long-awaited <a href="https://codewithandrea.com/tips/spacing-row-column/">Row and Column spacing</a> support</li><li>Impeller as the default rendering engine on Android</li><li>Full support for <a href="https://en.wikipedia.org/wiki/RGB_color_spaces">wide gamut color spaces</a> (with some <a href="https://codewithandrea.com/tips/color-deprecations-flutter-3-27/">Color API deprecations</a>)</li><li>Swift Package Manager support (now used by <a href="https://github.com/firebase/flutterfire/issues/13205">Firebase</a> and <a href="https://github.com/fluttercommunity/plus_plugins/issues/3152">plus plugins</a>)</li><li>Support for edge-to-edge mode on Android 15+</li></ul><p>Read the full announcement:</p><ul><li><a href="https://medium.com/flutter/whats-new-in-flutter-3-27-28341129570c">What’s New in Flutter 3.27</a></li></ul><h3><a id="📝-announcing-dart-36" href="#📝-announcing-dart-36">📝 Announcing Dart 3.6</a></h3><p>Released alongside Flutter 3.27, Dart 3.6 introduces <strong>pub workspaces</strong>. This allows the Flutter analyzer to process all of the packages in a pub workspace in a single analysis context. For large repositories, this can significantly reduce the memory used by the Dart language server, improving IDE performance.</p><p>Another nice addition is the support for <a href="https://codewithandrea.com/tips/digits-separators-dart-3-6/">digit separators</a>, for better readability of large numbers. Learn more here:</p><ul><li><a href="https://medium.com/dartlang/announcing-dart-3-6-778dd7a80983">Announcing Dart 3.6</a></li></ul><h3><a id="📝-announcing-flutter-ai-toolkit" href="#📝-announcing-flutter-ai-toolkit">📝 Announcing Flutter AI Toolkit</a></h3><p>The Flutter team also announced the <a href="https://medium.com/flutter/announcing-flutter-ai-toolkit-e36b16a840d2">Flutter AI Toolkit</a>, a collection of ready-to-use AI chat widgets designed to seamlessly integrate into your Flutter projects (a sample <a href="https://github.com/csells/flutter_ai_chat/tree/main">Flutter AI chat app</a> is also available).</p><p>The toolkit provides features like rich text display, voice input, multimedia attachments, and more, making it easier to add an AI chat experience to your app. The toolkit also offers pluggable LLMs, meaning you can use it with different AI models.</p><p>Here's the full announcement:</p><ul><li><a href="https://medium.com/flutter/announcing-flutter-ai-toolkit-e36b16a840d2">Announcing Flutter AI Toolkit</a></li></ul><blockquote><p>Note: the <a href="https://pub.dev/packages/flutter_ai_toolkit">flutter_ai_toolkit</a> package uses Firebase under the hood. If you don't want to use Firebase, it may not be the best fit for your project.</p></blockquote><h2><a id="flutter-in-production-event" href="#flutter-in-production-event">Flutter in Production Event</a></h2><p>Last week’s <strong>#FlutterInProduction</strong> livestream celebrated Flutter’s 10-year journey. Some stats stood out:</p><ul><li>1M+ monthly active Flutter developers 📈</li><li>28% of new iOS apps on the App Store use Flutter 🤯</li></ul><p>The livestream also offered a peek into some experiments and planned features, including:</p><ul><li><strong><a href="https://docs.google.com/document/d/1iMHDjC8HY_0xoOh1soxIf3MWLCtz4nD_Vn2goodO5YA/edit?tab=t.0">Flutter Widget Previews</a></strong> for visual editing</li><li><strong>Direct native interoperability</strong>, which will offer direct access to the latest native APIs (no platform channels required)</li><li><strong>Dart language improvements</strong>, including decorators, <a href="https://github.com/dart-lang/language/issues/357">enum shorthands</a>, and <a href="https://github.com/dart-lang/language/issues/2364">primary constructors</a></li></ul><blockquote><p>After the livestream, Matt Carroll expressed <a href="https://x.com/SuprDeclarative/status/1869105836779590113">strong opinions</a> and concerns about decorators. As a follow up, Remi Rousselet made an interesting proposal that uses a <a href="https://github.com/dart-lang/language/issues/4211">pipe operator to reduce nesting</a>. For now, keep in mind that decorators are only an experiment—time will tell if something will come out of this.</p></blockquote><p>If you missed the livestream, you can watch it on YouTube:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="AEXIrThTgb0"></div></div><p>A written recap is also available:</p><ul><li><a href="https://medium.com/flutter/flutter-in-production-f9418261d8e1">Celebrating Flutter’s “Production Era”</a></li></ul><h3><a id="✨-flutter-is-now-a-monorepo!" href="#✨-flutter-is-now-a-monorepo!">✨ Flutter is Now a Monorepo!</a></h3><p>Here’s a big change that flew under the radar: the Flutter engine <a href="https://github.com/flutter/flutter/issues/160628">has been merged</a> into the main Flutter repository! 🎉</p><p>This brings some clear benefits:</p><ul><li>Framework and engine code are now built, tested, and shipped together</li><li>Improved efficiency and collaboration for the Flutter team and external contributors</li></ul><h2><a id="architecture-case-study" href="#architecture-case-study">Architecture Case Study</a></h2><p><a href="https://codewithandrea.com/newsletter/november-2024/">Last month</a>, I've mentioned that the Flutter docs have a new guide about <a href="https://docs.flutter.dev/app-architecture">app architecture</a>.</p><p>Since then, this <a href="https://docs.flutter.dev/app-architecture/case-study">architecture case study</a> has been added, showing how the official <a href="https://github.com/flutter/samples/tree/main/compass_app">compass sample app</a> was built.</p><p>The guide covers:</p><ul><li>How to implement Flutter's app architecture guidelines, using <strong>views</strong>, <strong>view models</strong>, <strong>repositories</strong> and <strong>services</strong></li><li>How to use the <a href="https://docs.flutter.dev/app-architecture/case-study/ui-layer#command-objects">Command pattern</a> to render UI as data changes</li><li>How to use <a href="https://api.flutter.dev/flutter/foundation/ChangeNotifier-class.html">ChangeNotifier</a> and <a href="https://api.flutter.dev/flutter/foundation/Listenable-class.html">Listenable</a> objects to manage state</li><li>How to implement <a href="https://docs.flutter.dev/app-architecture/case-study/dependency-injection">Dependency Injection</a> and <a href="https://docs.flutter.dev/app-architecture/case-study/testing">test each layer</a> in the architecture</li></ul><p>For all the details, check the full case study:</p><ul><li><a href="https://docs.flutter.dev/app-architecture/case-study">Architecture case study</a></li></ul><blockquote><p>In my opinion, the guide does a good job at explaining important <a href="https://docs.flutter.dev/app-architecture/concepts">architectural concepts</a> and offers a good overview of how the <a href="https://docs.flutter.dev/app-architecture/case-study/ui-layer">UI layer</a> and <a href="https://docs.flutter.dev/app-architecture/case-study/data-layer">data layer</a> interact with each other. While implementation details such as using <code>ChangeNotifier</code> and the <a href="https://pub.dev/packages/provider">provider package</a> may work well for the <a href="https://github.com/flutter/samples/tree/main/compass_app">compass sample app</a>, they may fall a bit short for more complex apps. <strong>Bottom line</strong>: use the principles as guidance and find the most suitable implementation based on your preferences and project requirements.</p></blockquote><h2><a id="flutter-articles" href="#flutter-articles">Flutter Articles</a></h2><p>This month, I'm sharing two very good articles about Dart microbenchmarks and Union Types.</p><h3><a id="📝-microbenchmarks-are-experiments" href="#📝-microbenchmarks-are-experiments">📝 Microbenchmarks are experiments</a></h3><p>Lately, someone posted this <a href="https://x.com/BenjDicken/status/1861072804239847914">pretty visualization</a> comparing the performance of various programming languages on a <strong>meaningless benchmark</strong>.</p><p>While such posts are good for engagement farming, the devil is in the details. So I was extremely pleased when <a href="https://x.com/mraleph">Slava Egorov (tech lead of the Dart Language)</a> posted this take:</p><blockquote><p>Sigh. 2025 is almost around the corner and people still seem to think that the goal of a benchmark is to produce a number which by itself reveals some hidden truth about the world. You write a loop in Dart, you write a loop in C. You run it three times (statistics!). You compare the running time and, lo and behold, these two numbers reveal you all you need to know about Dart and C performance. The rest of the day can be spent browsing numerological magazines for the meaning of your date of birth…</p></blockquote><p>He then proceeded with a detailed breakdown of the benchmark, including a low-level performance comparison between JavaScript and Dart (yay, assembly code!).</p><p>The full post is very much worth a read:</p><ul><li><a href="https://mrale.ph/blog/2024/11/27/microbenchmarks-are-experiments.html">Microbenchmarks are experiments</a></li></ul><blockquote><p>Ironically, OpenAI recently <a href="https://x.com/OpenAI/status/1870186518230511844">announced their latest o3 model</a>, showing that o3 achieved a 87.5% score on the <a href="https://arcprize.org/arc">ARC-AGI</a> benchmark and implying that it is close to <a href="https://en.wikipedia.org/wiki/Artificial_general_intelligence">AGI</a>. As it turns out, the model itself was <a href="https://x.com/GaryMarcus/status/1870593476104176086">specifically trained</a> on the benchmark 🤦‍♂️. <strong>Bottom line</strong>: things aren't always as they seem, and some good old critical thinking is required to discern between hype and reality.</p></blockquote><h3><a id="📝-demystifying-union-types-in-dart-tagged-vs-untagged-once-and-for-all" href="#📝-demystifying-union-types-in-dart-tagged-vs-untagged-once-and-for-all">📝 Demystifying Union Types in Dart, Tagged vs. Untagged, Once and For All</a></h3><p>Union types are a concept that appears in many languages, but they are often misunderstood.</p><p>This article by <a href="https://bsky.app/profile/mhadaily.bsky.social">Majid Hajian</a> is a deep-dive into Union Types (untagged and tagged unions)—what they are, why they’re helpful, where they fall short, and why, in strongly typed languages like Dart, you might want to reconsider their use.</p><p>Read on to get a better conceptual understanding of union types, so you can choose the right language features or constructs (e.g. sealed classes) depending on your use case:</p><ul><li><a href="https://dcm.dev/blog/2024/12/10/demystifying-union-types-dart-tagged-untagged/">Demystifying Union Types in Dart, Tagged vs. Untagged, Once and For All</a></li></ul><h2><a id="latest-from-code-with-andrea" href="#latest-from-code-with-andrea">Latest from Code with Andrea</a></h2><p>Since the <a href="https://codewithandrea.com/newsletter/november-2024/">last newsletter</a>, I've been quite busy:</p><ul><li>Launched a new version of my <a href="https://fluttertips.dev/">Flutter Tips app</a> (with nicer <a href="https://play.google.com/store/apps/details?id=com.codewithandrea.flutter_tips_and_tricks">app store</a> <a href="https://apps.apple.com/us/app/flutter-tips/id6482293361?uo=4">screenshots</a> 🙂)</li><li>Published <a href="https://codewithandrea.com/tips/">13 new tips</a></li><li>Published a <a href="https://codewithandrea.com/articles/how-to-release-flutter-ios-app-store/">new article</a> and updated an <a href="https://codewithandrea.com/articles/robust-app-initialization-riverpod/">old favorite</a></li><li>Revamped my newsletter and archived all the past editions on <a href="https://codewithandrea.com/newsletter/archive/">this page</a></li><li>Added a <a href="https://pro.codewithandrea.com/flutter-in-production/10-release-android/01-intro">big module about Android app releases</a> to my <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> course</li></ul><p>If you missed them, here's a summary of the latest articles:</p><h3><a id="📝-how-to-release-your-flutter-app-on-the-ios-app-store" href="#📝-how-to-release-your-flutter-app-on-the-ios-app-store">📝 How to Release Your Flutter App on the iOS App Store</a></h3><p>The Flutter documentation already provides a <a href="https://docs.flutter.dev/deployment/ios">good technical guide</a> for building and releasing an iOS app. But publishing your first app involves more than just building and uploading. There are many steps to follow, and they aren’t always obvious.</p><p>This article will provide a high-level roadmap for releasing your Flutter app on the App Store. Even if you’re familiar with this process, you may discover a trick or two along the way.</p><ul><li><a href="https://codewithandrea.com/articles/how-to-release-flutter-ios-app-store/">How to Release Your Flutter App on the iOS App Store</a></li></ul><h3><a id="📝-how-to-build-a-robust-flutter-app-initialization-flow-with-riverpod" href="#📝-how-to-build-a-robust-flutter-app-initialization-flow-with-riverpod">📝 How to Build a Robust Flutter App Initialization Flow with Riverpod</a></h3><p>Earlier this year, I shared an article about <strong>stateful app initialization</strong> with Riverpod.</p><p>Since then, I discovered a flaw that was breaking URL-based navigation in my original setup. So, after finding the solution, I've rewritten the article entirely. 👇</p><ul><li><a href="https://codewithandrea.com/articles/robust-app-initialization-riverpod/">How to Build a Robust Flutter App Initialization Flow with Riverpod</a></li></ul><p>I've also updated my Starter Architecture repo to use the new technique:</p><ul><li><a href="https://github.com/bizz84/starter_architecture_flutter_firebase">Time Tracking app with Flutter &amp; Firebase</a></li></ul><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>2024 is nearly over and this year, I've shared <a href="https://codewithandrea.com/articles/">12 new articles</a> and <a href="https://codewithandrea.com/tips/">81 new tips</a> about Flutter app development.</p><p>Over the next week, I'll take some time to reflect on my journey as a Flutter educator and share my 2024 retro (if you're curious, here’s the <a href="https://codewithandrea.com/meta/my-2023-retro/">2023 edition</a>).</p><p>But for now, I want to thank you for reading and supporting my work this year, and I wish you a happy festive season! 🎉</p><p>Happy coding!</p><p>Andrea</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/list-wheel-scroll-view/</guid><title>The ListWheelScrollView Widget</title><description>If you need to select between a small list of values but have limited vertical space, you can use a ListWheelScrollView.</description><link>https://codewithandrea.com/tips/list-wheel-scroll-view/</link><pubDate>Fri, 20 Dec 2024 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If you need to select between a small list of values but have limited vertical space, you can use a <code>ListWheelScrollView</code>. 🎯</p><p>Pro Tip: use <code>FixedExtentScrollPhysics</code> to snap to the nearest item when the user stops scrolling. 👍</p><figure><picture><img class="bottom-40px" alt="The ListWheelScrollView Widget" srcset="images/220.gif 2x"/></picture></figure><h3><a id="how-to-use-it" href="#how-to-use-it">How to use it</a></h3><ul><li>call the regular <code>ListWheelScrollView</code> constructor and pass a list of children.</li><li>call <code>ListWheelScrollView.useDelegate</code> and pass a <code>ListWheelChildDelegate</code>.</li></ul><p>For all the details, check the official docs:</p><ul><li><a href="https://api.flutter.dev/flutter/widgets/ListWheelScrollView-class.html">ListWheelScrollView class</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/color-deprecations-flutter-3-27/</guid><title>Color API Deprecations in Flutter 3.27</title><description>To support the latest wide gamut color spaces, Flutter 3.27 has deprecated some properties and methods in the Color class.</description><link>https://codewithandrea.com/tips/color-deprecations-flutter-3-27/</link><pubDate>Wed, 18 Dec 2024 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>To support the latest <a href="https://en.wikipedia.org/wiki/RGB_color_spaces">wide gamut color spaces</a>, Flutter 3.27 has deprecated some properties and methods in the <code>Color</code> class. 🌈</p><p>⚠️ No Dart fix command is available for this breaking change, so you'll need to migrate your code manually.</p><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Color API Deprecations in Flutter 3.27" srcset="images/twitter-card.png 2x"/></picture></figure><p>Learn more about this breaking change here:</p><ul><li><a href="https://docs.flutter.dev/release/breaking-changes/wide-gamut-framework">Migration guide for wide gamut Color</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/text-style-tabular-figures/</guid><title>Text Style with Tabular Figures</title><description>If you want to render fixed width (monospaced) digits, set FontFeature.tabularFigures() inside your TextStyle.</description><link>https://codewithandrea.com/tips/text-style-tabular-figures/</link><pubDate>Tue, 17 Dec 2024 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If you want to render fixed width (monospaced) digits, set <a href="https://api.flutter.dev/flutter/dart-ui/FontFeature/FontFeature.tabularFigures.html"><code>FontFeature.tabularFigures()</code></a> inside your <code>TextStyle</code>. 🎯</p><p>This works great when showing numbers and dates that should align vertically or update in realtime! 🔥</p><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Text Style with Tabular Figures" srcset="images/twitter-card.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/digits-separators-dart-3-6/</guid><title>Digits Separators in Dart 3.6</title><description>Since Dart 3.6 (Flutter 3.27), you can use _ as a digits separator. This works with integers and floats, as well as custom formats (hex, scientific).</description><link>https://codewithandrea.com/tips/digits-separators-dart-3-6/</link><pubDate>Fri, 13 Dec 2024 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Since Dart 3.6 (Flutter 3.27), you can use <code>_</code> as a digits separator. 🎯</p><p>This works with integers and floats, as well as custom formats (hex, scientific).</p><p>To enable this, bump the Dart SDK to 3.6 in your <code>pubspec.yaml</code>.</p><figure><picture><source srcset="images/217.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Digits Separators in Dart 3.6" srcset="images/217.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/spacing-row-column/</guid><title>New Spacing Argument in Row/Column (Flutter 3.27)</title><description>Since Flutter 3.27, you can pass a spacing argument to your Row and Column widgets (rather than using SizedBox).</description><link>https://codewithandrea.com/tips/spacing-row-column/</link><pubDate>Thu, 12 Dec 2024 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Since Flutter 3.27, you can pass a <code>spacing</code> argument to your <code>Row</code> and <code>Column</code> widgets. ✅</p><p>This means you no longer need a <code>SizedBox</code> to add fixed spacing between each child. 🚀</p><figure><picture><img class="bottom-40px" alt="New Spacing Argument in Row/Column" srcset="images/216.1.png 2x"/></picture></figure><h3><a id="combining-spacing-and-flex" href="#combining-spacing-and-flex">Combining spacing and flex</a></h3><p>If you want, you can combine spacing and flex together:</p><figure><picture><img class="bottom-40px" alt="Combining spacing and flex" srcset="images/216.2.png 2x"/></picture></figure><p>This makes it easier to mix <strong>fixed</strong> and <strong>proportional</strong> spacing when laying out the children.</p><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/banner-widget/</guid><title>The Banner Widget</title><description>You can use the Banner widget to place a small diagonal banner over a child widget. For more custom styling, build your own using a custom painter.</description><link>https://codewithandrea.com/tips/banner-widget/</link><pubDate>Tue, 10 Dec 2024 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>You can use the <a href="https://api.flutter.dev/flutter/widgets/Banner-class.html"><code>Banner</code></a> widget to place a small diagonal banner over a child widget.</p><figure><picture><source srcset="images/215.webp 2x" type="image/webp"/><img class="bottom-40px" alt="The Banner Widget" srcset="images/215.png 2x"/></picture></figure><p>Note that <code>Banner</code> only offers limited customization options. If you need more custom styling, you have two options:</p><ul><li>Build your own using a custom painter.</li><li>Use the <a href="https://pub.dev/packages/super_banners">super_banners</a> package.</li></ul><blockquote><p><code>Banner</code> is very closely related to <a href="https://api.flutter.dev/flutter/widgets/Banner-class.html"><code>CheckedModeBanner</code></a>, which shows inside your <code>MaterialApp</code> in debug mode. For more info, check the official docs: <a href="https://api.flutter.dev/flutter/widgets/Banner-class.html">Banner class</a>.</p></blockquote><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/cursor-edit-mode/</guid><title>Improve your code with Cursor Edit Mode</title><description>You can use Cursor edit mode to sweep through your code and make changes. Works best with imperative code.</description><link>https://codewithandrea.com/tips/cursor-edit-mode/</link><pubDate>Thu, 5 Dec 2024 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>The Cursor IDE has a powerful edit mode (hit <code>CMD+K</code> to enable it).</p><p>I find it quite useful for finding edge cases in my imperative code.</p><p>It doesn't always get it right, though! So make sure you tweak the output as needed and discard any unwanted changes. 🙇‍♂️</p><figure><picture><img class="bottom-40px" alt="Improve your code with Cursor Edit Mode" srcset="images/214.gif 1x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/fixing-version-solving-failed-errors/</guid><title>Fixing Version Solving Failed Errors</title><description>A few suggestions for solving the "version solving failed" error when updating your Flutter dependencies.</description><link>https://codewithandrea.com/tips/fixing-version-solving-failed-errors/</link><pubDate>Wed, 4 Dec 2024 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>When you get a "version solving failed" error, how can you fix it?</p><p>Some useful tips:</p><ol><li>Use <code>flutter pub upgrade</code>, don't change versions manually 💡</li><li>Read the error logs 🧐</li><li>Remove all version constraints 👻</li><li>Update your Podfile 🍏</li><li>Update the Android project settings 🤖</li></ol><figure><picture><img class="bottom-40px" alt="Fixing Version Solving Failed Errors" srcset="images/213.1.png 2x"/></picture></figure><p>Some more details:</p><ol><li><code>flutter pub upgrade</code> is your friend. It will try to resolve all dependencies without causing conflicts</li><li>Be persistent. Inspect the error log closely and see if you can figure it out</li><li>Remove version constraints from conflicting packages and try again</li></ol><figure><picture><img class="bottom-40px" alt="Fixing Version Solving Failed Errors" srcset="images/213.2.png 2x"/></picture></figure><p>Here's some more info about the <code>flutter pub upgrade</code> command:</p><ul><li><a href="https://codewithandrea.com/tips/flutter-pub-upgrade/">What does flutter pub upgrade do?</a></li></ul><p>If you need to update the Android project settings, this script may help:</p><ul><li><a href="https://codewithandrea.com/tips/update-android-project-script/">Script to Update the Android Project Settings</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/articles/how-to-release-flutter-ios-app-store/</guid><title>How to Release Your Flutter App on the iOS App Store</title><description>A step-by-step guide on how to publish your Flutter app, including metadata, compliance, privacy manifests, Xcode settings, and building your IPA file.</description><link>https://codewithandrea.com/articles/how-to-release-flutter-ios-app-store/</link><pubDate>Tue, 3 Dec 2024 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Your Flutter app is ready, and you’ve completed all the important <a href="https://codewithandrea.com/articles/key-steps-before-releasing-flutter-app/">pre-release steps</a>. Now it’s time for the final step: publishing it on the <strong>App Store</strong> and sharing it with the world. But where do you start?</p><p>Sure, the Flutter documentation provides a technical guide for <a href="https://docs.flutter.dev/deployment/ios">building and releasing an iOS app</a>. But publishing your first app involves more than just building and uploading—it’s a process filled with <strong>red tape</strong>, <strong>guidelines</strong>, and <strong>steps that aren’t always obvious</strong>.</p><p>If you’re new to this, don’t worry. This article will provide a <strong>high-level roadmap</strong> for releasing your Flutter app on the App Store. Even if you’re familiar with this process, you may discover a trick or two along the way. 👍</p><h3><a id="what-you’ll-learn" href="#what-you’ll-learn">What You’ll Learn:</a></h3><ol><li>How to <strong>enroll in the Apple Developer Program</strong>.</li><li>How to <strong>register your App ID</strong> in the Apple Developer Portal.</li><li>How to <strong>create your app</strong> in App Store Connect.</li><li>How to <strong>prepare your app for review</strong>, including metadata, privacy, and compliance.</li><li>How to <strong>create a Privacy Manifest</strong> in Xcode.</li><li>How to <strong>update the Xcode project settings</strong>, including code signing.</li><li>How to <strong>build, upload, and submit</strong> your app for App Store review.</li></ol><blockquote><p>Want a more detailed, hands-on guide? My <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> course includes a complete module with 20 lessons that cover each step in depth—from account setup to submission and beyond.</p></blockquote><p>Let’s dive in! 🚀</p><h2><a id="should-you-sell-apps-on-the-app-store?" href="#should-you-sell-apps-on-the-app-store?">Should You Sell Apps on the App Store?</a></h2><p>For many developers, the <strong>App Store</strong> is the ultimate platform for reaching a high-quality audience. Apple users are far more willing to pay for apps compared to Android users (it’s <a href="https://9to5mac.com/2023/09/06/iphone-users-spend-apps/">not even close</a>). And with payments seamlessly linked to their Apple ID, purchasing is frictionless.</p><p>However, joining Apple’s <strong>walled garden</strong> comes with trade-offs:</p><ul><li><strong>15% revenue cut</strong>: Even with the <a href="https://developer.apple.com/app-store/small-business-program/">small business program</a>, Apple takes a significant cut of your earnings.</li><li><strong>No direct customer ownership</strong>: Apple controls the relationship with your customers, making it harder to connect with them or market additional products.</li><li><strong>App review delays</strong>: The app review process can take hours or days—and even longer if your app is rejected, causing delays in your release timeline.</li></ul><p>The App Store offers real opportunities, but remember that Apple stands <strong>between</strong> you and your users. If your app fits Apple’s ecosystem and you’re prepared to work within its constraints, the App Store can be a powerful platform to reach millions of users worldwide.</p><h2><a id="1-enroll-in-the-apple-developer-program" href="#1-enroll-in-the-apple-developer-program">1. Enroll in the Apple Developer Program</a></h2><p>To publish apps on the App Store, you’ll need to join the <a href="https://developer.apple.com/programs/enroll/">Apple Developer Program</a>:</p><figure><picture><source srcset="images/enroll-01-become-a-member.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Apple Developer Program: enrollment page" srcset="images/enroll-01-become-a-member.png 2x"/></picture><figcaption><center><i>Apple Developer Program: enrollment page</i></center></figcaption></figure><ul><li><strong>Cost</strong>: $99 per year.</li><li><strong>Who can enroll</strong>: Individuals or organizations.</li><li><strong>How to enroll</strong>: Start the process at <a href="https://developer.apple.com/programs/enroll/">developer.apple.com/programs/enroll/</a>.</li><li><strong>Reduce fees</strong>: Join the <a href="https://developer.apple.com/app-store/small-business-program/">Small Business Program</a> to pay a <strong>15% commission</strong> instead of 30% (eligibility applies).</li></ul><h3><a id="enrollment-steps" href="#enrollment-steps">Enrollment Steps</a></h3><p>The enrollment process is fairly simple and involves the following steps:</p><ul><li><a href="https://developer.apple.com/programs/enroll/">Start your Enrollment</a></li><li>Sign in with your Apple ID (or create an account)</li><li>Agree to the Apple Developer Agreement</li><li>Confirm your personal information</li><li>Select your entity type (individual, organization, or others)</li><li>Obtain a D-U-N-S Number using <a href="https://developer.apple.com/enroll/duns-lookup/">this service</a> (organizations only - can take <strong>up to 30 business days</strong>, or up to <strong>8 business days</strong> for the expedited service)</li><li>Agree to the legal terms</li><li>Purchase your membership</li><li>Complete the verification process</li></ul><h3><a id="what-happens-next?" href="#what-happens-next?">What Happens Next?</a></h3><p>Once you’ve submitted all the required information:</p><ul><li><strong>Verification</strong>: Apple may take a few days to verify your enrollment.</li><li><strong>Activation</strong>: Once verified, you’ll receive an activation email.</li><li><strong>Access to Resources</strong>: With your membership, you’ll have access to your <a href="https://developer.apple.com/account">Apple Developer Account</a>, documentation, and additional resources.</li></ul><h2><a id="2-register-your-app-id" href="#2-register-your-app-id">2. Register Your App ID</a></h2><p>After enrolling in the Apple Developer Program, you'll get access to your <a href="https://developer.apple.com/account">Apple Developer Account</a>:</p><figure><picture><source srcset="images/apple-developer-portal-account.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Apple Developer Account page" srcset="images/apple-developer-portal-account.png 2x"/></picture><figcaption><center><i>Apple Developer Account page</i></center></figcaption></figure><p>The next step is to <strong>register your App ID</strong>, which uniquely identifies your app within Apple’s ecosystem.</p><h3><a id="how-to-register-your-app-id" href="#how-to-register-your-app-id">How to Register Your App ID</a></h3><ol><li>Open the <a href="https://developer.apple.com/account/resources/identifiers/list">identifiers</a> page of your Apple Developer account.</li><li>Click <strong>+</strong> to register a new App ID.</li><li>Enter an app name, select <strong>Explicit App ID</strong>, and enter the Bundle ID (e.g., <code>com.yourcompany.yourapp</code>).</li><li>Select the <strong>capabilities</strong> your app requires (e.g., Push Notifications, In-App Purchases), then click <strong>Continue</strong>.</li><li>Click <strong>Register</strong> to confirm your App ID.</li></ol><p>Note that the <strong>Bundle ID</strong> must match the <strong>Bundle Identifier property</strong> in Xcode under <strong>Runner &gt; General &gt; Identity</strong>:</p><figure><picture><source srcset="images/xcode-bundle-identifier.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Where to find the Bundle Identifier in Xcode" srcset="images/xcode-bundle-identifier.png 2x"/></picture><figcaption><center><i>Where to find the Bundle Identifier in Xcode</i></center></figcaption></figure><h2><a id="3-create-your-app-in-app-store-connect" href="#3-create-your-app-in-app-store-connect">3. Create your app in App Store Connect</a></h2><p>After registering your App ID, the next step is to create your app in <a href="https://appstoreconnect.apple.com/">App Store Connect</a>.</p><h3><a id="how-to-create-a-new-app-in-app-store-connect" href="#how-to-create-a-new-app-in-app-store-connect">How to Create a New App in App Store Connect</a></h3><p>Go to <a href="https://appstoreconnect.apple.com/apps">App Store Connect &gt; Apps</a>, then add a new app:</p><figure><picture><source srcset="images/create-app-01-new-app.webp 2x" type="image/webp"/><img class="bottom-12px" alt="To create a new app, click the "+" button" srcset="images/create-app-01-new-app.png 2x"/></picture><figcaption><center><i>To create a new app, click the "+" button</i></center></figcaption></figure><p>Fill in the required details:</p><figure><picture><source srcset="images/create-app-02-new-app.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Adding the new app details" srcset="images/create-app-02-new-app.png 2x"/></picture><figcaption><center><i>Adding the new app details</i></center></figcaption></figure><p>These include:</p><ul><li><strong>Platforms</strong>: Ensure <strong>iOS</strong> is selected.</li><li><strong>Name</strong>: The app name displayed to users on the App Store.</li><li><strong>Primary Language</strong>: The default language for your app’s metadata.</li><li><strong>Bundle ID</strong>: Select the App ID you registered in the previous step.</li><li><strong>SKU</strong>: A unique internal ID for your app (not visible to users).</li><li><strong>User Access</strong>: Decide which App Store Connect users can manage the app.</li></ul><p>When you're done, click <strong>Create</strong> to finalize the setup.</p><h2><a id="4-prepare-your-app-for-review" href="#4-prepare-your-app-for-review">4. Prepare your App For Review</a></h2><p>After clicking <strong>Create</strong>, you’ll land on the app’s overview page:</p><figure><picture><source srcset="images/create-app-10-overview.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Main page for a new app in App Store Connect" srcset="images/create-app-10-overview.png 2x"/></picture><figcaption><center><i>Main page for a new app in App Store Connect</i></center></figcaption></figure><p>Note that entering all the information is <strong>long process</strong> and you'll need to fill multiple pages:</p><figure><picture><source srcset="images/create-app-24-other-pages.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Fill all the data in "App Information", "App Privacy", "Pricing and Availability", before adding your app for review" srcset="images/create-app-24-other-pages.png 2x"/></picture><figcaption><center><i>Fill all the data in "App Information", "App Privacy", "Pricing and Availability", before adding your app for review</i></center></figcaption></figure><p>Here’s what you’ll need to provide and where to find it.</p><h3><a id="sections-to-complete" href="#sections-to-complete">Sections to Complete</a></h3><ul><li><strong>Main Page</strong><ul><li><strong>Preview and Screenshots</strong>: up to 3 app previews (optional), and up to 10 screenshots</li><li><strong>App Metadata</strong>: description, keywords, support URL, marketing URL, version, copyright</li><li><strong>App Review Information</strong>: Provide:<ul><li><strong>Sign-in Information</strong>: If your app requires login credentials for testing, create a test account and share them here.</li><li><strong>Contact Information</strong>: Provide details for Apple to contact you during the review process.</li><li><strong>Notes</strong>: Add any additional context for the App Review team.</li></ul></li></ul></li></ul><ul><li><strong>App Information</strong><ul><li><strong>Localizable Information</strong>: your app's name and subtitle</li><li><strong>General Information</strong>: bundle ID, primary and secondary category, content rights information</li><li><strong>Age Rating</strong>: a questionnaire with multiple selections and yes/no questions</li><li><strong>App Encryption Documentation</strong>: specify whether your app uses non-exempt encryption (see tip below for most cases).</li></ul></li></ul><ul><li><strong>Pricing and Availability</strong><ul><li><strong>Price Schedule</strong>: starting price for your app (free or paid)</li><li><strong>App Availability</strong>: countries where your app will be available to purchase or download</li></ul></li></ul><ul><li><strong>App Privacy</strong><ul><li><strong>Privacy Policy</strong>: a link to your app’s Privacy Policy (required).</li><li><strong>Data Collection</strong>: Specify your app’s data collection and usage practices. This is a multi-step process where you’ll need to disclose:<ul><li>Types of data collected (e.g., location, identifiers).</li><li>How the data is used (e.g., analytics, advertising).</li><li>Whether the data is linked to user identity or used for tracking.</li></ul></li></ul></li></ul><blockquote><p><strong>Tip</strong>: If your app doesn’t use non-exempt encryption (this applies to most apps), add a <code>ITSAppUsesNonExemptEncryption</code> key and set the value to <code>NO</code> in your <code>Info.plist</code> file in Xcode. This will prevent extra encryption documentation steps when uploading your builds to App Store Connect. Read <a href="https://codewithandrea.com/tips/fix-missing-compliance-warning/">this tip</a> for more details.</p></blockquote><h3><a id="before-submitting-for-review" href="#before-submitting-for-review">Before Submitting for Review</a></h3><p>Make sure you’ve entered all the required app details. If any information is missing, you’ll see an error when you click <strong>Add for Review</strong>:</p><figure><picture><source srcset="images/create-app-11-unable-to-add-for-review.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Warning about required items before the app can be added for review" srcset="images/create-app-11-unable-to-add-for-review.png 2x"/></picture><figcaption><center><i>Warning about required items before the app can be added for review</i></center></figcaption></figure><h2><a id="5-create-a-privacy-manifest-in-xcode" href="#5-create-a-privacy-manifest-in-xcode">5. Create a Privacy Manifest in Xcode</a></h2><p>When you specify your data collection practices in <strong>App Store Connect</strong>, Apple generates <strong>privacy labels</strong> for your app, which are displayed on your App Store listing. For example:</p><figure><picture><source srcset="images/app-store-privacy-labels.webp 2x" type="image/webp"/><img class="bottom-12px" alt="App Store privacy labels" srcset="images/app-store-privacy-labels.png 2x"/></picture><figcaption><center><i>App Store privacy labels</i></center></figcaption></figure><p>Starting <strong>November 12, 2024</strong>, Apple also requires you to <a href="https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/adding_a_privacy_manifest_to_your_app_or_third-party_sdk">add a Privacy Manifest to your app</a> before submitting it to App Store Connect.</p><h3><a id="what-is-a-privacy-manifest?" href="#what-is-a-privacy-manifest?">What is a Privacy Manifest?</a></h3><p>A <strong>Privacy Manifest</strong> is a file named <code>PrivacyInfo.xcprivacy</code> that documents your app's <strong>data collection practices</strong> and <strong>API usage</strong>. This file helps Apple understand what data your app collects, how it’s used, and why.</p><h3><a id="how-to-create-a-privacy-manifest-in-xcode" href="#how-to-create-a-privacy-manifest-in-xcode">How to Create a Privacy Manifest in Xcode</a></h3><p>Follow these steps to create a Privacy Manifest for your app:</p><ul><li>Open the <code>ios/Runner.xcworkspace</code> project in Xcode.</li><li>Right-click the <strong>Runner</strong> folder and select <strong>New File from Template...</strong></li><li>Scroll down to the <strong>Resource</strong> section, choose <strong>App Privacy</strong>, and click <strong>Next</strong>.</li><li>Create the file with the default name <code>PrivacyInfo</code>.</li></ul><h3><a id="what-to-add-to-the-privacy-manifest" href="#what-to-add-to-the-privacy-manifest">What to Add to the Privacy Manifest</a></h3><p>Once the file is created, add the following values to the <strong>App Privacy Configuration</strong> section:</p><ul><li><strong>NSPrivacyTracking</strong>: Indicates whether your app uses <a href="https://developer.apple.com/app-store/app-privacy-details/#user-tracking">tracking</a>, as defined by Apple’s <a href="https://developer.apple.com/documentation/apptrackingtransparency/">App Tracking Transparency</a> framework.</li><li><strong>NSPrivacyTrackingDomains</strong>: Lists internet domains your app uses for tracking purposes.</li><li><strong>NSPrivacyCollectedDataTypes</strong>: Details the types of data collected by your app (e.g., location, identifiers).</li><li><strong>NSPrivacyAccessedAPITypes</strong>: Describes the APIs your app accesses and the reasons for their use (e.g., <strong>User Defaults</strong> for local storage).</li></ul><p>These should match the data collection types you declared in App Store Connect.</p><p>Once all the information is complete, your <code>PrivacyInfo</code> file should look similar to this:</p><figure><picture><source srcset="images/xcode-privacy-manifest-final.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Xcode privacy manifest file" srcset="images/xcode-privacy-manifest-final.png 2x"/></picture><figcaption><center><i>Xcode privacy manifest file</i></center></figcaption></figure><h3><a id="example-file" href="#example-file">Example File</a></h3><p>Here's an example of the completed <code>PrivacyInfo.xcprivacy</code> for one of my apps:</p><ul><li><a href="https://github.com/bizz84/flutter_ship_app/blob/main/ios/Runner/PrivacyInfo.xcprivacy">Example PrivacyInfo.xcprivacy</a></li></ul><blockquote><p>To learn more, read the official Apple docs about <a href="https://developer.apple.com/app-store/user-privacy-and-data-use/">User privacy and data use</a> and <a href="https://developer.apple.com/documentation/bundleresources/privacy-manifest-files">Privacy manifest files</a>.</p></blockquote><h2><a id="6-update-the-xcode-project-settings" href="#6-update-the-xcode-project-settings">6. Update the Xcode project settings</a></h2><p>Before building and submitting your app to <strong>App Store Connect</strong>, review your <strong>Xcode project settings</strong> to ensure everything is configured correctly. You can find these settings in <strong>Runner &gt; General</strong>:</p><figure><picture><source srcset="images/xcode-review-settings-01-general.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Xcode project settings (General tab)" srcset="images/xcode-review-settings-01-general.png 2x"/></picture><figcaption><center><i>Xcode project settings (General tab)</i></center></figcaption></figure><h3><a id="key-xcode-project-settings" href="#key-xcode-project-settings">Key Xcode project settings</a></h3><p>Here are the key settings you need to review and update:</p><ul><li><strong>Supported Destinations</strong>: In addition to <strong>iPhone</strong>, decide if your app should support <strong>iPad</strong>, <strong>Mac</strong>, and <strong>Apple Vision</strong></li><li><strong>Minimum Deployment Version</strong>: Targeting an iOS version that is <strong>2 years old</strong> ensures your app will be compatible with <strong>over 90% of devices</strong> (<a href="https://gs.statcounter.com/ios-version-market-share/">source</a>).</li><li><strong>Identity</strong>: Double-check the <strong>App Category</strong>, as it impacts your app’s discoverability on the App Store. Other fields like <strong>Display Name</strong>, <strong>Bundle Identifier</strong>, <strong>Version</strong>, and <strong>Build Number</strong> are derived from your <code>pubspec.yaml</code>, file, so there should be no need to change them.</li><li><strong>Deployment Info</strong>: Review and update the <strong>supported orientations</strong> based on your app’s design.</li><li><strong>App Icons and Launch Screen</strong>: Verify that they look as intended.</li></ul><blockquote><p>Tip: use packages like <a href="https://pub.dev/packages/flutter_launcher_icons">flutter_launcher_icons</a> and <a href="https://pub.dev/packages/flutter_native_splash">flutter_native_splash</a> to generate consistent app icons and launch screens for all platforms.</p></blockquote><h3><a id="code-signing" href="#code-signing">Code Signing</a></h3><p>In addition to the general settings, you’ll need to review the <strong>Signing &amp; Capabilities</strong> tab:</p><figure><picture><source srcset="images/xcode-signing-01.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Signing & Capabilities tab in Xcode" srcset="images/xcode-signing-01.png 2x"/></picture><figcaption><center><i>Signing &amp; Capabilities tab in Xcode</i></center></figcaption></figure><ul><li><strong>Enable Automatic Signing</strong>: Ensure <strong>Automatically manage signing</strong> is enabled.</li><li><strong>Select Your Team</strong>: In the dropdown, choose your <strong>Team</strong>. This allows Xcode to automatically generate the necessary <strong>certificate</strong> and <strong>provisioning profile</strong> (if they don’t already exist).</li></ul><blockquote><p>By default, a <strong>development</strong> certificate is created (rather than a <strong>distribution</strong> one). This is normal and won’t prevent you from uploading your app to App Store Connect. You can find all your certificates in the <a href="https://developer.apple.com/account/resources/certificates/list">Apple Developer Portal</a>.</p></blockquote><h3><a id="app-version-and-build-number" href="#app-version-and-build-number">App Version and Build Number</a></h3><p>In Flutter, the <strong>version</strong> and <strong>build number</strong> are defined in the <code>pubspec.yaml</code> file under the <code>version</code> key:</p><pre><code><div class="highlight"><span></span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">0.3.4+18</span>
</div></code></pre><p>In <strong>App Store Connect</strong>, you’ll need to manually set the app version number to match the version in your <code>pubspec.yaml</code>. Here’s how:</p><ol><li>Go to <strong>App Store Connect</strong> &gt; <strong>My Apps</strong> &gt; <strong>Your App</strong>.</li><li>Scroll down to the metadata and set the correct <strong>version number</strong> to match what’s in your <code>pubspec.yaml</code>:</li></ol><figure><picture><source srcset="images/xcode-signing-07-app-store-connect-version.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Setting the version number in App Store Connect" srcset="images/xcode-signing-07-app-store-connect-version.png 2x"/></picture><figcaption><center><i>Setting the version number in App Store Connect</i></center></figcaption></figure><h2><a id="7-build-upload-and-submit-your-app-to-app-store-connect" href="#7-build-upload-and-submit-your-app-to-app-store-connect">7. Build, upload, and submit your app to App Store Connect</a></h2><p>Once you’ve verified your <strong>Xcode settings</strong> and <strong>versioning</strong>, it’s time to build your app bundle and upload it to <strong>App Store Connect</strong> for review.</p><h3><a id="building-the-ios-app-bundle" href="#building-the-ios-app-bundle">Building the iOS App Bundle</a></h3><p>To build the iOS app bundle, run the following command:</p><pre><code><div class="highlight"><span></span>flutter<span class="w"> </span>build<span class="w"> </span>ipa
</div></code></pre><blockquote><p>Depending on your app’s configuration, you might need to pass additional flags. To see all supported arguments, run: <code>flutter build ipa --help</code>.</p></blockquote><p>If your app supports multiple flavors, specify the flavor using the <code>--flavor</code> flag. For example:</p><pre><code><div class="highlight"><span></span><span class="c1"># To build the prod flavor</span>
flutter<span class="w"> </span>build<span class="w"> </span>ipa<span class="w"> </span>--flavor<span class="w"> </span>prod<span class="w"> </span>-t<span class="w"> </span>lib/main_prod.dart<span class="w"> </span>--dart-define-from-file<span class="o">=</span>.env.prod
</div></code></pre><p>This command will generate an <strong>IPA</strong> file (iOS App Archive) in the <code>build/ios/ipa/</code> directory. The IPA is the format used to distribute iOS apps.</p><h3><a id="uploading-the-app-bundle-to-app-store-connect" href="#uploading-the-app-bundle-to-app-store-connect">Uploading the App Bundle to App Store Connect</a></h3><p>There are multiple ways to upload your app bundle, but for first-time uploads, I recommend using the <a href="https://apps.apple.com/us/app/transporter/id1450874784">Apple Transporter</a> app. Transporter simplifies the upload process by handling both the upload and validation for you.</p><p><strong>How to Use Transporter:</strong></p><ul><li>Open the Apple Transporter app and sign in with your Apple ID.</li><li>Drag and drop the <code>build/ios/ipa/*.ipa</code> file into the app.</li><li>Click <strong>Deliver</strong> to upload your app bundle:</li></ul><figure><picture><source srcset="images/xcode-transporter-02-app.webp 2x" type="image/webp"/><img class="bottom-40px" alt="How to deliver the IPA with the Transporter app" srcset="images/xcode-transporter-02-app.png 2x"/></picture></figure><p>After a few minutes, Transporter will show the app as <strong>Ready for Internal Testing</strong>:</p><figure><picture><source srcset="images/xcode-transporter-05-ready-for-testing.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Transporter showing that the app is ready for internal testing" srcset="images/xcode-transporter-05-ready-for-testing.png 2x"/></picture></figure><h3><a id="submitting-your-app-for-review" href="#submitting-your-app-for-review">Submitting Your App for Review</a></h3><p>Go to your app in <strong>App Store Connect</strong>, scroll down to <strong>Build</strong>, click the <strong>+</strong> icon, select your build, and click <strong>Done</strong>:</p><figure><picture><source srcset="images/submit-app-02-select-build.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Selecting the build in App Store Connect" srcset="images/submit-app-02-select-build.png 2x"/></picture></figure><p>Then, click <strong>Add for Review</strong> at the top of the page, then <strong>Submit to App Review</strong>:</p><figure><picture><source srcset="images/submit-app-05-add-for-review.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Saving the changes and adding the app for review" srcset="images/submit-app-05-add-for-review.png 2x"/></picture></figure><h3><a id="what-happens-next?" href="#what-happens-next?">What Happens Next?</a></h3><p>Your app will now enter Apple’s <strong>review queue</strong>. Apple’s review team will assess your app for compliance with their guidelines. If everything checks out, you’ll receive a notification, and your app will be ready for release!</p><h2><a id="wrapping-up" href="#wrapping-up">Wrapping Up</a></h2><p>Congratulations! You’ve successfully submitted your iOS app to <strong>App Store Connect</strong>—the final step before Apple reviews it for release on the App Store. 🎉</p><h3><a id="what’s-next?" href="#what’s-next?">What’s Next?</a></h3><p>While waiting for App Review, it’s a good idea to get familiar with Apple’s review guidelines and resources to ensure a smooth approval process. Start with the <a href="https://developer.apple.com/distribute/app-review/">App Review</a> page, which includes helpful documentation like:</p><ul><li><a href="https://developer.apple.com/app-store/review/guidelines/">App Review Guidelines</a></li><li><a href="https://developer.apple.com/design/human-interface-guidelines/">Human Interface Guidelines</a></li><li><a href="https://www.apple.com/legal/intellectual-property/guidelinesfor3rdparties.html">Guidelines for Using Apple Trademarks and Copyrights</a></li><li><a href="https://developer.apple.com/distribute/app-review/#submitting-for-review">Submitting for review</a></li><li><a href="https://developer.apple.com/distribute/app-review/#common-app-rejections">Avoiding common issues</a></li></ul><h3><a id="submitting-app-updates-to-app-store-connect" href="#submitting-app-updates-to-app-store-connect">Submitting App Updates to App Store Connect</a></h3><p>Once your app is approved, you’ll likely submit updates to add new features and fix bugs. While using the <strong>Transporter app</strong> works for occasional uploads, it can quickly become tedious if you update your app frequently.</p><p>A better solution? <strong>Automate the process</strong> with a build script that uses the <code>xcrun</code> command to automatically build and upload your app to App Store Connect.</p><p>To learn more about this, read:</p><ul><li><a href="https://codewithandrea.com/tips/build-upload-ios-script/">iOS App Store: Build and Upload Script</a></li></ul><h3><a id="scaling-with-cicd-pipelines" href="#scaling-with-cicd-pipelines">Scaling with CI/CD Pipelines</a></h3><p>If you work as part of a team or release updates often, consider setting up <strong>CI/CD pipelines</strong> to automate your app builds, testing, and releases. CI/CD enables you to:</p><ul><li>Save time by automating repetitive tasks.</li><li>Run specific workflows (e.g. run tests) when certain <a href="https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#on">events are triggered</a> (e.g. when pushing a branch).</li><li>Distribute apps more efficiently via TestFlight or the App Store.</li></ul><p>To dive deeper into these topics, and many more, you can explore my new course. 👇</p><h2><a id="new-course-flutter-in-production" href="#new-course-flutter-in-production">New Course: Flutter in Production</a></h2><p>When it comes to <strong>shipping</strong> and <strong>maintaining</strong> apps in production, there are many important aspects to consider:</p><ul><li><strong>Preparing for release</strong>: splash screens, flavors, environments, error reporting, analytics, force update, privacy, T&amp;Cs.</li><li><strong>App Submissions</strong>: app store metadata &amp; screenshots, compliance, testing vs distribution tracks, dealing with rejections.</li><li><strong>Release automation:</strong> CI workflows, environment variables, custom build steps, code signing, uploading to the stores.</li><li><strong>Post-release</strong>: error monitoring, bug fixes, addressing user feedback, over-the-air updates, feature flags &amp; A/B testing.</li></ul><p>My latest course will help you get your app to the stores faster and with fewer headaches.</p><p>If you’re interested, you can learn more and enroll here. 👇</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/xcode-privact-manifest/</guid><title>Adding a Privacy Manifest in Xcode</title><description>Starting November 12, 2024, apps that don’t include a Privacy Manifest can’t be submitted for review in App Store Connect.</description><link>https://codewithandrea.com/tips/xcode-privact-manifest/</link><pubDate>Mon, 2 Dec 2024 02:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Starting November 12, 2024, apps that don’t include a Privacy Manifest can’t be submitted for review in App Store Connect.</p><p>To address this, add a privacy manifest to the Runner project inside Xcode.</p><figure><picture><source srcset="images/212.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Adding a Privacy Manifest in Xcode" srcset="images/212.png 2x"/></picture></figure><p>To learn more, read:</p><ul><li><a href="https://developer.apple.com/documentation/bundleresources/adding-a-privacy-manifest-to-your-app-or-third-party-sdk">Adding a privacy manifest to your app or third-party SDK</a></li></ul><p>For instructions about what to put in your privacy manifest, read:</p><ul><li><a href="https://developer.apple.com/documentation/bundleresources/privacy-manifest-files">Privacy manifest files</a></li></ul><hr><p>My latest course, <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a>, covers the entire iOS app release process in detail—from joining the Apple Developer Program to submitting your app for review.</p><p>Here’s a free lesson to get started:</p><ul><li><a href="https://pro.codewithandrea.com/flutter-in-production/09-release-ios/01-intro">Intro: Releasing Your iOS App to the App Store</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/fix-missing-compliance-warning/</guid><title>Fix for Missing Compliance Warning in App Store Connect</title><description>If your app does not use Non-Exempt Encryption, set ITSAppUsesNonExemptEncryption to NO in your Info.plist file in Xcode.</description><link>https://codewithandrea.com/tips/fix-missing-compliance-warning/</link><pubDate>Fri, 29 Nov 2024 02:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Tired of seeing the "Missing Compliance" warning when uploading builds to App Store Connect?</p><p>If your app does <strong>not</strong> use Non-Exempt Encryption, set <code>ITSAppUsesNonExemptEncryption</code> to <code>NO</code> in your <code>Info.plist</code> file in Xcode.</p><p>Upload your next build, and the warning will disappear. 👍</p><figure><picture><source srcset="images/211.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Fix for Missing Compliance Warning in App Store Connect" srcset="images/211.png 2x"/></picture></figure><p>For more details, read:</p><ul><li><a href="https://developer.apple.com/documentation/security/complying-with-encryption-export-regulations">Complying with Encryption Export Regulations</a></li></ul><hr><p>My latest course, <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a>, covers the entire iOS app release process in detail—from joining the Apple Developer Program to submitting your app for review.</p><p>Here’s a free lesson to get started:</p><ul><li><a href="https://pro.codewithandrea.com/flutter-in-production/09-release-ios/01-intro">Intro: Releasing Your iOS App to the App Store</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/material-icons-theme-vscode-extension/</guid><title>Material Icons Theme (VSCode Extension)</title><description>Upgrade all your file icons with the Material Icons Theme. File nesting is properly supported, too, and the icons will be correctly left-aligned.</description><link>https://codewithandrea.com/tips/material-icons-theme-vscode-extension/</link><pubDate>Wed, 27 Nov 2024 02:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Tired of the old, boring VSCode default theme?</p><p>Then, install the <a href="https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme">Material Icons Theme</a> and upgrade all your file icons! 🤩</p><p>File nesting is properly supported, too, and the icons will be correctly left-aligned! 🎉</p><figure><picture><source srcset="images/210.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Material Icons Theme (VSCode Extension)" srcset="images/210.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/build-upload-ios-script/</guid><title>iOS App Store: Build and Upload Script</title><description>A simple script to build and upload your iOS app to App Store Connect. You can run this locally, no CI/CD needed!</description><link>https://codewithandrea.com/tips/build-upload-ios-script/</link><pubDate>Tue, 26 Nov 2024 02:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>With this simple script, you can build and upload your iOS app to App Store Connect.</p><p>The best thing? You can run it from your local machine (no CI/CD needed!)</p><p>Read below for instructions on how to get it working. 🧵</p><figure><picture><source srcset="images/209.webp 2x" type="image/webp"/><img class="bottom-40px" alt="iOS App Store: Build and Upload Script" srcset="images/209.png 2x"/></picture></figure><hr><p>The script uses the <code>xcrun</code> command line tool to upload the IPA to App Store Connect.</p><p>You’ll need to authenticate with one of these methods:</p><ul><li>Username and Password</li><li>App Store Connect API Key (recommended)</li></ul><p>This guide explains how to obtain an API key:</p><ul><li><a href="https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api">Creating API Keys for App Store Connect API</a></li></ul><hr><p>Once you have obtained your Key ID and Issuer ID, set them as environment variables in your system (I like to store mine in <code>~/.zshrc</code> for convenience.)</p><p>Then, simply download the script from here and use it:</p><ul><li><a href="https://gist.github.com/bizz84/0a00a48dce7982cf3b3cc59c940ee344">Simple script to build and upload the IPA file to App Store Connect</a></li></ul><hr><p>My latest course, <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a>, covers the entire iOS app release process in detail—from joining the Apple Developer Program to submitting your app for review.</p><p>Here’s a free lesson to get started:</p><ul><li><a href="https://pro.codewithandrea.com/flutter-in-production/09-release-ios/01-intro">Intro: Releasing Your iOS App to the App Store</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/api-keys-client-server/</guid><title>API keys storage: Client or Server?</title><description>Some guidelines to help you decide which API keys belong on the client, and which belong to the server.</description><link>https://codewithandrea.com/tips/api-keys-client-server/</link><pubDate>Mon, 25 Nov 2024 02:00:00 +0100</pubDate><content:encoded><![CDATA[<p>When building mobile or web apps, security is paramount.</p><p>Some API keys belong on the client, others on the server—but do you know which is which? 🤔</p><p>Here are some guidelines to help you decide.</p><figure><picture><source srcset="images/208.webp 2x" type="image/webp"/><img class="bottom-40px" alt="API keys storage: Client or Server?" srcset="images/208.png 2x"/></picture></figure><h3><a id="additional-resources" href="#additional-resources">Additional Resources</a></h3><p>For more in-depth guidance about securing API keys, read this article:</p><ul><li><a href="https://codewithandrea.com/articles/flutter-api-keys-dart-define-env-files/">How to Store API Keys in Flutter: --dart-define vs .env files</a></li></ul><p>Some API keys must be stored on the server and never transmitted to the client. Dart Shelf works great in this scenario, and this article covers all the details:</p><ul><li><a href="https://codewithandrea.com/articles/build-deploy-dart-shelf-app-globe/">How to Build and Deploy a Dart Shelf App on Globe.dev</a></li></ul><p>If you work with Firebase Cloud Functions and want to learn about best practices for securing your server-side keys, this guide has you covered:</p><ul><li><a href="https://codewithandrea.com/articles/api-keys-2ndgen-cloud-functions-firebase/">How to Secure API Keys with 2nd-Gen Cloud Functions and Firebase</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/downloads-count-pub-dev/</guid><title>How to Show the Downloads Count on pub.dev</title><description>How to enable dark mode and see downloads count for your favourite packages on pub.dev.</description><link>https://codewithandrea.com/tips/downloads-count-pub-dev/</link><pubDate>Thu, 21 Nov 2024 02:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>You can now see the downloads count for your favourite packages on pub.dev.</p><p>To enable this, go to <a href="https://pub.dev/experimental">pub.dev/experimental</a>, and toggle "Download count metrics".</p><p>Dark mode is also supported. 🌚</p><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Downloads Count on pub.dev" srcset="images/twitter-card.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/november-2024/</guid><title>November 2024: Architecting Flutter Apps, Flutter Forum, Image Filters, Meshes and Gradients</title><description>Also included: Flutter Community on Bluesky, Flock (Flutter Fork), and the latest from Code with Andrea.</description><link>https://codewithandrea.com/newsletter/november-2024/</link><pubDate>Thu, 21 Nov 2024 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>November is a Flutter release month, but as I write this, the new stable version hasn’t landed just yet. No need to worry—it’s likely just around the corner.</p><p>In the meantime, I have a bunch of exciting news and updates from the Flutter community. Let’s dive right in! 🚀</p><h2><a id="flutter-news" href="#flutter-news">Flutter News</a></h2><p>This month, there have been some very interesting developments in the Flutter ecosystem. Here are the top stories. 👇</p><h3><a id="💙-architecting-flutter-apps" href="#💙-architecting-flutter-apps">💙 Architecting Flutter Apps</a></h3><p>Building scalable and maintainable apps starts with a solid <strong>app architecture</strong>, and I’ve written extensively on the topic (<a href="https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/">here</a> and <a href="https://codewithandrea.com/articles/comparison-flutter-app-architectures/">here</a>). So I’m very happy to see the Flutter team address this with new official <a href="https://docs.flutter.dev/app-architecture">docs on app architecture</a>.</p><p>These docs introduce concepts like <a href="https://en.wikipedia.org/wiki/Separation_of_concerns">separation of concerns</a>, <strong>unidirectional data flow</strong>, and the <strong>MVVM design pattern</strong>. While there’s room for further expansion, they’re a great starting point for developers looking to build robust and scalable apps.</p><ul><li><a href="https://docs.flutter.dev/app-architecture">Architecting Flutter Apps</a></li></ul><p>The Flutter team also released a well-structured sample app to demonstrate best practices, including multiple environments, brand-specific styling, and high test coverage. Check it out here:</p><ul><li><a href="https://github.com/flutter/samples/tree/main/compass_app">Compass App (Flutter samples)</a></li></ul><h3><a id="💬-flutter-forum" href="#💬-flutter-forum">💬 Flutter Forum</a></h3><p>The brand-new <a href="https://forum.itsallwidgets.com/">Flutter Forum</a> has quickly become one of my favorite places to hang out.</p><p>It features high-quality discussions, like these on <a href="https://forum.itsallwidgets.com/t/full-stack-dart-flutter-reference-architecture/">full-stack Flutter architectures</a>, <a href="https://forum.itsallwidgets.com/t/responsiveness-in-flutter/">responsiveness in Flutter</a>, and <a href="https://forum.itsallwidgets.com/t/lesser-known-classes-and-functions-from-the-dart-core-libraries/236">lesser-known Dart classes</a>, and many experienced Flutter devs have already joined.</p><p>Compared to Reddit, Discord, and all the other channels, this forum has a much better signal-to-noise ratio. Best of all, it’s free and public! 🙌</p><ul><li><a href="https://forum.itsallwidgets.com/">Flutter Forum - A Home for All Flutter Developers</a></li></ul><h3><a id="🦋-flutter-community-on-bluesky" href="#🦋-flutter-community-on-bluesky">🦋 Flutter Community on Bluesky</a></h3><p>On a related note, many Flutter developers are <a href="https://forum.itsallwidgets.com/t/moving-to-bluesky-from-x-twitter-whats-your-profile/">migrating from X to Bluesky</a>. While I’m still posting on both platforms, Bluesky is becoming a great space for focused Flutter discussions, without the usual noise.</p><p>If you’re exploring Bluesky, I recommend these curated lists to get started:</p><ul><li><a href="https://bsky.app/starter-pack/kerberjg.bsky.social/3l72plkrou72z">#FlutterDev 💙 Starter Pack ✨</a></li><li><a href="https://bsky.app/profile/did:plc:5jbxvfe4etepz54qhd7a5zlv/lists/3lbh46f7bgx2p">Flutter Devs</a></li></ul><h3><a id="📝-we’re-forking-flutter-this-is-why" href="#📝-we’re-forking-flutter-this-is-why">📝 We’re Forking Flutter. This is Why.</a></h3><p>A few weeks ago, Matt Carroll published <a href="https://getflocked.dev/blog/posts/we-are-forking-flutter-this-is-why/">this controversial post</a>, announcing his decision to fork the Flutter framework, with the stated goal of “expanding Flutter's available labor, and accelerate development”.</p><p>The post talks about the “Flutter team's labor shortage”, why that's a problem, and how the community can help by contributing to a new Flutter fork called <a href="https://getflocked.dev/">Flock</a>.</p><p>While the concept has sparked debate, some developers are skeptical about its feasibility. For example, in <a href="https://forum.itsallwidgets.com/t/flock-flutter-fork/502/2">this forum discussion</a>, one commenter noted:</p><blockquote><p>I don’t understand why you would consider swapping a fully funded team working on a very complicated project with what I can tell currently appears to be a team of two people mostly.</p></blockquote><p>Personally, I have mixed feelings about the whole thing, and don’t see a realistic scenario where Flock can live up to its claims, but time will tell.</p><p>For now, you can read the original post here:</p><ul><li><a href="https://getflocked.dev/blog/posts/we-are-forking-flutter-this-is-why/">We're forking Flutter. This is why.</a></li></ul><h2><a id="flutter-videos" href="#flutter-videos">Flutter Videos</a></h2><p>I haven’t shared any Observable Flutter episodes in a while, but two recent videos stood out to me, covering <strong>Image Filters</strong> and <strong>Mesh Gradients</strong>. If you’re into graphics and shaders, these are must-watches.</p><h3><a id="📹-image-filters" href="#📹-image-filters">📹 Image Filters</a></h3><p>In this episode, Raouf Rahiche explores Flutter’s built-in image filters, including:</p><ul><li>Implementing Android’s overscroll effect using shaders</li><li>Examples of shaders ported from Metal and Skia to GLSL</li><li>Blur filters, tile modes, and color filters (grayscale, sepia, etc.)</li></ul><p>The episode also features a live coding session to create a "selective focus" effect, highlighting specific areas of an image. If you want to dive deeper into filters and shaders, give it a watch:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="gmcsWTcIdD4"></div></div><h3><a id="📹-meshes-and-gradients" href="#📹-meshes-and-gradients">📹 Meshes and Gradients</a></h3><p>In this episode, <a href="https://bsky.app/profile/renan.flutter.community">Renan Araujo</a> introduces <a href="https://pub.dev/packages/mesh">O’Mesh</a>, the <a href="https://x.com/reNotANumber/status/1820731673686012378">mesh gradient</a> library he used to power this <a href="https://omesh-playground.renan.gg/">playground</a>.</p><p>He explains how mesh gradients differ from traditional gradients, how to overcome shader limitations, and how Bézier curves are used to create distortions. A live coding session is also included, showing how to use the library in your projects:</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="CmXObCFH_Uw"></div></div><h2><a id="latest-from-code-with-andrea" href="#latest-from-code-with-andrea">Latest from Code with Andrea</a></h2><p>Here’s what I’ve been up to this past month:</p><ul><li><strong>New Course Module</strong>: as part of my <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> course, this module is about <strong>Releasing iOS apps on the App Store</strong>—it’ll be out soon!</li><li><strong>New Tips</strong>: as usual, you can find them <a href="https://codewithandrea.com/tips/">here on my site</a>.</li><li><strong>New Video</strong>: this is a guide about <a href="https://codewithandrea.com/videos/how-to-design-flutter-app-icons-figma/">how to design your Flutter app icons in Figma</a>.</li><li><strong>Course Updates</strong>: My <a href="https://codewithandrea.com/courses/flutter-foundations/">Flutter Foundations</a> and <a href="https://codewithandrea.com/courses/flutter-firebase-masterclass/">Flutter &amp; Firebase</a> courses are now updated with the latest package versions.</li></ul><h2><a id="one-more-thing" href="#one-more-thing">One More Thing</a></h2><p>My <strong>Black Friday Sale</strong> will start on <strong>Monday 25th</strong>.</p><p>If you’ve been planning to get my courses, wait until then, as I'll be offering a <strong>big discount</strong>.</p><p>Other than that, I’ll continue to share new content as usual, including many new tips about the upcoming Flutter 3.27 release! 🗓️</p><p>Thanks for reading, and happy coding! 🎉</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/update-android-project-script/</guid><title>Script to Update the Android Project Settings</title><description>Use this script to update the Gradle, Java, NDK version and other settings in your Android project.</description><link>https://codewithandrea.com/tips/update-android-project-script/</link><pubDate>Mon, 18 Nov 2024 02:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Tired of dealing with Gradle and other build errors on Android? 🤮</p><p>Me too! So, I built a script to fix it all at once:</p><ul><li>Gradle version</li><li>Java version</li><li>NDK version</li><li>Min SDK</li><li>Target SDK</li></ul><p>The result? Faster updates and fewer headaches. 👍</p><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Script to Update the Android Project Settings" srcset="images/twitter-card.png 2x"/></picture></figure><p>You can grab the script here:</p><ul><li><a href="https://gist.github.com/bizz84/605e2ca2088cb4acb7a076ca993f41cd">Script to update Gradle, Java and other Android project settings in a Flutter project</a></li></ul><p>To use it:</p><ul><li>Download the script and add it to a folder in your system PATH</li><li>Give it execution access: <code>chmod +x update-android-project.sh</code></li><li>Tweak the versions if needed</li><li>Run it from the root of your Flutter project</li></ul><hr><p>Some notes:</p><ul><li>This script has been updated to support both the Groovy and Kotlin DSL. Learn more here: <a href="https://codewithandrea.com/articles/flutter-android-gradle-kts/">Kotlin DSL in Flutter 3.29: How to Update Your Android Gradle Files</a></li></ul><ul><li>The script may not work perfectly for older Android projects. If your Android project is very old, the best fix is to nuke it and create it again with the Flutter CLI, as described here: <a href="https://codewithandrea.com/tips/fixing-build-issues-nuclear-option/">Fixing Build Issues - Nuclear Option</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/videos/how-to-design-flutter-app-icons-figma/</guid><title>How to Design Your Flutter App Icons in Figma</title><description>A video showing how to create a custom app icon from scratch in Figma, even if you're new to design.</description><link>https://codewithandrea.com/videos/how-to-design-flutter-app-icons-figma/</link><pubDate>Fri, 15 Nov 2024 01:00:00 +0100</pubDate><content:encoded><![CDATA[<p>The app icon and app store screenshots are the first things people notice when browsing the app store.</p><p>A well-designed icon grabs attention and makes your app stand out. 💡</p><p>But let’s be real. If you’re a busy developer, designing the app icon is probably the last thing on your mind, and your home screen might look like this:</p><figure><picture><source srcset="images/flutter-logo-ios-home-screen.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Every Flutter developer's iOS home screen" srcset="images/flutter-logo-ios-home-screen.png 2x"/></picture></figure><p>Good news: it doesn’t have to be this way. 👇</p><h2><a id="how-to-design-a-flutter-app-icon-in-figma" href="#how-to-design-a-flutter-app-icon-in-figma">How to Design a Flutter App Icon in Figma</a></h2><p>While having design skills is helpful, you don’t need to be a pro to create a simple app icon.</p><p>In this video, I’ll show you how to design a custom app icon from scratch—even if you're new to design.</p><div class="edge-to-edge"><div class="youtube-player video-positioning-middle" data-id="yxg9yrZdDlw"></div></div><h2><a id="summary" href="#summary">Summary</a></h2><p>To help you follow along, here’s the <a href="https://www.figma.com/design/2fR5TMDpzhDEVJyhQvhsiX/App-Icon-Template?node-id=1-131&t=wM1bxBAhlCe7XMWz-1">app icon template</a> I shared in the video.</p><p>Here’s a quick recap of the steps:</p><ul><li>Start with a 1000x1000px square for your icon background.</li><li>Design the foreground by combining shapes. Group them and center them.</li><li>For iOS icons, scale the foreground content to <strong>75%</strong> of the total size.</li><li>For Android icons, scale the foreground content to <strong>50%</strong> of the total size.</li><li>Android icons require separate foreground and background layers.</li><li>Export your icons with <code>1024w</code> as the size selector.</li></ul><p>Once you're done, add the icons as assets to your Flutter project and generate the launcher icon using the <a href="https://pub.dev/packages/flutter_launcher_icons">flutter_launcher_icons</a> package.</p><h3><a id="extra-tip-importing-flaticon-icons-into-figma" href="#extra-tip-importing-flaticon-icons-into-figma">Extra tip: Importing FlatIcon icons into Figma</a></h3><p>Want to save time? Search for icons on <a href="https://www.flaticon.com/">FlatIcon</a>.</p><p>Once you find a suitable icon, export it as PNG or SVG and import it into Figma (remember to check the license!).</p><p>I recommend using SVG since it allows you to edit individual layers in Figma, though note that SVG export is a paid feature.</p><h2><a id="wrap-up" href="#wrap-up">Wrap Up</a></h2><p>As we’ve seen, creating a simple icon in Figma is fairly straightforward.</p><p>If you’d like to reuse my design, you can <a href="https://www.figma.com/design/2fR5TMDpzhDEVJyhQvhsiX/App-Icon-Template?node-id=1-131&t=wM1bxBAhlCe7XMWz-1">grab my template here</a>.</p><p>Even better - you can use the <a href="https://www.figma.com/community/file/1155362909441341285">Expo App Icon &amp; Splash</a> community template, which follows Apple’s and Android’s official design guidelines.</p><h2><a id="new-course-flutter-in-production" href="#new-course-flutter-in-production">New course: Flutter in Production</a></h2><p>Designing app icons is a small but important step in the app development process.</p><p>But when it comes to <strong>shipping</strong> and <strong>monitoring</strong> apps in production, there are many more things to consider:</p><ul><li><strong>Preparing for release</strong>: splash screens, flavors, environments, error reporting, analytics, force update, privacy, T&amp;Cs</li><li><strong>App Submissions</strong>: app store metadata &amp; screenshots, compliance, testing vs distribution tracks, dealing with rejections</li><li><strong>Release automation:</strong> CI workflows, environment variables, custom build steps, code signing, uploading to the stores</li><li><strong>Post-release</strong>: error monitoring, bug fixes, addressing user feedback, adding new features, over-the-air updates</li></ul><p>My latest course will help you get your app to the stores faster and with fewer headaches.</p><p>If you’re interested, you can learn more and enroll here. 👇</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/error-throw-with-stack-trace/</guid><title>Error.throwWithStackTrace</title><description>With Error.throwWithStackTrace, you can throw custom exceptions while keeping the original stack trace intact.</description><link>https://codewithandrea.com/tips/error-throw-with-stack-trace/</link><pubDate>Tue, 12 Nov 2024 02:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Using domain-specific exceptions makes your code easier to test and maintain.</p><p>But don’t lose the original stack trace for debugging!</p><p>With <code>Error.throwWithStackTrace</code>, you can throw custom exceptions while keeping the original stack trace intact. 👇</p><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Error.throwWithStackTrace" srcset="images/twitter-card.png 2x"/></picture></figure><p>To learn more, read the official docs:</p><ul><li><a href="https://api.flutter.dev/flutter/dart-core/Error/throwWithStackTrace.html">Error.throwWithStackTrace</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/apple-small-business-program/</guid><title>Apple Small Business Program</title><description>If your app sales are less than $1M/year, you can apply to the Small Business Program and slash your fees to 15%!</description><link>https://codewithandrea.com/tips/apple-small-business-program/</link><pubDate>Thu, 7 Nov 2024 02:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If you sell apps on the iOS App Store, Apple will take a big cut:</p><ul><li>paid apps &amp; IAPs: <strong>30%</strong></li><li>subscriptions: <strong>30% in the first year, 15% after</strong><ul></ul></li></ul><p>But if you make less than $1M/year, you can apply to the Small Business Program and <strong>slash your fees to 15%</strong>!</p><p>That's a no-brainer! 💰</p><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Apple Small Business Program" srcset="images/twitter-card.png 2x"/></picture></figure><p>Read this page to learn more and enroll:</p><ul><li><a href="https://developer.apple.com/app-store/small-business-program/">Apple Small Business Program</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/riverpod-prodivers-with-ref/</guid><title>Declaring Riverpod Providers with Ref</title><description>Since Riverpod 2.6.0, all generated providers can be declared with a Ref argument. Here's how to migrate to the new syntax.</description><link>https://codewithandrea.com/tips/riverpod-prodivers-with-ref/</link><pubDate>Mon, 4 Nov 2024 02:00:00 +0100</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Since Riverpod 2.6.0, all generated providers can be declared with a <code>Ref</code> argument.</p><p>The old <code>[ProviderName]Ref</code> syntax is deprecated.</p><p>To upgrade existing projects, simply run: <code>dart run custom_lint --fix</code>. 👍</p><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Declaring Riverpod Providers with Ref" srcset="images/twitter-card.png 2x"/></picture></figure><p><strong>Note</strong>: in order to update all providers in your codebase, <code>custom_lint</code> needs to be installed and configured:</p><pre><code><div class="highlight"><span></span><span class="c1"># pubspec.yaml</span>
<span class="nt">dev_dependencies</span><span class="p">:</span>
<span class="w">  </span><span class="nt">custom_lint</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">0.7.0</span>
<span class="w">  </span><span class="nt">riverpod_lint</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">2.6.2</span>
</div></code></pre><pre><code><div class="highlight"><span></span><span class="c1"># analysis_options.yaml</span>
<span class="nt">analyzer</span><span class="p">:</span>
<span class="w">  </span><span class="nt">plugins</span><span class="p">:</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">custom_lint</span>
</div></code></pre><p>Once this is done, run: <code>dart run custom_lint --fix</code>.</p><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/newsletter/october-2024/</guid><title>October 2024: Flutter Fundamentals, Memory Leaks, Offline-First Apps, In-App Payments, New UI/UX Packages</title><description>Also included: getting a Flutter job, auto stop services (Firebase extension), animated Flutter widgets, and the latest from Code with Andrea.</description><link>https://codewithandrea.com/newsletter/october-2024/</link><pubDate>Thu, 24 Oct 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Welcome to another edition of my Flutter newsletter!</p><p>This month, we’re diving into some essential reads, including articles on memory leaks, offline-first apps, and in-app payments with RevenueCat. Plus, I’ve got a fresh batch of packages and resources to help you with your Flutter projects.</p><p>Let’s jump right in! 🚀</p><h2><a id="flutter-fundamentals" href="#flutter-fundamentals">Flutter Fundamentals</a></h2><p>A recent addition to the Flutter docs, <a href="https://docs.flutter.dev/get-started/fundamentals">Flutter Fundamentals</a> is a must-read for anyone new to Flutter. If you've already completed your first codelab, this guide will take you further by covering essential topics like widgets, layout, state management, and handling user input.</p><p>If you missed this update, now may be the perfect time to explore these core concepts and better understand how Flutter works.</p><p>Check it out here:</p><ul><li><a href="https://docs.flutter.dev/get-started/fundamentals">Learn the Fundamentals</a></li></ul><h2><a id="flutter-articles" href="#flutter-articles">Flutter Articles</a></h2><p>Here are some curated reads from the Flutter community this month.</p><h3><a id="📝-getting-a-flutter-job" href="#📝-getting-a-flutter-job">📝 Getting a Flutter Job</a></h3><p>Landing a Flutter job isn’t just about searching for “Flutter” in job listings. <a href="https://x.com/_eseidel">Eric Seidel</a>, former founder/lead of Flutter, emphasizes the importance of focusing on broader mobile jobs—many companies don’t even realize they need Flutter yet!</p><p>He also suggests targeting smaller companies and startups, or even approaching companies directly with projects that showcase your skills. This article is packed with practical tips on networking, building a portfolio, and even cold outreach.</p><p>If you’re serious about finding a Flutter role, this is a must-read:</p><ul><li><a href="https://shorebird.dev/blog/flutter-jobs/">Getting a Flutter Job</a></li></ul><h3><a id="📝-let’s-talk-about-memory-leaks-in-dart-and-flutter" href="#📝-let’s-talk-about-memory-leaks-in-dart-and-flutter">📝 Let’s Talk About Memory Leaks In Dart And Flutter</a></h3><p>Memory leaks are tricky to detect and can cause performance issues in long-running Flutter apps. In this article, <a href="https://x.com/mhadaily">Majid Hajian</a> discusses common causes of memory leaks in Dart and Flutter, focusing on the challenges introduced by asynchronous programming and Streams.</p><p>He also reviews tools like DevTools, <a href="https://github.com/dart-lang/leak_tracker/blob/main/doc/leak_tracking/OVERVIEW.md">Leak Tracker</a>, and DCM's static analysis to help identify and prevent leaks early in development.</p><p>Check out the full article to improve your app’s performance:</p><ul><li><a href="https://dcm.dev/blog/2024/10/21/lets-talk-about-memory-leaks-in-dart-and-flutter/">Let’s Talk About Memory Leaks In Dart And Flutter</a></li></ul><h3><a id="📝-how-to-add-in-app-payments-with-revenuecat-in-flutter" href="#📝-how-to-add-in-app-payments-with-revenuecat-in-flutter">📝 How to Add In-App Payments With RevenueCat in Flutter</a></h3><p>Monetizing your Flutter app is crucial, but setting up payments can be a headache. So here's a comprehensive guide on integrating RevenueCat into your Flutter app to handle in-app purchases smoothly.</p><p>The guide shows how to create a RevenueCat account, link Google Play Console to RevenueCat, configure products, entitlements, and offerings, and use the RevenueCat plugin to display products, manage purchases, and handle cancellations.</p><p>If you're planning to add payments to your app, check it out:</p><ul><li><a href="https://onlyflutter.com/how-to-add-in-app-payments-with-revenuecat-in-flutter/">How to Add In-App Payments With RevenueCat in Flutter</a></li></ul><h3><a id="📝-building-offline-first-mobile-apps-with-supabase-flutter-and-brick" href="#📝-building-offline-first-mobile-apps-with-supabase-flutter-and-brick">📝 Building Offline-First Mobile Apps with Supabase, Flutter and Brick</a></h3><p><a href="https://github.com/GetDutchie/brick">Brick</a> is a powerful data manager for Flutter that simplifies syncing and caching data between Supabase and local storage like SQLite, making it ideal for building offline-first apps.</p><p>This article explains how Brick ensures your app works seamlessly without an internet connection, while also speeding up performance through local caching. If you want to build robust mobile apps that can handle both offline and online scenarios, this guide walks you through setting up Brick with Supabase step by step.</p><p>Check out the full guide here:</p><ul><li><a href="https://supabase.com/blog/offline-first-flutter-apps">Building Offline-First Mobile Apps with Supabase, Flutter and Brick</a></li></ul><h2><a id="flutter-packages-and-tools" href="#flutter-packages-and-tools">Flutter Packages and Tools</a></h2><p>Here are some packages, tools, and open source examples you can use to improve your Flutter apps.</p><h3><a id="🔥-auto-stop-services-firebase-extension" href="#🔥-auto-stop-services-firebase-extension">🔥 Auto Stop Services (Firebase Extension)</a></h3><p>Worried about unexpected Firebase costs? The <a href="https://extensions.dev/extensions/kurtweston/functions-auto-stop-billing">Auto Stop Services</a> extension helps you avoid cost overruns by automatically disabling Firebase and Google Cloud services once your project reaches a predefined budget threshold.</p><p>You can either stop all services by removing the billing account, or selectively disable specific services. This tool helps you keep your project’s budget in check, supporting different use cases for production and non-production environments:</p><ul><li><a href="https://extensions.dev/extensions/kurtweston/functions-auto-stop-billing">Auto Stop Services (Firebase Extension)</a></li></ul><h3><a id="🐙-flutterfx-widget---animated-flutter-widgets" href="#🐙-flutterfx-widget---animated-flutter-widgets">🐙 Flutterfx Widget - Animated Flutter Widgets</a></h3><p>Looking to add some flair to your Flutter app? <a href="https://github.com/flutterfx/flutterfx_widgets">Flutterfx Widget</a> offers a growing collection of animated widgets, with new additions every week.</p><p>Each animation is implemented as a separate widget, making it easy to understand and integrate into your own projects:</p><ul><li><a href="https://github.com/flutterfx/flutterfx_widgets">Flutterfx Widget - Animated Flutter Widgets</a></li></ul><h3><a id="🧱-packages-to-improve-uiux-of-your-app" href="#🧱-packages-to-improve-uiux-of-your-app">🧱 Packages to Improve UI/UX of your App</a></h3><p>Recently, there’s been a surge of new UI/UX packages, helping you customize the look and feel of your Flutter apps.</p><p>This Reddit thread contains some good suggestions:</p><ul><li><a href="https://www.reddit.com/r/FlutterDev/comments/1fozqdy/packages_to_improve_uiux_of_your_app/">Packages to Improve UI/UX of your App</a></li></ul><p>In addition, here are a few standout packages I’ve discovered on pub.dev:</p><ul><li><a href="https://pub.dev/packages/pretty_animated_text">pretty_animated_text</a>: Easily add beautiful, customizable animated text widgets to your project.</li><li><a href="https://pub.dev/packages/soft_edge_blur">soft_edge_blur</a>: Apply smooth, soft blur effects to your widgets for a polished, modern look.</li><li><a href="https://pub.dev/packages/forui">forui</a>: a UI library for Flutter that provides a set of minimalistic widgets heavily inspired by <a href="https://ui.shadcn.com/">shadcn/ui</a>.</li></ul><p>You might find some gems here to add some extra shine to your app’s UI and UX. 💎</p><h2><a id="latest-from-code-with-andrea" href="#latest-from-code-with-andrea">Latest from Code with Andrea</a></h2><p>After months of hard work, I finally launched <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a>! The course already includes nine modules, each with a free introductory lesson. If you want a sneak peek, you can check the <a href="https://pro.codewithandrea.com/flutter-in-production/01-intro/01-intro">course introduction</a>.</p><p>On top of that, I’ve shared a <strong>personal record</strong> of <a href="https://codewithandrea.com/tips/">14 new Flutter tips</a>—so there’s plenty to explore. 🙂</p><p>But that’s not all. I’ve also published three new articles this month. 👇</p><h3><a id="📝-6-key-steps-to-take-before-releasing-your-next-flutter-app" href="#📝-6-key-steps-to-take-before-releasing-your-next-flutter-app">📝 6 Key Steps to Take Before Releasing Your Next Flutter App</a></h3><p>In this article, I break down six important steps you need to follow before hitting "publish". From setting up <strong>flavors and environments</strong> to keep your development and production separate, to implementing <strong>error monitoring</strong> and <strong>analytics</strong> for tracking performance and user behavior, these steps will help ensure a smooth release.</p><p>Plus, I cover strategies for handling <strong>force updates</strong>, collecting <strong>user feedback</strong>, and prompting <strong>in-app reviews</strong>:</p><ul><li><a href="https://codewithandrea.com/articles/key-steps-before-releasing-flutter-app/">6 Key Steps to Take Before Releasing Your Next Flutter App</a></li></ul><h3><a id="📝-how-to-setup-flutter-&-firebase-with-multiple-flavors-using-the-flutterfire-cli" href="#📝-how-to-setup-flutter-&-firebase-with-multiple-flavors-using-the-flutterfire-cli">📝 How to Setup Flutter & Firebase with Multiple Flavors Using the FlutterFire CLI</a></h3><p>If your Flutter app supports multiple flavors (like development, staging, and production), you'll need to set up separate Firebase environments for each. In this guide, I break down how to use the <a href="https://pub.dev/packages/flutterfire_cli">FlutterFire CLI</a> to manage Firebase configurations across different flavors, ensuring a clear separation between environments.</p><p>From configuring Firebase projects to streamlining the process with shell scripts, this article will save you time and prevent setup headaches:</p><ul><li><a href="https://codewithandrea.com/articles/flutter-firebase-multiple-flavors-flutterfire-cli/">How to Setup Flutter &amp; Firebase with Multiple Flavors Using the FlutterFire CLI</a></li></ul><h3><a id="📝-how-to-ask-for-in-app-reviews-in-your-flutter-app" href="#📝-how-to-ask-for-in-app-reviews-in-your-flutter-app">📝 How to Ask for In-App Reviews in Your Flutter App</a></h3><p>App reviews play an important role in the success of your apps. In this article, I walk you through using the <a href="https://pub.dev/packages/in_app_review">in_app_review</a> package to prompt users for reviews at the perfect moment—when they’re most engaged and satisfied.</p><p>From setting up the package to timing the prompt properly (without overwhelming users), this guide helps you boost your app’s rating and visibility in the store:</p><ul><li><a href="https://codewithandrea.com/articles/flutter-in-app-review-prompt/">How to Ask for In-App Reviews in Your Flutter App</a></li></ul><h2><a id="until-next-time" href="#until-next-time">Until Next Time</a></h2><p>The past few months have been quite intense, so I’m taking a much-needed break. 🏖️</p><p>Once I’m back, I’ll be diving into more articles, tips, and course content to keep you up to speed with all things Flutter.</p><blockquote><p>Also, a heads-up: my <strong>Black Friday sale</strong> is approaching. If you’ve been waiting to get my courses, you’ll be able to grab them with a <strong>big discount</strong> next month. <em>(Note: <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> is already 40% off, so it won't receive a further discount.)</em></p></blockquote><p>Thanks for reading, and happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/flutter-pub-upgrade/</guid><title>What does flutter pub upgrade do?</title><description>If you want to upgrade all dependencies to the latest non-major version, ignoring the pubspec.lock file, use flutter pub upgrade.</description><link>https://codewithandrea.com/tips/flutter-pub-upgrade/</link><pubDate>Wed, 23 Oct 2024 03:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>There's a subtle difference between <strong>pub get</strong> and <strong>pub upgrade</strong>:</p><ul><li><strong>pub get</strong> will get all dependencies, <strong>keeping</strong> the versions inside <code>pubspec.lock</code>.</li><li><strong>pub upgrade</strong> will upgrade all dependencies to the latest non-major version, <strong>ignoring</strong> the <code>pubspec.lock</code> file.</li></ul><figure><picture><source srcset="images/202.webp 2x" type="image/webp"/><img class="bottom-40px" alt="What does flutter pub upgrade do?" srcset="images/202.png 2x"/></picture></figure><p>Also note:</p><ul><li>If <code>pubspec.lock</code> doesn't exist yet, both commands behave identically.</li><li>Neither command updates any dependencies that are <strong>locked</strong> to a specific version (no caret syntax).</li></ul><p>To learn more, read these resources:</p><ul><li><a href="https://docs.flutter.dev/packages-and-plugins/using-packages#updating-package-dependencies">Updating package dependencies</a></li><li><a href="https://dart.dev/tools/pub/cmd/pub-upgrade">dart pub upgrade</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/firebase-init-multiple-flavors/</guid><title>Firebase Initialization with Multiple Flavors in Dart</title><description>An overview of two different strategies for initializing Firebase inside a Flutter app with multiple flavors.</description><link>https://codewithandrea.com/tips/firebase-init-multiple-flavors/</link><pubDate>Fri, 18 Oct 2024 03:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If your Flutter app has multiple flavors, you can put all the Firebase initialization logic in one file and switch based on the <a href="https://api.flutter.dev/flutter/services/appFlavor-constant.html"><code>appFlavor</code></a>.</p><figure><picture><img class="bottom-40px" alt="Firebase Init with Multiple Flavors (switch expression)" srcset="images/201.1.png 2x"/></picture></figure><p><strong>Note:</strong> when you do this, all the Firebase config files are bundled in the final app, which is not ideal.</p><p>A better solution is to create three entry points that load the corresponding config file and pass it to the function that performs the actual initialization.</p><p>When running, you can use the <code>-t</code> flag to specify the entry point:</p><figure><picture><img class="bottom-40px" alt="Firebase Init with Multiple Flavors (multiple entry points)" srcset="images/201.2.png 2x"/></picture></figure><p>This requires a bit more work but is more secure. 👍</p><hr><p>My latest course covers flavors and environments in great depth.</p><p>To learn more, check it out here:</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/articles/flutter-firebase-multiple-flavors-flutterfire-cli/</guid><title>How to Setup Flutter &amp; Firebase with Multiple Flavors using the FlutterFire CLI</title><description>Learn how to set up Firebase for multiple flavors in your Flutter app using the FlutterFire CLI. This guide covers iOS, Android, and web configurations.</description><link>https://codewithandrea.com/articles/flutter-firebase-multiple-flavors-flutterfire-cli/</link><pubDate>Fri, 18 Oct 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>If your Flutter app supports multiple flavors and connects to Firebase, you need some extra setup to ensure <strong>each flavor corresponds to a different Firebase environment</strong>.</p><p>The best approach is to create a <strong>separate Firebase project for each flavor</strong>. This keeps your development, staging, and production environments separate.</p><figure><picture><source srcset="images/diagram-flutter-firebase-flavors.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Each Flutter flavor maps to a separate Firebase project" srcset="images/diagram-flutter-firebase-flavors.png 2x"/></picture></figure><blockquote><p>When using custom backends or Dart SDKs like Supabase, you can connect to the correct environment by switching the URL and API key based on the flavor. But Firebase does <strong>not</strong> offer a Dart SDK and requires some platform-specific setup, making the flavoring process more complex.</p></blockquote><p>Thankfully, the <a href="https://pub.dev/packages/flutterfire_cli">FlutterFire CLI</a> comes to the rescue. I'll walk you through using it to flavor your Flutter &amp; Firebase apps <strong>without losing your mind</strong>. 😅</p><p>Here's what we will cover:</p><ul><li>Why do we need FlutterFire?</li><li>Installing the Firebase and FlutterFire CLI</li><li>FlutterFire Config Syntax for Multiple Flavors</li><li>Easier Setup with a Shell Script</li><li>Initializing Firebase during App Startup (iOS, Android, and web)</li></ul><p>By the end, you'll be able to confidently integrate Firebase into your multi-flavor Flutter app, <strong>saving time</strong> and <strong>avoiding common setup headaches</strong>.</p><h2><a id="prerequisites" href="#prerequisites">Prerequisites</a></h2><p><strong>Note</strong>: This guide assumes you <strong>already have</strong> a Flutter app that runs correctly with <code>dev</code>, <code>stg</code>, and <code>prod</code> flavors on iOS and Android:</p><pre><code><div class="highlight"><span></span>flutter<span class="w"> </span>run<span class="w"> </span>--flavor<span class="w"> </span>dev
flutter<span class="w"> </span>run<span class="w"> </span>--flavor<span class="w"> </span>stg
flutter<span class="w"> </span>run<span class="w"> </span>--flavor<span class="w"> </span>prod
</div></code></pre><p>You'll also need three Firebase projects, like in this example:</p><figure><picture><source srcset="images/flutter-ship-firebase-projects.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Three Firebase projects for the Flutter app" srcset="images/flutter-ship-firebase-projects.png 2x"/></picture></figure><blockquote><p>You can use <a href="https://pub.dev/packages/flutter_flavorizr">Flutter Flavorizr</a> to add flavors to your app. For more guidance, check out my <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a> course.</p></blockquote><p>Ready? Let's go! 🚀</p><h2><a id="why-do-we-need-flutterfire?" href="#why-do-we-need-flutterfire?">Why do we need FlutterFire?</a></h2><p>Adding Firebase to a Flutter app used to be a tedious process. You’d have to manually download configuration files for each platform (like <code>GoogleService-Info.plist</code> for iOS and <code>google-services.json</code> for Android).</p><p>Now, the process is <a href="https://firebase.google.com/docs/flutter/setup"><strong>much simpler</strong></a>. You just run <code>flutterfire configure</code> and follow some interactive prompts. Once finished, these files are automatically added to your project:</p><ul><li><code>lib/firebase_options.dart</code></li><li><code>ios/Runner/GoogleService-Info.plist</code></li><li><code>android/app/google-services.json</code></li></ul><p>However, when working with multiple flavors, it gets trickier. You’ll need <strong>separate versions</strong> of these files for each flavor, stored in different locations to avoid overwriting them during the configuration process.</p><p>Luckily, <a href="https://pub.dev/packages/flutterfire_cli/changelog#100"><strong>FlutterFire 1.0.0</strong></a> added support for multiple flavors. Let's explore how to use it.</p><h2><a id="installing-the-firebase-and-flutterfire-cli" href="#installing-the-firebase-and-flutterfire-cli">Installing the Firebase and FlutterFire CLI</a></h2><p>The <a href="https://firebase.google.com/docs/flutter/setup">official docs</a> cover all the steps for installing the Firebase and FlutterFire CLIs.</p><p>The Firebase CLI can be installed as a <strong>standalone library</strong> or via <strong>npm</strong>. I’ve found the <code>npm</code> approach to be the most reliable:</p><pre><code><div class="highlight"><span></span>npm<span class="w"> </span>install<span class="w"> </span>-g<span class="w"> </span>firebase-tools
</div></code></pre><p>To check your installation, run: <code>firebase --version</code>.</p><p>Next, log in by running <code>firebase login</code>. You’ll see this prompt:</p><pre><code><div class="highlight"><span></span><span class="p">?</span> <span class="n">Allow</span> <span class="n">Firebase</span> <span class="n">to</span> <span class="n">collect</span> <span class="n">CLI</span> <span class="n">and</span> <span class="n">Emulator</span> <span class="n">Suite</span> <span class="n">usage</span> <span class="n">and</span> <span class="n">error</span> <span class="n">reporting</span> <span class="n">information</span><span class="p">?</span> <span class="n">Yes</span>
</div></code></pre><p>This opens a browser window where you can sign in with Google. After selecting your account, you’ll see this:</p><figure><picture><source srcset="images/firebase-cli-login.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Firebase CLI wants to access your Google Account" srcset="images/firebase-cli-login.png 2x"/></picture></figure><p>Click "Allow" and close the window. Now, the Firebase CLI is logged in.</p><blockquote><p>Note: Make sure you use the Google account linked to the Firebase projects you want to work with. If you’re logged in with the wrong account, run <code>firebase logout</code> and <code>firebase login</code> again.</p></blockquote><h3><a id="installing-the-flutterfire-cli" href="#installing-the-flutterfire-cli">Installing the FlutterFire CLI</a></h3><p>To install the FlutterFire CLI, run:</p><pre><code><div class="highlight"><span></span>dart<span class="w"> </span>pub<span class="w"> </span>global<span class="w"> </span>activate<span class="w"> </span>flutterfire_cli
</div></code></pre><p>Then, check that you’re on version <code>1.0.0</code> or above by running <code>flutterfire --version</code>.</p><h2><a id="flutterfire-config-syntax-with-multiple-flavors" href="#flutterfire-config-syntax-with-multiple-flavors">FlutterFire Config Syntax with Multiple Flavors</a></h2><p>Let's consider this command, which generates all the config files for the <code>dev</code> flavor:</p><pre><code><div class="highlight"><span></span>flutterfire<span class="w"> </span>config<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>--project<span class="o">=</span>flutter-ship-dev<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>--out<span class="o">=</span>lib/firebase_options_dev.dart<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>--ios-bundle-id<span class="o">=</span>com.codewithandrea.flutterShipApp.dev<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>--ios-out<span class="o">=</span>ios/flavors/dev/GoogleService-Info.plist<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>--android-package-name<span class="o">=</span>com.codewithandrea.flutter_ship_app.dev<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>--android-out<span class="o">=</span>android/app/src/dev/google-services.json
</div></code></pre><p>Here’s what each argument does:</p><ul><li><code>--project</code>: The Firebase project to use (note: pass the <strong>project ID</strong>, not the alias).</li><li><code>--out</code>: Output path for the Firebase config file.</li><li><code>--ios-bundle-id</code>: iOS app’s bundle ID. Find it in Xcode under <strong>Runner</strong> &gt; <strong>General</strong> &gt; <strong>Identity</strong> &gt; <strong>Bundle Identifier</strong>.</li><li><code>--ios-out</code>: Output path for the iOS <code>GoogleService-Info.plist</code>.</li><li><code>--android-package-name</code>: Android app’s package name (found as <code>applicationId</code> in <code>android/app/build.gradle.kts</code>).</li><li><code>--android-out</code>: Output path for the Android <code>google-services.json</code>.</li></ul><blockquote><p>To learn about all the available options, run <code>flutterfire config --help</code>.</p></blockquote><p>To use this command, you can:</p><ol><li>Copy it into your terminal.</li><li>Update the <code>project</code>, <code>ios-bundle-id</code>, and <code>android-package-name</code> for your app.</li><li>Run it and follow the interactive prompts (we’ll cover these in a moment).</li></ol><p>But you’ll need to repeat this for the <code>stg</code> and <code>prod</code> flavors. That’s time-consuming and error prone.</p><p>Let's see if we can automate the process. 👇</p><h2><a id="easier-setup-with-a-shell-script" href="#easier-setup-with-a-shell-script">Easier Setup with a Shell Script</a></h2><p>While <code>flutterfire config</code> handles most of the work, you still need to run it for each flavor with different arguments.</p><p>To streamline this, create a <code>flutterfire-config.sh</code> script and save it at the root of your project:</p><pre><code><div class="highlight"><span></span><span class="ch">#!/bin/bash</span>
<span class="c1"># Script to generate Firebase configuration files for different environments/flavors</span>
<span class="c1"># Feel free to reuse and adapt this script for your own projects</span>

<span class="k">if</span><span class="w"> </span><span class="o">[[</span><span class="w"> </span><span class="nv">$#</span><span class="w"> </span>-eq<span class="w"> </span><span class="m">0</span><span class="w"> </span><span class="o">]]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">  </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;Error: No environment specified. Use &#39;dev&#39;, &#39;stg&#39;, or &#39;prod&#39;.&quot;</span>
<span class="w">  </span><span class="nb">exit</span><span class="w"> </span><span class="m">1</span>
<span class="k">fi</span>

<span class="k">case</span><span class="w"> </span><span class="nv">$1</span><span class="w"> </span><span class="k">in</span>
<span class="w">  </span>dev<span class="o">)</span>
<span class="w">    </span>flutterfire<span class="w"> </span>config<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--project<span class="o">=</span>flutter-ship-dev<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--out<span class="o">=</span>lib/firebase_options_dev.dart<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--ios-bundle-id<span class="o">=</span>com.codewithandrea.flutterShipApp.dev<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--ios-out<span class="o">=</span>ios/flavors/dev/GoogleService-Info.plist<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--android-package-name<span class="o">=</span>com.codewithandrea.flutter_ship_app.dev<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--android-out<span class="o">=</span>android/app/src/dev/google-services.json
<span class="w">    </span><span class="p">;;</span>
<span class="w">  </span>stg<span class="o">)</span>
<span class="w">    </span>flutterfire<span class="w"> </span>config<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--project<span class="o">=</span>flutter-ship-stg<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--out<span class="o">=</span>lib/firebase_options_stg.dart<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--ios-bundle-id<span class="o">=</span>com.codewithandrea.flutterShipApp.stg<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--ios-out<span class="o">=</span>ios/flavors/stg/GoogleService-Info.plist<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--android-package-name<span class="o">=</span>com.codewithandrea.flutter_ship_app.stg<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--android-out<span class="o">=</span>android/app/src/stg/google-services.json
<span class="w">    </span><span class="p">;;</span>
<span class="w">  </span>prod<span class="o">)</span>
<span class="w">    </span>flutterfire<span class="w"> </span>config<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--project<span class="o">=</span>flutter-ship-prod<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--out<span class="o">=</span>lib/firebase_options_prod.dart<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--ios-bundle-id<span class="o">=</span>com.codewithandrea.flutterShipApp<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--ios-out<span class="o">=</span>ios/flavors/prod/GoogleService-Info.plist<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--android-package-name<span class="o">=</span>com.codewithandrea.flutter_ship_app<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>--android-out<span class="o">=</span>android/app/src/prod/google-services.json
<span class="w">    </span><span class="p">;;</span>
<span class="w">  </span>*<span class="o">)</span>
<span class="w">    </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;Error: Invalid environment specified. Use &#39;dev&#39;, &#39;stg&#39;, or &#39;prod&#39;.&quot;</span>
<span class="w">    </span><span class="nb">exit</span><span class="w"> </span><span class="m">1</span>
<span class="w">    </span><span class="p">;;</span>
<span class="k">esac</span>
</div></code></pre><p>With this script, you still need to set the correct arguments for your project, but <strong>you only need to do this once</strong>.</p><p>Then, generating all the Firebase config files becomes a breeze—no need to remember each argument.</p><p>Time to take take the script for a ride. 👇</p><h2><a id="running-the-flutterfire-script-for-each-flavor" href="#running-the-flutterfire-script-for-each-flavor">Running the FlutterFire Script for each Flavor</a></h2><p>To configure the <code>dev</code> flavor, run:</p><pre><code><div class="highlight"><span></span>./flutterfire-config.sh<span class="w"> </span>dev
</div></code></pre><p>When prompted, select "Build configuration":</p><pre><code><div class="highlight"><span></span>? You have to choose a configuration type. Either build configuration (most likely choice) or a target set up. ›                    
❯ Build configuration
  Target   
</div></code></pre><p>Then, choose the <code>Debug-dev</code> build configuration:</p><pre><code><div class="highlight"><span></span>? Please choose one of the following build configurations ›
  Debug                                        
  Release                                        
  Profile                                        
❯ Debug-dev                                      
  Profile-dev                                        
  Release-dev                                        
  Debug-stg                                        
  Profile-stg                                        
  Release-stg                                        
  Debug-prod                                        
  Profile-prod                                        
  Release-prod
</div></code></pre><blockquote><p><strong>Note</strong>: If you encounter a <strong>"Failed to list Firebase projects"</strong> error, run <code>firebase logout</code>, then <code>firebase login</code>, and try again.</p></blockquote><p>Next, choose the platforms you want to configure:</p><pre><code><div class="highlight"><span></span>? Which platforms should your configuration support (use arrow keys &amp; space to select)? ›
✔ android                                      
✔ ios                                      
  macos                                        
✔ web                                      
  windows
</div></code></pre><p>This step may take some time as the CLI registers the necessary apps with Firebase. If successful, you’ll see a confirmation similar to this:</p><pre><code><div class="highlight"><span></span>✔ You have to choose a configuration type. Either build configuration (most likely choice) or a target set up. · Build configuration
✔ Please choose one of the following build configurations · Debug-dev
i Found 40 Firebase projects. Selecting project flutter-ship-dev.         
✔ Which platforms should your configuration support (use arrow keys &amp; space to select)? · android, ios, web
i Firebase android app com.codewithandrea.flutter_ship_app.dev is not registered on Firebase project flutter-ship-dev.
i Registered a new Firebase android app on Firebase project flutter-ship-dev.
i Firebase ios app com.codewithandrea.flutterShipApp.dev is not registered on Firebase project flutter-ship-dev.
i Registered a new Firebase ios app on Firebase project flutter-ship-dev. 
i Firebase web app flutter_ship_app (web) is not registered on Firebase project flutter-ship-dev.
i Registered a new Firebase web app on Firebase project flutter-ship-dev. 

Firebase configuration file lib/firebase_options_dev.dart generated successfully with the following Firebase apps:

Platform  Firebase App Id
web       1:424176442589:web:c86e231d1eeaba0e90cf34
android   1:424176442589:android:c5841ba53606b4c490cf34
ios       1:424176442589:ios:592b56a800affa4e90cf34

Learn more about using this file and next steps from the documentation:
 &gt; https://firebase.google.com/docs/flutter/setup
</div></code></pre><p>Next, repeat the same steps for the <code>stg</code> flavor by running:</p><pre><code><div class="highlight"><span></span>./flutterfire-config.sh<span class="w"> </span>stg
</div></code></pre><p>And again for the <code>prod</code> flavor:</p><pre><code><div class="highlight"><span></span>./flutterfire-config.sh<span class="w"> </span>prod
</div></code></pre><p>Once complete, your project will have these new files:</p><pre><code><div class="highlight"><span></span>lib/firebase_options_dev.dart
lib/firebase_options_stg.dart
lib/firebase_options_prod.dart
ios/flavors/dev/GoogleService-Info.plist
ios/flavors/stg/GoogleService-Info.plist
ios/flavors/prod/GoogleService-Info.plist
android/app/src/dev/google-services.json
android/app/src/stg/google-services.json
android/app/src/prod/google-services.json
</div></code></pre><h3><a id="should-the-firebase-config-files-be-added-to-git?" href="#should-the-firebase-config-files-be-added-to-git?">Should the Firebase config files be added to Git?</a></h3><p>The files above don’t contain sensitive information, so it’s safe to commit them to Git.</p><p>However, in my open-source projects, I prefer to add them to <code>.gitignore</code>:</p><pre><code><div class="highlight"><span></span><span class="c1"># Ignore Firebase configuration files</span>
lib/firebase_options*.dart
ios/Runner/GoogleService-Info.plist
ios/flavors/*/GoogleService-Info.plist
macos/Runner/GoogleService-Info.plist
macos/flavors/*/GoogleService-Info.plist
android/app/google-services.json
android/app/src/*/google-services.json
</div></code></pre><p>This has two implications:</p><ul><li>For a fresh checkout, you’ll need to run <code>flutterfire-config.sh</code> again for each flavor.</li><li>On CI, you can store these files as environment secrets and add a pre-build step to restore them to their correct locations.</li></ul><h2><a id="flutterfire-setup-complete-✅" href="#flutterfire-setup-complete-✅">FlutterFire setup complete ✅</a></h2><p>If you followed all the steps above without errors, all the Firebase configuration files should now be in your project.</p><p>Before running the app with Firebase, there are a few more steps to tackle:</p><ul><li>Install the <code>firebase_core</code> package and verify the app runs on Android and iOS</li><li>Initialize Firebase when the app starts</li></ul><p>Let’s walk through them. 👇</p><h2><a id="installing-the-firebase_core-package" href="#installing-the-firebase_core-package">Installing the firebase_core package</a></h2><p>To add <code>firebase_core</code>, run this in your terminal:</p><pre><code><div class="highlight"><span></span>flutter<span class="w"> </span>pub<span class="w"> </span>add<span class="w"> </span>firebase_core
flutter<span class="w"> </span>pub<span class="w"> </span>get
</div></code></pre><h3><a id="running-the-app-on-android" href="#running-the-app-on-android">Running the app on Android</a></h3><p>If your Android app was configured with <a href="https://pub.dev/packages/flutterfire_cli/changelog#110">FlutterFire CLI 1.1.0</a> or above, it should run without errors.</p><p>But if your FlutterFire setup is incorrect, you may encounter this error:</p><pre><code><div class="highlight"><span></span>Plugin [id: &#39;com.google.gms.google-services&#39;] was not found in any of the following sources:

- Gradle Core Plugins (plugin is not in &#39;org.gradle&#39; namespace)
- Included Builds (No included builds contain this plugin)
- Plugin Repositories (plugin dependency must include a version number for this source)
</div></code></pre><p>To fix this, open <code>android/settings.gradle.kts</code> and ensure <code>com.google.gms.google-services</code> is added as a plugin:</p><pre><code><div class="highlight"><span></span><span class="n">plugins</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;dev.flutter.flutter-plugin-loader&quot;</span><span class="p">)</span><span class="w"> </span><span class="n">version</span><span class="w"> </span><span class="s">&quot;1.0.0&quot;</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;com.android.application&quot;</span><span class="p">)</span><span class="w"> </span><span class="n">version</span><span class="w"> </span><span class="s">&quot;8.7.0&quot;</span><span class="w"> </span><span class="n">apply</span><span class="w"> </span><span class="kc">false</span>
<span class="w">    </span><span class="c1">// START: FlutterFire Configuration</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;com.google.gms.google-services&quot;</span><span class="p">)</span><span class="w"> </span><span class="n">version</span><span class="p">(</span><span class="s">&quot;4.3.15&quot;</span><span class="p">)</span><span class="w"> </span><span class="n">apply</span><span class="w"> </span><span class="kc">false</span>
<span class="w">    </span><span class="c1">// END: FlutterFire Configuration</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;org.jetbrains.kotlin.android&quot;</span><span class="p">)</span><span class="w"> </span><span class="n">version</span><span class="w"> </span><span class="s">&quot;1.8.22&quot;</span><span class="w"> </span><span class="n">apply</span><span class="w"> </span><span class="kc">false</span>
<span class="p">}</span>
</div></code></pre><p>The same plugin should also be listed in the <code>plugins</code> block in <code>android/app/build.gradle.kts</code>:</p><pre><code><div class="highlight"><span></span><span class="n">plugins</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;com.android.application&quot;</span><span class="p">)</span>
<span class="w">    </span><span class="c1">// START: FlutterFire Configuration</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;com.google.gms.google-services&quot;</span><span class="p">)</span>
<span class="w">    </span><span class="c1">// END: FlutterFire Configuration</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;kotlin-android&quot;</span><span class="p">)</span>
<span class="w">    </span><span class="c1">// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.</span>
<span class="w">    </span><span class="n">id</span><span class="p">(</span><span class="s">&quot;dev.flutter.flutter-gradle-plugin&quot;</span><span class="p">)</span>
<span class="p">}</span>
</div></code></pre><p>After applying this fix, the Android app should run correctly.</p><p>Note: you can find the latest version of <code>com.google.gms.google-services</code> in <a href="https://maven.google.com/web/index.html?q=google-services#com.google.gms:google-services">Google's Maven Repository</a>.</p><h3><a id="running-the-app-on-ios" href="#running-the-app-on-ios">Running the app on iOS</a></h3><p>Before running the iOS app, open <code>ios/Podfile</code> and ensure the platform version is set to <code>13.0</code> or higher:</p><pre><code><div class="highlight"><span></span><span class="c1"># Uncomment this line to define a global platform for your project</span>
<span class="n">platform</span><span class="w"> </span><span class="ss">:ios</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;13.0&#39;</span>
</div></code></pre><p>Run <code>pod install</code>, and you should be able to run the app on iOS.</p><h2><a id="firebase-initialization-during-app-startup" href="#firebase-initialization-during-app-startup">Firebase Initialization During App Startup</a></h2><p>According to the <a href="https://firebase.google.com/docs/flutter/setup">official docs</a>, you should add the Firebase initialization code to <code>lib/main.dart</code>:</p><pre><code><div class="highlight"><span></span><span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/firebase_options.dart&#39;</span><span class="p">;</span>

<span class="c1">// inside main()</span>
<span class="kd">await</span><span class="w"> </span><span class="n">Firebase</span><span class="p">.</span><span class="n">initializeApp</span><span class="p">(</span>
<span class="w">  </span><span class="nl">options:</span><span class="w"> </span><span class="n">DefaultFirebaseOptions</span><span class="p">.</span><span class="n">currentPlatform</span><span class="p">,</span>
<span class="p">);</span>
</div></code></pre><p>However, this default setup won’t work for us because we have separate configuration files for each flavor:</p><figure><picture><source srcset="images/vscode-firebase-config-gitignored.webp 2x" type="image/webp"/><img class="bottom-40px" alt="The Firebase options files" srcset="images/vscode-firebase-config-gitignored.png 2x"/></picture></figure><p>So, how can we handle this?</p><h3><a id="option-1-centralize-the-firebase-initialization-logic" href="#option-1-centralize-the-firebase-initialization-logic">Option 1: Centralize the Firebase Initialization logic</a></h3><p>One option is to create a <code>firebase.dart</code> file with the following code:</p><pre><code><div class="highlight"><span></span><span class="c1">// firebase.dart</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:firebase_core/firebase_core.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter/foundation.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter/services.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/firebase_options_prod.dart&#39;</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">prod</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/firebase_options_stg.dart&#39;</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">stg</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/firebase_options_dev.dart&#39;</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">dev</span><span class="p">;</span>

<span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">initializeFirebaseApp</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="c1">// Determine which Firebase options to use based on the flavor</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="n">firebaseOptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">switch</span><span class="w"> </span><span class="p">(</span><span class="n">appFlavor</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="s1">&#39;prod&#39;</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">prod</span><span class="p">.</span><span class="n">DefaultFirebaseOptions</span><span class="p">.</span><span class="n">currentPlatform</span><span class="p">,</span>
<span class="w">    </span><span class="s1">&#39;stg&#39;</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">stg</span><span class="p">.</span><span class="n">DefaultFirebaseOptions</span><span class="p">.</span><span class="n">currentPlatform</span><span class="p">,</span>
<span class="w">    </span><span class="s1">&#39;dev&#39;</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">dev</span><span class="p">.</span><span class="n">DefaultFirebaseOptions</span><span class="p">.</span><span class="n">currentPlatform</span><span class="p">,</span>
<span class="w">    </span><span class="n">_</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="n">UnsupportedError</span><span class="p">(</span><span class="s1">&#39;Invalid flavor: </span><span class="si">$</span><span class="n">flavor</span><span class="s1">&#39;</span><span class="p">),</span>
<span class="w">  </span><span class="p">};</span>
<span class="w">  </span><span class="kd">await</span><span class="w"> </span><span class="n">Firebase</span><span class="p">.</span><span class="n">initializeApp</span><span class="p">(</span><span class="nl">options:</span><span class="w"> </span><span class="n">firebaseOptions</span><span class="p">);</span>
<span class="p">}</span>
</div></code></pre><p>This works by switching on the <a href="https://api.flutter.dev/flutter/services/appFlavor-constant.html"><code>appFlavor</code></a> constant to return the correct <code>FirebaseOptions</code> object based on the flavor.</p><blockquote><p><strong>Note</strong>: When running on Flutter web with the <code>--flavor</code> option, you'll get a warning you that flavors are not fully supported. But the <code>appFlavor</code> constant will still return the correct value.</p></blockquote><p>Now, you can simply call <code>await initializeFirebaseApp()</code> in <code>lib/main.dart</code>, which remains the <strong>single entry point</strong> for the app:</p><pre><code><div class="highlight"><span></span><span class="k">import</span><span class="w"> </span><span class="s1">&#39;firebase.dart&#39;</span><span class="p">;</span>

<span class="kt">void</span><span class="w"> </span><span class="n">main</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="n">WidgetsFlutterBinding</span><span class="p">.</span><span class="n">ensureInitialized</span><span class="p">();</span>
<span class="w">  </span><span class="kd">await</span><span class="w"> </span><span class="n">initializeFirebaseApp</span><span class="p">();</span>
<span class="w">  </span><span class="n">runApp</span><span class="p">(</span><span class="k">const</span><span class="w"> </span><span class="n">MainApp</span><span class="p">());</span>
<span class="p">}</span>
</div></code></pre><p>With this setup, the Flutter app will initialize and connect to the correct Firebase project, depending on the flavor.</p><p>However, there's one issue. 👇</p><h3><a id="all-the-firebase-config-files-are-bundled-no-tree-shaking" href="#all-the-firebase-config-files-are-bundled-no-tree-shaking">All the Firebase Config Files are Bundled (No Tree Shaking)</a></h3><p>If you look closely at <code>firebase.dart</code>, you’ll notice that although the correct Firebase config is selected based on the flavor, <strong>all three</strong> <code>firebase_options_*.dart</code> files are still imported:</p><pre><code><div class="highlight"><span></span><span class="c1">// firebase.dart</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:firebase_core/firebase_core.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter/foundation.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter/services.dart&#39;</span><span class="p">;</span>
<span class="c1">// Note: all three files are imported</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/firebase_options_prod.dart&#39;</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">prod</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/firebase_options_stg.dart&#39;</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">stg</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/firebase_options_dev.dart&#39;</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">dev</span><span class="p">;</span>

<span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">initializeFirebaseApp</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="c1">// Determine which Firebase options to use based on the flavor</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="n">firebaseOptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">switch</span><span class="w"> </span><span class="p">(</span><span class="n">appFlavor</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="s1">&#39;prod&#39;</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">prod</span><span class="p">.</span><span class="n">DefaultFirebaseOptions</span><span class="p">.</span><span class="n">currentPlatform</span><span class="p">,</span>
<span class="w">    </span><span class="s1">&#39;stg&#39;</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">stg</span><span class="p">.</span><span class="n">DefaultFirebaseOptions</span><span class="p">.</span><span class="n">currentPlatform</span><span class="p">,</span>
<span class="w">    </span><span class="s1">&#39;dev&#39;</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">dev</span><span class="p">.</span><span class="n">DefaultFirebaseOptions</span><span class="p">.</span><span class="n">currentPlatform</span><span class="p">,</span>
<span class="w">    </span><span class="n">_</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="n">UnsupportedError</span><span class="p">(</span><span class="s1">&#39;Invalid flavor: </span><span class="si">$</span><span class="n">flavor</span><span class="s1">&#39;</span><span class="p">),</span>
<span class="w">  </span><span class="p">};</span>
<span class="w">  </span><span class="kd">await</span><span class="w"> </span><span class="n">Firebase</span><span class="p">.</span><span class="n">initializeApp</span><span class="p">(</span><span class="nl">options:</span><span class="w"> </span><span class="n">firebaseOptions</span><span class="p">);</span>
<span class="p">}</span>
</div></code></pre><p>This means that <strong>all three files are compiled and bundled</strong> during the build process because tree-shaking doesn't work here (the <code>switch</code> happens at <strong>runtime</strong>).</p><p>In theory, this could expose your development or staging environment details (which may be less secure than production) if someone reverse engineers your app.</p><p>While this might not be a big issue for apps that don’t handle sensitive data, it’s still a potential risk. If you want to mitigate this entirely, consider a more secure approach. 👇</p><h3><a id="option-2-use-multiple-entry-points" href="#option-2-use-multiple-entry-points">Option 2: Use Multiple Entry Points</a></h3><p>As we've seen, this code can be problematic:</p><pre><code><div class="highlight"><span></span><span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/firebase_options_prod.dart&#39;</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">prod</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/firebase_options_stg.dart&#39;</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">stg</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/firebase_options_dev.dart&#39;</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">dev</span><span class="p">;</span>
</div></code></pre><p>A more secure approach is to create three separate entry points—<code>main_dev.dart</code>, <code>main_stg.dart</code>, and <code>main_prod.dart</code>—which look like this:</p><pre><code><div class="highlight"><span></span><span class="c1">// main_dev.dart</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter_ship_app/firebase_options_dev.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;main.dart&#39;</span><span class="p">;</span>

<span class="kt">void</span><span class="w"> </span><span class="n">main</span><span class="p">()</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="n">runMainApp</span><span class="p">(</span><span class="n">DefaultFirebaseOptions</span><span class="p">.</span><span class="n">currentPlatform</span><span class="p">);</span>
<span class="p">}</span>
</div></code></pre><p>These files should do one thing only: import the correct <code>firebase_options_*.dart</code> file and pass the config as an argument to a function inside <code>main.dart</code> that performs the actual initialization. Here’s an example:</p><pre><code><div class="highlight"><span></span><span class="c1">// main.dart</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:firebase_core/firebase_core.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter/material.dart&#39;</span><span class="p">;</span>

<span class="kt">void</span><span class="w"> </span><span class="n">runMainApp</span><span class="p">(</span><span class="n">FirebaseOptions</span><span class="w"> </span><span class="n">firebaseOptions</span><span class="p">)</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="n">WidgetsFlutterBinding</span><span class="p">.</span><span class="n">ensureInitialized</span><span class="p">();</span>
<span class="w">  </span><span class="kd">await</span><span class="w"> </span><span class="n">Firebase</span><span class="p">.</span><span class="n">initializeApp</span><span class="p">(</span><span class="nl">options:</span><span class="w"> </span><span class="n">firebaseOptions</span><span class="p">);</span>
<span class="w">  </span><span class="n">runApp</span><span class="p">(</span><span class="k">const</span><span class="w"> </span><span class="n">MainApp</span><span class="p">());</span>
<span class="p">}</span>
</div></code></pre><p>This approach ensures that only the <strong>required Firebase configuration</strong> file is bundled, making it a secure and efficient solution for managing multiple flavors.</p><p>But how do you run the app with the right flavor? 👇</p><h3><a id="running-the-app-with-a-specific-flavor" href="#running-the-app-with-a-specific-flavor">Running the App with a Specific Flavor</a></h3><p>If you use the second option outlined above, you'll have four files:</p><ul><li><code>main_dev.dart</code>: Entry point for <code>dev</code></li><li><code>main_stg.dart</code>: Entry point for <code>stg</code></li><li><code>main_prod.dart</code>: Entry point for <code>prod</code></li><li><code>main.dart</code>: Contains the app initialization code</li></ul><p>As a result, you can run the app with a specific flavor using these commands:</p><pre><code><div class="highlight"><span></span>flutter<span class="w"> </span>run<span class="w"> </span>--flavor<span class="w"> </span>dev<span class="w"> </span>-t<span class="w"> </span>lib/main_dev.dart
flutter<span class="w"> </span>run<span class="w"> </span>--flavor<span class="w"> </span>stg<span class="w"> </span>-t<span class="w"> </span>lib/main_stg.dart
flutter<span class="w"> </span>run<span class="w"> </span>--flavor<span class="w"> </span>prod<span class="w"> </span>-t<span class="w"> </span>lib/main_prod.dart
</div></code></pre><p>This way, the correct entry point is used for each flavor, ensuring that the right Firebase environment is connected when the app launches.</p><blockquote><p>If you follow this approach, make sure to update your local configuration (e.g., <code>.vscode/launch.json</code>) and CI/CD scripts to reflect these changes.</p></blockquote><h3><a id="which-option-should-you-choose?" href="#which-option-should-you-choose?">Which Option Should you Choose?</a></h3><p>Both options have their pros and cons, and the right choice depends on your project’s needs:</p><ul><li><strong>Option 1: Centralized Firebase Initialization</strong>. This approach is easier and quicker to implement. It allows you to use a single <code>main.dart</code> file and handle flavor-specific Firebase options dynamically at runtime. However, because all Firebase configuration files are bundled in the final app (even if they’re not used), it's not the best choice for security reasons.</li><li><strong>Option 2: Multiple Entry Points for Each Flavor</strong>. This option requires a bit more setup because you’ll need to create separate entry points for each flavor (<code>main_dev.dart</code>, <code>main_stg.dart</code>, <code>main_prod.dart</code>). However, it only bundles the necessary Firebase configuration file for each build, making it a more secure solution, since an attacker won’t have access to the environment details of other flavors.</li></ul><p>While option 2 takes a bit more work, I recommend it for multi-flavor Flutter apps that use Firebase. 👍</p><blockquote><p>Option 1 works just fine for <strong>non-Firebase</strong> apps, since you can use <code>--dart-define-from-file</code> and define environment variables inside separate files (e.g. <code>.env.dev</code>, <code>.env.stg</code>, <code>.env.prod</code>) for each flavor. To learn more, read: <a href="https://codewithandrea.com/articles/flutter-api-keys-dart-define-env-files/">How to Store API Keys in Flutter: --dart-define vs .env files</a>.</p></blockquote><h2><a id="conclusion" href="#conclusion">Conclusion</a></h2><p>By leveraging <strong>FlutterFire</strong> alongside a simple shell script, we’ve streamlined what used to be a complex and error-prone flavoring process. Instead of manually configuring Firebase for each flavor, you can now generate the necessary files for all environments with a single script, saving time and reducing the chance of mistakes.</p><p>The <strong>centralized Firebase initialization</strong> option offers a quick and simple way to connect your app to the correct Firebase project at startup, using a single <code>main.dart</code> file and flavor-specific logic. However, this approach bundles all Firebase configurations, which may not be ideal for security-sensitive apps.</p><p>For more secure setups, the <strong>multiple entry points</strong> strategy ensures that only the necessary Firebase configuration is included in each build, making it a better choice when handling sensitive data or production-grade apps.</p><p>With either approach, your Flutter app will automatically connect to the appropriate Firebase environment—whether that’s <code>dev</code>, <code>stg</code>, or <code>prod</code>—ensuring your app behaves as expected in every stage of development and production.</p><figure><picture><source srcset="images/diagram-flutter-firebase-flavors.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Each Flutter flavor maps to a separate Firebase project" srcset="images/diagram-flutter-firebase-flavors.png 2x"/></picture></figure><p>This setup makes it easier to manage multiple flavors in your Flutter &amp; Firebase apps, and I've been happily using it in production for my own apps. ✅</p><h2><a id="new-course-flutter-in-production" href="#new-course-flutter-in-production">New Course: Flutter in Production</a></h2><p>When it comes to <strong>shipping</strong> and <strong>maintaining</strong> apps in production, there are many important aspects to consider:</p><ul><li><strong>Preparing for release</strong>: splash screens, flavors, environments, error reporting, analytics, force update, privacy, T&amp;Cs</li><li><strong>App Submissions</strong>: app store metadata &amp; screenshots, compliance, testing vs distribution tracks, dealing with rejections</li><li><strong>Release automation:</strong> CI workflows, environment variables, custom build steps, code signing, uploading to the stores</li><li><strong>Post-release</strong>: error monitoring, bug fixes, addressing user feedback, adding new features, over-the-air updates</li></ul><p>My latest course will help you get your app to the stores faster and with fewer headaches.</p><p>If you’re interested, you can learn more and enroll here. 👇</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/fixing-build-issues-nuclear-option/</guid><title>Fixing Build Issues - Nuclear Option</title><description>If you have a Flutter project that no longer builds on a specific platform, you can delete the whole folder and generate it again.</description><link>https://codewithandrea.com/tips/fixing-build-issues-nuclear-option/</link><pubDate>Wed, 16 Oct 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If you have a Flutter project that no longer builds on a specific platform, you can try this:</p><ul><li>delete the whole folder</li><li>use the Flutter CLI to generate it again</li><li>discard any unwanted changes</li></ul><p>When it works, this can save you hours of frustration. 😌</p><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Fixing Build Issues - Nuclear Option" srcset="images/twitter-card.png 2x"/></picture></figure><p>Here are the steps:</p><pre><code><div class="highlight"><span></span><span class="c1"># Commit to git before making any changes</span>
git<span class="w"> </span>add<span class="w"> </span>.<span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span>git<span class="w"> </span>commit<span class="w"> </span>-m<span class="w"> </span><span class="s2">&quot;Working copy&quot;</span>
<span class="c1"># Delete android folder</span>
rm<span class="w"> </span>-rf<span class="w"> </span>android
<span class="c1"># Create it again with the Flutter CLI</span>
flutter<span class="w"> </span>create<span class="w"> </span>.<span class="w"> </span>--platforms<span class="w"> </span>android
<span class="c1"># See what&#39;s changed, reapply previous settings</span>
git<span class="w"> </span>diff
<span class="c1"># Run again</span>
flutter<span class="w"> </span>run
<span class="c1"># All good? Commit to git</span>
git<span class="w"> </span>add<span class="w"> </span>.<span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span>git<span class="w"> </span>commit<span class="w"> </span>-m<span class="w"> </span><span class="s2">&quot;Updated Android project&quot;</span>
</div></code></pre><hr><p>To learn more about effective techniques for shipping your apps in production, check out my latest course:</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/force-update-helper/</guid><title>Force Update with Remote Config</title><description>If you ever needed a force update prompt that is controlled remotely, you can use the force_update_helper package.</description><link>https://codewithandrea.com/tips/force-update-helper/</link><pubDate>Tue, 15 Oct 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Ever needed a force update prompt that is controlled remotely?</p><p>There's a package for that: <a href="https://pub.dev/packages/force_update_helper">force<em>update</em>helper</a>.</p><p>This works by comparing the <strong>current</strong> app version with a <strong>required</strong> app version that is fetched from a remote source.</p><figure><picture><source srcset="images/199.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Force Update with Remote Config" srcset="images/199.png 2x"/></picture></figure><p>The package requires a bit of setup, and this is all documented in the README:</p><ul><li><a href="https://pub.dev/packages/force_update_helper">force_update_helper</a></li></ul><p>Example apps are also included, showing how to use a GitHub Gist or a Dart Shelf app as the remote source.</p><hr><p>To learn more about force update and how to get your app ready for production, check out my latest course:</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/show-licenses-flutter-app/</guid><title>Show the Licenses in your Flutter app</title><description>Your Flutter app should show the licenses for packages in use. This is often a legal requirement, as many open-source licenses require attribution.</description><link>https://codewithandrea.com/tips/show-licenses-flutter-app/</link><pubDate>Mon, 14 Oct 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>Your Flutter app should show the licenses for packages in use. This is often a legal requirement, as many open-source licenses require attribution.</p><p>To do this, you have two options: - call the <a href="https://api.flutter.dev/flutter/material/showLicensePage.html"><code>showLicensePage</code></a> API directly - use the <a href="https://api.flutter.dev/flutter/material/AboutListTile-class.html"><code>AboutListTile</code></a> widget</p><figure><picture><source srcset="images/198.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Show the Licenses in your Flutter app" srcset="images/198.png 2x"/></picture></figure><blockquote><p>Note that the <a href="https://api.flutter.dev/flutter/material/showLicensePage.html"><code>showLicensePage</code></a> function displays licenses for all the packages your app depends on, including transitive dependencies. The large number of licenses is expected since even a few direct dependencies can pull in many others.</p></blockquote><p>You can also use a more custom approach and get the raw licenses from the <a href="https://api.flutter.dev/flutter/foundation/LicenseRegistry-class.html"><code>LicenseRegistry</code></a> class.</p><p>For more details, read:</p><ul><li><a href="https://docs.flutter.dev/resources/faq#how-can-i-determine-the-licenses-my-flutter-application-needs-to-show">How can I determine the licenses my Flutter application needs to show?</a></li></ul><p>To learn more about how to get your app ready for production, check out my latest course:</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/articles/key-steps-before-releasing-flutter-app/</guid><title>6 Key Steps to Take Before Releasing your Next Flutter App</title><description>Prepare your Flutter app for launch with these 6 steps, including flavors and environments, error monitoring, force updates, and in-app reviews.</description><link>https://codewithandrea.com/articles/key-steps-before-releasing-flutter-app/</link><pubDate>Fri, 11 Oct 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Launching a Flutter app is more than just coding and hitting "publish". To increase your chances of success, there are crucial <strong>pre-release steps</strong> you need to take. These steps go beyond development—they involve setting up systems to track your app’s performance, gather user feedback, and ensure smooth updates.</p><p>In this article, we’ll walk through six key challenges you need to consider before releasing your app:</p><ul><li><strong>Flavors and Environments</strong>: keep development, testing, and production separate.</li><li><strong>Error Monitoring</strong>: catch bugs and crashes in production.</li><li><strong>Analytics</strong>: understand how users interact with your app.</li><li><strong>Force Update</strong>: ensure users are always on the latest version.</li><li><strong>In-app user feedback</strong>: gain valuable insights directly from your users.</li><li><strong>In-app reviews</strong>: improve your app’s visibility and ranking in the stores.</li></ul><p>Let’s dive into each of these challenges. 👇</p><blockquote><p>Think of this article as a checklist covering all the <strong>pre-release</strong> steps. For advice about how to <strong>design and develop</strong> your app, read: <a href="https://codewithandrea.com/articles/steps-to-follow-your-next-flutter-app/">8 Steps to Follow When Building Your Next Flutter App</a>.</p></blockquote><h2><a id="1-flavors-and-environments" href="#1-flavors-and-environments">1. Flavors and Environments</a></h2><p>When building and releasing a Flutter app, one of the first challenges is managing <strong>different environments</strong> for development, testing, staging, and production.</p><figure><picture><source srcset="images/diagram-four-environments.webp 1x" type="image/webp"/><img class="bottom-12px" alt="An overview of the most common environments" srcset="images/diagram-four-environments.png 1x"/></picture><figcaption><center><i>An overview of the most common environments</i></center></figcaption></figure><p>Each environment should run in isolation to ensure that your production data, analytics, and error logs stay clean and unaffected during development. For example, you don't want test data mixing with production data, or error reports from development popping up in your production logs.</p><h3><a id="what-are-flavors?" href="#what-are-flavors?">What Are Flavors?</a></h3><p>Environments go hand-in-hand with flavors.</p><p>In Flutter, <strong>flavors</strong> allow you to build distinct versions of your app that connect to the right environment. These flavors can have different app icons, API keys, and even behavior. This is especially useful for QA testing because you can install multiple versions of your app on a single device without conflicts:</p><figure><picture><source srcset="images/ios-icons.webp 2x" type="image/webp"/><img class="bottom-12px" alt="By supporting flavors, you can install and test multiple versions of your app simultaneously" srcset="images/ios-icons.png 2x"/></picture><figcaption><center><i>By supporting flavors, you can install and test multiple versions of your app simultaneously</i></center></figcaption></figure><p>Flavors are also ideal in these scenarios:</p><ul><li><strong>Free and paid</strong>: Publish separate versions of your app, deciding which features are available based on the flavor.</li><li><strong>Whitelabel apps</strong>: Rebrand and customize for multiple customers by applying cosmetic changes (assets, theming, etc.) for each version.</li></ul><blockquote><p><strong>Note</strong>: If your app doesn’t use user-generated content, third-party APIs, or analytics, and doesn’t require different configurations for testing and production, you might not need multiple flavors or environments. In practice, I find this is only true for very simple apps.</p></blockquote><h3><a id="how-to-add-flavors-to-your-flutter-app" href="#how-to-add-flavors-to-your-flutter-app">How to Add Flavors to Your Flutter App</a></h3><p>There are two main ways to set up flavors in Flutter:</p><ol><li><strong>Using the <a href="https://pub.dev/packages/flutter_flavorizr">flutter_flavorizr</a> package</strong>: This makes the process much quicker, but it works best on a new Flutter project since it works by modifying some specific project files with a specific folder structure.</li><li><strong>Manual setup</strong>: This is more time-consuming and error-prone, but it may be your only choice if you’re working on an existing project and need to retrofit flavors without breaking things.</li></ol><blockquote><p>Both approaches are covered in detail in my course about <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a>.</p></blockquote><h3><a id="benefits-of-using-flavors-and-environments" href="#benefits-of-using-flavors-and-environments">Benefits of Using Flavors and Environments</a></h3><ul><li><strong>Safer development</strong>: Keep your testing and production environments separate to avoid data and error pollution.</li><li><strong>Multiple installs</strong>: Test multiple versions of your app on the same device—great for QA and user acceptance testing.</li><li><strong>Whitelabel and free/paid versions</strong>: Easily manage different versions of your app with different features or branding.</li></ul><h2><a id="2-error-monitoring" href="#2-error-monitoring">2. Error Monitoring</a></h2><p>Once your app is published, things get tricky. Unlike in development, where your IDE gives you instant feedback on crashes and errors, in production, you're flying blind unless you have proper error monitoring in place. When users encounter a crash, you need to know <strong>what went wrong</strong>, <strong>how often it happens</strong>, and <strong>how to fix it</strong>—without relying on user reports.</p><p>And with tools like <strong><a href="https://sentry.io">Sentry</a></strong> or <strong><a href="https://firebase.google.com/products/crashlytics/">Crashlytics</a></strong>, you can capture errors in real-time.</p><figure><picture><source srcset="images/sentry-issues.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Sentry page showing the most frequent and recent issues in my Flutter Tips app" srcset="images/sentry-issues.png 2x"/></picture><figcaption><center><i>Sentry page showing the most frequent and recent issues in my Flutter Tips app</i></center></figcaption></figure><p>These tools give you detailed reports, including:</p><ul><li><strong>Stack traces</strong>: Get the exact location in your code where the crash occurred.</li><li><strong>Device and OS information</strong>: Know which platform the error happened on.</li><li><strong>App version</strong>: See if the issue is confined to a specific release.</li><li><strong>Breadcrumbs</strong>: Discover what events led to the crash.</li></ul><h3><a id="when-to-add-error-monitoring" href="#when-to-add-error-monitoring">When to Add Error Monitoring</a></h3><p>You should add error monitoring <strong>before your first release</strong>. Once your app is live, every crash that goes unnoticed is a potential user lost. Plus, if your app is being tested during the app store review process, a crash could lead to rejection. With error monitoring, you’ll have immediate insights into what failed, so you can fix the issue quickly.</p><h3><a id="using-sentry-for-error-monitoring-in-flutter" href="#using-sentry-for-error-monitoring-in-flutter">Using Sentry for Error Monitoring in Flutter</a></h3><p>To monitor errors in your Flutter app, I recommend using <strong><a href="https://sentry.io">Sentry</a></strong>.</p><p>When configured, Sentry will automatically capture and report unhandled exceptions. It also allows you to capture additional context, such as breadcrumbs (e.g., navigation events, HTTP requests) to help you trace what led up to the crash.</p><h3><a id="benefits-of-error-monitoring" href="#benefits-of-error-monitoring">Benefits of Error Monitoring</a></h3><ul><li><strong>Proactive bug fixing</strong>: You can catch and fix issues without waiting for user reports.</li><li><strong>Improved app stability</strong>: As you address crashes earlier, your app becomes more reliable over time.</li><li><strong>Better user experience</strong>: Fewer crashes mean happier users, and happier users leave better reviews.</li></ul><p>Don’t wait until users start complaining—add error monitoring now, and stay ahead of any issues.</p><h2><a id="3-analytics" href="#3-analytics">3. Analytics</a></h2><p>You’ve built an amazing app, but how do you know if people are using it the way you intended? Without analytics, you’re left guessing about user behavior, engagement, and the effectiveness of your features.</p><h3><a id="why-you-need-analytics" href="#why-you-need-analytics">Why You Need Analytics</a></h3><p>Analytics allows you to make <strong>data-driven decisions</strong> instead of relying on assumptions. You can track how users interact with your app, which features they love, and where they might be getting stuck. Some key insights you can gain include:</p><ul><li><strong>User Engagement</strong>: How often do users open your app? How long do they stay? Which features do they interact with the most?</li><li><strong>Retention</strong>: Are users coming back after the first session, or are they leaving after a single use?</li><li><strong>Feature Popularity</strong>: Which features are genuinely useful, and which ones are being ignored?</li></ul><p>With this information, you can optimize your app to meet user needs, improve retention, and ultimately drive more revenue.</p><h3><a id="choosing-an-analytics-provider" href="#choosing-an-analytics-provider">Choosing an Analytics Provider</a></h3><p>When it comes to Flutter, you have several options for analytics providers. Two of the most popular choices are <strong><a href="https://firebase.google.com/docs/analytics/">Firebase Analytics</a></strong> and <strong><a href="https://mixpanel.com">Mixpanel</a></strong>. Both offer robust features but cater to different needs. Firebase is good for basic event tracking, while Mixpanel offers more advanced segmentation and funnel analysis.</p><figure><picture><source srcset="images/mixpanel-user-metrics.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Mixpanel user metrics for my Flutter Tips app" srcset="images/mixpanel-user-metrics.png 2x"/></picture><figcaption><center><i>Mixpanel user metrics for my Flutter Tips app</i></center></figcaption></figure><blockquote><p><strong>Note</strong>: While App Store Connect and the Google Play Console provide basic analytics, their reports are limited to general app performance metrics like downloads, crashes, and retention. They don’t capture custom in-app events or user behavior specific to your app’s unique features. For deeper insights and more control, it’s best to integrate a dedicated analytics SDK.</p></blockquote><p>You'll also want to choose a suitable architecture for tracking custom events, page views, and more. Here's an example of what I use in one of my apps:</p><figure><picture><source srcset="images/diagram-app-analytics-navigator-observer.webp 2x" type="image/webp"/><img class="bottom-12px" alt="App Analytics Architecture with Navigator Observer" srcset="images/diagram-app-analytics-navigator-observer.png 2x"/></picture><figcaption><center><i>App Analytics Architecture with Navigator Observer</i></center></figcaption></figure><h3><a id="benefits-of-using-analytics" href="#benefits-of-using-analytics">Benefits of Using Analytics</a></h3><ul><li><strong>Better Product Decisions</strong>: Understand which features work and which don’t.</li><li><strong>Improved Retention</strong>: Identify why users might be dropping off and take action to keep them engaged.</li><li><strong>Monetization Insights</strong>: Optimize your revenue strategy by tracking purchases and subscriptions.</li></ul><p>In short, <strong>analytics is essential</strong> if you want to grow your app and make informed decisions.</p><h2><a id="4-force-update" href="#4-force-update">4. Force Update</a></h2><p>Imagine this: you’ve just discovered a critical bug in your app that needs to be fixed immediately. If you’re developing a web app, you can quickly deploy the fix, and users will get it instantly. But on mobile, it’s not that simple. Even though iOS and Android support automatic app updates, not all users have it enabled, and updates can take time.</p><h3><a id="why-you-need-force-update" href="#why-you-need-force-update">Why You Need Force Update</a></h3><p>Without a force update mechanism, you can’t assume users will ever be on the latest version of your app. This can cause major headaches, such as:</p><ul><li><strong>Missed security updates</strong>: Users on old versions might be vulnerable to issues you’ve already fixed.</li><li><strong>Backend incompatibility</strong>: You may need to maintain old API versions because some users haven’t updated.</li><li><strong>Limited feature availability</strong>: Some users won’t see your latest improvements or bug fixes unless they update.</li></ul><p>By implementing a <strong>force update</strong> strategy, you can ensure that all users eventually upgrade to the latest version of your app, minimizing these risks.</p><p>Force update should be implemented in the <strong>very first version</strong> of your app. This diagram shows why:</p><figure><picture><source srcset="images/force-update-versions.webp 2x" type="image/webp"/><img class="bottom-12px" alt="The force update prompts will only appear for older versions that support the force update logic" srcset="images/force-update-versions.png 2x"/></picture><figcaption><center><i>The force update prompts will only appear for older versions that support the force update logic</i></center></figcaption></figure><h3><a id="how-force-update-works" href="#how-force-update-works">How Force Update Works</a></h3><p>The basic concept is simple: your app checks its current version against a minimum required version stored on your backend (or Firebase Remote Config, or even a <a href="https://codewithandrea.com/tips/remote-config-github-gist/">GitHub Gist</a>). If the app version is outdated, the user is blocked from continuing until they update.</p><figure><picture><source srcset="images/ios-update-app-store-flutter-tips.webp 2x" type="image/webp"/><img class="bottom-12px" alt="Force update flow on iOS" srcset="images/ios-update-app-store-flutter-tips.png 2x"/></picture><figcaption><center><i>Force update flow on iOS</i></center></figcaption></figure><p>To implement it in Flutter, I recommend using my <strong><a href="https://pub.dev/packages/force_update_helper">force_update_helper</a></strong> package, which allows you to show a force update prompt that is controlled remotely.</p><h3><a id="benefits-of-force-update" href="#benefits-of-force-update">Benefits of Force Update</a></h3><ul><li><strong>Ensure all users are on the latest version</strong>: No more worrying about outdated versions causing issues.</li><li><strong>Simplify backend maintenance</strong>: You can deprecate old API versions without disrupting users.</li><li><strong>Greater control over feature rollouts</strong>: Force users to update when major new features or critical bug fixes are released.</li></ul><p>By adding a force update strategy early on, you’ll save yourself from future headaches and ensure a better experience for your users.</p><blockquote><p>As an alternative to force updates, you can use <strong><a href="https://shorebird.dev/">Shorebird</a></strong> to push app updates directly to your users, without going through the app submission process. You can use Shorebird for free for up to 5,000 patches per month.</p></blockquote><h2><a id="5-in-app-user-feedback" href="#5-in-app-user-feedback">5. In-App User Feedback</a></h2><p>Crash reports are great for identifying technical bugs, but what if users want to provide direct feedback? Maybe they’ve encountered some confusing UI, or they want to suggest a new feature. Without an easy way to collect this feedback, you’re missing out on valuable insights.</p><h3><a id="why-you-need-in-app-user-feedback" href="#why-you-need-in-app-user-feedback">Why You Need In-App User Feedback</a></h3><p>Making it easy for users to provide feedback—without leaving your app—reduces friction and encourages them to share their thoughts. This helps you:</p><ul><li><strong>Identify usability issues</strong>: Users might struggle with certain features, and their feedback can highlight these pain points.</li><li><strong>Prioritize new features</strong>: Understand what users want, so you can focus on building the features they care about most.</li><li><strong>Improve user satisfaction</strong>: By listening to your users and acting on their feedback, you build trust and loyalty.</li></ul><h3><a id="easy-feedback-collection-with-flutter" href="#easy-feedback-collection-with-flutter">Easy Feedback Collection with Flutter</a></h3><p>Using the <a href="https://pub.dev/packages/feedback">Feedback</a> package, you can easily add an interactive feedback system to your Flutter app. When a user clicks a "Send Feedback" option within the app, they can describe the issue, take a screenshot, and submit it directly:</p><figure><picture><img class="bottom-12px" alt="Example showing the feedback package in action" srcset="images/user-feedback-example.gif 1x"/></picture><figcaption><center><i>Example showing the feedback package in action</i></center></figcaption></figure><p>If integrated with a tool like <strong>Sentry</strong> (there's a <a href="https://pub.dev/packages/feedback_sentry">package</a> for that), this feedback can be automatically sent to your error tracking system, allowing you to address both bugs and user feedback in one place.</p><h3><a id="benefits-of-in-app-feedback" href="#benefits-of-in-app-feedback">Benefits of In-App Feedback</a></h3><ul><li><strong>Lower friction</strong>: Users don’t need to switch to email or social media to give feedback—they can do it right within the app.</li><li><strong>Actionable insights</strong>: Annotated screenshots help you understand exactly what users are experiencing.</li><li><strong>Improved user experience</strong>: By acting on feedback, you can continuously improve your app and show users that their input matters.</li></ul><p>Adding in-app feedback is an easy win that can help you catch issues early and keep your users happy.</p><blockquote><p>Note: In addition to in-app feedback, certain apps benefit from fostering an active community of users. Whether it’s through a Discord server, Facebook group, or another platform, building a community can create a sense of belonging and accountability among users.</p></blockquote><h2><a id="6-in-app-reviews" href="#6-in-app-reviews">6. In-App Reviews</a></h2><p>App reviews are critical to the success of any mobile app. They directly impact your app's visibility and ranking in the app stores. Positive reviews can boost your app’s ranking and drive more downloads, while negative reviews can scare off potential users.</p><h3><a id="why-you-need-in-app-reviews" href="#why-you-need-in-app-reviews">Why You Need In-App Reviews</a></h3><p>To get good reviews, you need to make it easy for users to leave them. The best way to do this is by showing an <strong>in-app review prompt</strong> at the right moment—when users are engaged and happy with your app. For instance, this could be after they’ve completed a task, leveled up in a game, or successfully used a feature. Here's an example from my <a href="https://fluttertips.dev/">Flutter Tips app</a>:</p><figure><picture><source srcset="images/flutter-tips-in-app-rating-ios.webp 2x" type="image/webp"/><img class="bottom-40px" alt="When the user likes N tips, show the in-app rating prompt" srcset="images/flutter-tips-in-app-rating-ios.png 2x"/></picture></figure><p>By asking for reviews at the right time, you increase the chances of receiving positive feedback.</p><h3><a id="how-to-ask-for-reviews-in-flutter" href="#how-to-ask-for-reviews-in-flutter">How to Ask for Reviews in Flutter</a></h3><p>The easiest way to request reviews in Flutter is by using the <a href="https://pub.dev/packages/in_app_review">in_app_review</a> package, which allows you to show an in-app review dialog directly within your app:</p><figure><picture><source srcset="images/in-app-review-prompt.webp 2x" type="image/webp"/><img class="bottom-12px" alt="In-app rating prompt for my Flutter Tips app" srcset="images/in-app-review-prompt.png 2x"/></picture><figcaption><center><i>In-app rating prompt for my Flutter Tips app</i></center></figcaption></figure><h3><a id="timing-is-everything" href="#timing-is-everything">Timing is Everything</a></h3><p>If you show the review prompt right away, it may annoy users who haven’t had enough time to form a positive opinion. On the other hand, if you wait too long, many users may never see the prompt. The sweet spot is showing the prompt when users are most satisfied with their experience.</p><blockquote><p><strong>Note</strong>: many popular apps show the rating prompt very early on, as soon as the user has completed the onboarding, or after making a purchase. While this may seem counter-intuitive, it appears to work well in practice.</p></blockquote><h3><a id="benefits-of-in-app-reviews" href="#benefits-of-in-app-reviews">Benefits of In-App Reviews</a></h3><ul><li><strong>More reviews</strong>: Making it easy for users to leave a review increases the number of reviews you receive.</li><li><strong>Higher ratings</strong>: Asking for reviews when users are engaged leads to more positive feedback.</li><li><strong>Boosted app visibility</strong>: More positive reviews improve your app’s ranking in the store, driving more downloads.</li></ul><h2><a id="conclusion" href="#conclusion">Conclusion</a></h2><p>Launching a Flutter app is an exciting milestone, but if you want to avoid headaches down the line, there are several key challenges you need to tackle before hitting "publish". From managing <strong>flavors and environments</strong>, to setting up <strong>error monitoring</strong> and <strong>analytics</strong>, each step ensures your app is ready for real-world use. Adding a <strong>force update</strong> mechanism, collecting <strong>in-app user feedback</strong>, and encouraging <strong>in-app reviews</strong> will help you maintain quality, keep users engaged, and grow your app's presence in the store.</p><p>These steps are essential if you're serious about launching your app successfully, and I cover them in detail in my latest course.</p><h2><a id="new-course-flutter-in-production" href="#new-course-flutter-in-production">New course: Flutter in Production</a></h2><p>When it comes to <strong>shipping</strong> and <strong>maintaining</strong> apps in production, there are many important aspects to consider:</p><ul><li><strong>Preparing for release</strong>: splash screens, flavors, environments, error reporting, analytics, force update, privacy, T&amp;Cs</li><li><strong>App Submissions</strong>: app store metadata &amp; screenshots, compliance, testing vs distribution tracks, dealing with rejections</li><li><strong>Release automation:</strong> CI workflows, environment variables, custom build steps, code signing, uploading to the stores</li><li><strong>Post-release</strong>: error monitoring, bug fixes, addressing user feedback, adding new features, over-the-air updates</li></ul><p>My latest course will help you get your app to the stores faster and with fewer headaches.</p><p>If you’re interested, you can learn more and enroll here. 👇</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/dark-tinted-icons-ios-18/</guid><title>Dark and Tinted Icons on iOS 18</title><description>How to enable dark and tinted icons on iOS 18 using the flutter_launcher_icons package.</description><link>https://codewithandrea.com/tips/dark-tinted-icons-ios-18/</link><pubDate>Fri, 11 Oct 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>iOS 18 supports dark and tinted icons.</p><p>To enable this in your Flutter app:</p><ul><li>Add one icon variant with transparency</li><li>Install and configure <code>flutter_launcher_icons</code> in your <code>pubspec.yaml</code></li><li>Run <code>dart run flutter_launcher_icons</code></li></ul><p>Then, run the app and join the dark side! 🌚</p><figure><picture><source srcset="images/197.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Dark and Tinted Icons on iOS 18" srcset="images/197.png 2x"/></picture></figure><hr><p>Following all the app icon guidelines on iOS and Android can be tricky.</p><p>To make life easier, my new course includes a whole module about launcher icons and splash screens.</p><p>If you're interested, check it out here:</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/flutterfire-config-multiple-flavors/</guid><title>FlutterFire Config with Multiple Flavors (Shell Script)</title><description>If your Flutter app uses multiple flavors, you can use the FlutterFire CLI to generate the config files for each flavor.</description><link>https://codewithandrea.com/tips/flutterfire-config-multiple-flavors/</link><pubDate>Thu, 10 Oct 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If your Flutter app uses multiple flavors, you can use the FlutterFire CLI to generate the config files for each flavor.</p><figure><picture><source srcset="images/196.webp 2x" type="image/webp"/><img class="bottom-40px" alt="FlutterFire Config with Multiple Flavors" srcset="images/196.png 2x"/></picture></figure><p>Here’s what each argument does:</p><ul><li><code>--project</code>: The Firebase project to use (note: pass the <strong>project ID</strong>, not the alias).</li><li><code>--out</code>: Output path for the Firebase config file.</li><li><code>--ios-bundle-id</code>: iOS app’s bundle ID. Find it in Xcode under <strong>Runner</strong> &gt; <strong>General</strong> &gt; <strong>Identity</strong> &gt; <strong>Bundle Identifier</strong>.</li><li><code>--ios-out</code>: Output path for the iOS <code>GoogleService-Info.plist</code>.</li><li><code>--android-package-name</code>: Android app’s package name (found as <code>applicationId</code> in <code>android/app/build.gradle</code>).</li><li><code>--android-out</code>: Output path for the Android <code>google-services.json</code>.</li></ul><h3><a id="pro-tip-create-a-shell-script" href="#pro-tip-create-a-shell-script">Pro Tip: Create a Shell Script</a></h3><p>To simplify the setup, here's a sample shell script that takes the flavor as an argument:</p><ul><li><a href="https://github.com/bizz84/flutter_ship_app/blob/main/flutterfire-config.sh">flutterfire-config.sh</a></li></ul><p>To use this script:</p><ul><li>Copy it to the root of your project</li><li>Update the <code>project</code>, <code>ios-bundle-id</code>, and <code>android-package-name</code> for your app.</li><li>Run it and follow the interactive prompts</li></ul><hr><p>This is only a small part of the Flutter app flavoring process.</p><p>For all the details, check my latest course. 👇</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/remote-config-github-gist/</guid><title>Remote Config via GitHub Gist</title><description>Here's how to remotely control the behaviour of your app by fetching some JSON from a GitHub gist.</description><link>https://codewithandrea.com/tips/remote-config-github-gist/</link><pubDate>Wed, 9 Oct 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>You can remotely control the behaviour of your app by fetching some JSON from a GitHub gist.</p><p>This is super useful when implementing:</p><ul><li>Force update ✅</li><li>Feature flags 🚩</li><li>A/B testing 🧪</li></ul><p>No Firebase or custom backend needed! 🙌</p><figure><picture><source srcset="images/195.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Remote Config via GitHub Gist" srcset="images/195.png 2x"/></picture></figure><p>I cover this in more detail in my latest course.</p><p>Check it out here and take advantage of my launch sale (currently 40% off!) 👇</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/copilot-generate-commit-messages/</guid><title>Generate Commit Messages with Copilot</title><description>If you're not too picky about how you write your commit messages, this can be a neat little time saver!</description><link>https://codewithandrea.com/tips/copilot-generate-commit-messages/</link><pubDate>Tue, 8 Oct 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know that <a href="https://github.com/features/copilot">GitHub Copilot</a> can generate commit messages for you?</p><p>If you're not too picky about how you write your commit messages, this can be a neat little time saver!</p><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Generate Commit Messages with Copilot" srcset="images/twitter-card.png 2x"/></picture></figure><p>Check this article for more Copilot tips and tricks:</p><ul><li><a href="https://codewithandrea.com/articles/github-copilot-tips-for-flutter-devs/">GitHub Copilot: Tips and Tricks for Flutter Devs</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/control-codegen-order/</guid><title>Control the Code Generation Order</title><description>If you're using multiple code generators that depend on each other, you can enforce the code generation order in your build.yaml file.</description><link>https://codewithandrea.com/tips/control-codegen-order/</link><pubDate>Mon, 7 Oct 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If you're using multiple code generators that depend on each other, build_runner may fail.</p><p>To fix this, you can enforce an explicit code generation order in your <code>build.yaml</code> file. 👇</p><figure><picture><source srcset="images/193.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Control the Code Generation Order" srcset="images/193.png 2x"/></picture></figure><p>For more techniques about effective codebase maintenance, read my ultimate guide about code generation:</p><ul><li><a href="https://codewithandrea.com/articles/dart-flutter-code-generation/">Code Generation with Dart &amp; Flutter: The Ultimate Guide</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/async-stream-initialization/</guid><title>Async Stream Initialization with async*</title><description>If you want to return a stream that depends on some asynchronous code, you can use async* and yield*</description><link>https://codewithandrea.com/tips/async-stream-initialization/</link><pubDate>Tue, 1 Oct 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>If you want to return a stream that depends on some asynchronous code, you can use <code>async*</code> and <code>yield*</code>.</p><p>This can be handy when you have an object that needs to be initialized asynchronously before it can start emitting events.</p><figure><picture><source srcset="images/192.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Async Stream Initialization with async*" srcset="images/192.png 2x"/></picture></figure><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/articles/flutter-in-app-review-prompt/</guid><title>How to Ask for In-App Reviews in Your Flutter App</title><description>The in_app_review package makes it easy to ask for reviews. And by using a data-driven approach, you can show the prompt at the right time.</description><link>https://codewithandrea.com/articles/flutter-in-app-review-prompt/</link><pubDate>Fri, 27 Sep 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>App reviews play an important role in the success of your apps. They not only provide essential feedback but also significantly affect the app's store performance:</p><ul><li>Positive reviews boost the app's ranking and drive more downloads.</li><li>Negative reviews can discourage new users from downloading.</li></ul><p>For example, here are the reviews from my <a href="https://fluttertips.dev/">Flutter Tips app</a>:</p><figure><picture><source srcset="images/example-reviews-flutter-tips.webp 2x" type="image/webp"/><img class="bottom-40px" alt="App Store Reviews for the Flutter Tips app" srcset="images/example-reviews-flutter-tips.png 2x"/></picture></figure><p>Glowing reviews don't just fall from the sky: <strong>you have to earn them</strong> by making a good app and make it <strong>as easy as possible</strong> for users to leave a review.</p><p>The best way to do this is by showing an in-app rating prompt at the right moment—when users are most engaged and satisfied. This could be after completing a task, leveling up in a game, or using a feature successfully.</p><p>Timing the prompt correctly increases the chances of getting positive reviews, boosting your app’s rating and visibility in the app store.</p><p>And by using the <a href="https://pub.dev/packages/in_app_review">in_app_review</a> package and a bit of extra code, you can do exactly that.</p><h2><a id="the-in_app_review-package" href="#the-in_app_review-package">The in_app_review package</a></h2><p>The <a href="https://pub.dev/packages/in_app_review">in_app_review</a> package allows you to ask for reviews in two different ways:</p><ol><li><strong>Call-to-action</strong>: by redirecting the user to the store with the <a href="https://pub.dev/documentation/in_app_review/latest/in_app_review/InAppReview/openStoreListing.html"><code>openStoreListing</code></a> API</li><li><strong>Programmatically</strong>: by triggering the in-app review prompt with the <a href="https://pub.dev/documentation/in_app_review/latest/in_app_review/InAppReview/requestReview.html"><code>requestReview</code></a> API</li></ol><figure><picture><source srcset="images/in-app-review-prompt.webp 2x" type="image/webp"/><img class="bottom-40px" alt="The in-app review prompt on iOS" srcset="images/in-app-review-prompt.png 2x"/></picture></figure><p>The second method is most effective, since it can be triggered when the user is most satisfied with the app. For example, I've programmed my <a href="https://fluttertips.dev/">Flutter Tips app</a> to show the prompt after users like 5 tips in the app:</p><figure><picture><source srcset="images/flutter-tips-in-app-rating-ios.webp 2x" type="image/webp"/><img class="bottom-40px" alt="When the user likes N tips, show the in-app rating prompt" srcset="images/flutter-tips-in-app-rating-ios.png 2x"/></picture></figure><h2><a id="what-we-will-cover" href="#what-we-will-cover">What we will cover</a></h2><p>In this article, I'll show you how to apply the same technique to your apps.</p><p>Here's what we will cover:</p><ul><li><a href="https://pub.dev/packages/in_app_review">in_app_review</a> installation and how to show the app-review prompt programmatically</li><li>How to avoid showing the prompt too early (by following the quotas on the app stores)</li><li>How to use analytics to ensure the prompt shows at the right time</li></ul><h3><a id="when-to-show-the-prompt?" href="#when-to-show-the-prompt?">When to show the prompt?</a></h3><p>Showing the prompt is the easy part.</p><p>The real challenge is deciding <strong>when</strong> to do it:</p><ul><li><strong>too early</strong>, and you will annoy your users (so many apps get this wrong!)</li><li><strong>too late</strong>, and hardly any users will even see it at all</li></ul><p>As we will see, by using <strong>analytics</strong> and a <strong>data-driven</strong> approach, we can show the prompt at the right time, for the most engaged users, thus maximising our chances of getting positive reviews.</p><p>Ready? Let's go! 🚀</p><blockquote><p>This article is not a step-by-step tutorial—it's more of a high-level guide showing how things fit together. If you want to go deeper, check my latest course about <a href="https://codewithandrea.com/courses/flutter-in-production/">Flutter in Production</a>.</p></blockquote><h2><a id="installation" href="#installation">Installation</a></h2><p>Installing the <a href="https://pub.dev/packages/in_app_review">in_app_review</a> package is easy enough:</p><pre><code><div class="highlight"><span></span>dart<span class="w"> </span>pub<span class="w"> </span>add<span class="w"> </span>in_app_review:2.0.9
flutter<span class="w"> </span>pub<span class="w"> </span>get
</div></code></pre><p>If you're using Riverpod, consider creating a separate provider for this:</p><pre><code><div class="highlight"><span></span><span class="c1">// in_app_review_provider.dart</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:in_app_review/in_app_review.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:riverpod_annotation/riverpod_annotation.dart&#39;</span><span class="p">;</span>

<span class="k">part</span><span class="w"> </span><span class="s1">&#39;in_app_review_provider.g.dart&#39;</span><span class="p">;</span>

<span class="nd">@riverpod</span>
<span class="n">InAppReview</span><span class="w"> </span><span class="n">inAppReview</span><span class="p">(</span><span class="n">InAppReviewRef</span><span class="w"> </span><span class="n">ref</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="n">InAppReview</span><span class="p">.</span><span class="n">instance</span><span class="p">;</span>
<span class="p">}</span>
</div></code></pre><blockquote><p>Since the code above relies on code generation, you'll need to run <code>dart run build_runner watch -d</code> to generate your provider. To learn more, read: <a href="https://codewithandrea.com/articles/flutter-riverpod-generator/">How to Auto-Generate your Providers with Flutter Riverpod Generator</a>.</p></blockquote><h2><a id="showing-the-in-app-review-prompt" href="#showing-the-in-app-review-prompt">Showing the in-app review prompt</a></h2><p>As discussed, we need to show the prompt at the right time.</p><p>This means that you need to choose the single, <strong>most important event</strong> that happens when the user is <strong>most satisfied</strong> with your app (e.g. completed a task, uses a feature successfully).</p><p>For my <a href="https://fluttertips.dev/">Flutter Tips app</a>, this is the "tip liked" event:</p><figure><picture><source srcset="images/flutter-tips-like-button.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Choosing an event that will trigger the in-app review prompt" srcset="images/flutter-tips-like-button.png 2x"/></picture></figure><p>The callback handler for this button looks something like this:</p><pre><code><div class="highlight"><span></span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">updateTipLiked</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">tipIndex</span><span class="p">,</span><span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="n">isLiked</span><span class="p">)</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">isLiked</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="c1">// * Show app review prompt based on some conditional logic</span>
<span class="w">    </span><span class="kd">await</span><span class="w"> </span><span class="n">inAppRatingService</span><span class="p">.</span><span class="n">requestReviewIfNeeded</span><span class="p">(</span>
<span class="w">          </span><span class="nl">userTotalLikesCount:</span><span class="w"> </span><span class="n">userTotalLikesCount</span>
<span class="w">        </span><span class="p">);</span>
<span class="w">    </span><span class="c1">// * Log the event with analytics</span>
<span class="w">    </span><span class="n">unawaited</span><span class="p">(</span><span class="n">analyticsFacade</span><span class="p">.</span><span class="n">trackTipLiked</span><span class="p">(</span>
<span class="w">      </span><span class="nl">tipIndex:</span><span class="w"> </span><span class="n">tipIndex</span><span class="p">,</span>
<span class="w">      </span><span class="nl">userTotalLikesCount:</span><span class="w"> </span><span class="n">userTotalLikesCount</span><span class="p">,</span>
<span class="w">    </span><span class="p">));</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</div></code></pre><p>Two things to note:</p><ul><li>The <code>updateTipLiked</code> method has two purposes: <strong>show the app review prompt</strong> and <strong>track the event</strong>.</li><li>Both the <code>requestReviewIfNeeded</code> and <code>trackTipLiked</code> methods take <code>userTotalLikesCount</code> as an argument.</li></ul><p>We'll get back to some of these details later. But for now, let's focus on the <code>InAppRatingService</code>.</p><blockquote><p>If you're not familiar with the <code>unawaited</code> function, read: <a href="https://codewithandrea.com/tips/futures-await-unawaited-ignore/">Futures: await vs unawaited vs ignore</a> and <a href="https://codewithandrea.com/tips/use-unawaited-analytics-calls/">Use unawaited for your analytics calls</a>.</p></blockquote><h2><a id="the-inappratingservice-class" href="#the-inappratingservice-class">The InAppRatingService class</a></h2><p>To handle all the in-app review logic, we can use a dedicated class that looks like this:</p><pre><code><div class="highlight"><span></span><span class="c1">/// Helper class used to show the in-app rating prompt when a certain number of</span>
<span class="c1">/// tips has been liked</span>
<span class="kd">class</span><span class="w"> </span><span class="nc">InAppRatingService</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">const</span><span class="w"> </span><span class="n">InAppRatingService</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">ref</span><span class="p">);</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="n">Ref</span><span class="w"> </span><span class="n">ref</span><span class="p">;</span>

<span class="w">  </span><span class="c1">// * Used to show the prompt</span>
<span class="w">  </span><span class="n">InAppReview</span><span class="w"> </span><span class="kd">get</span><span class="w"> </span><span class="n">_inAppReview</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">inAppReviewProvider</span><span class="p">);</span>

<span class="w">  </span><span class="c1">/// Requests a review if certain conditions are met</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">requestReviewIfNeeded</span><span class="p">({</span><span class="kd">required</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">userTotalLikesCount</span><span class="p">})</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="c1">// * Don&#39;t show rating prompt on web (not supported)</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">kIsWeb</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="k">return</span><span class="p">;</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="c1">// TODO: Only show prompt after a certain number of tips has been liked</span>
<span class="w">    </span><span class="c1">// * If we can show a review dialog</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="kd">await</span><span class="w"> </span><span class="n">_inAppReview</span><span class="p">.</span><span class="n">isAvailable</span><span class="p">())</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="c1">// * Request the review</span>
<span class="w">      </span><span class="kd">await</span><span class="w"> </span><span class="n">_inAppReview</span><span class="p">.</span><span class="n">requestReview</span><span class="p">();</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>

<span class="nd">@riverpod</span>
<span class="n">InAppRatingService</span><span class="w"> </span><span class="n">inAppRatingService</span><span class="p">(</span><span class="n">InAppRatingServiceRef</span><span class="w"> </span><span class="n">ref</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="n">InAppRatingService</span><span class="p">(</span><span class="n">ref</span><span class="p">);</span>
<span class="p">}</span>
</div></code></pre><p>Some notes:</p><ul><li>The class takes a <code>ref</code> argument, which can be used to read other providers (such as <code>inAppReviewProvider</code>).</li><li>The <code>requestReviewIfNeeded</code> method doesn't do anything if <code>kIsWeb</code> is true (we can only show the prompt on iOS and Android).</li><li>We check if the review dialog is available before showing it.</li></ul><blockquote><p>If your app doesn't use Riverpod, you can delete the provider and inject <code>InAppReview</code> as a constructor argument instead.</p></blockquote><h2><a id="does-this-code-work-as-intended?" href="#does-this-code-work-as-intended?">Does this code work as intended?</a></h2><p>If we added the <code>InAppRatingService</code> class above to Flutter Tips app and ran it on iOS, the rating prompt would appear <strong>as soon as we like a tip for the first time, and then again for each subsequent like</strong>:</p><figure><picture><source srcset="images/flutter-tips-in-app-rating-ios-too-early.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Showing the rating prompt too early will annoy the user" srcset="images/flutter-tips-in-app-rating-ios-too-early.png 2x"/></picture></figure><p>That's a bit too eager!</p><p>To avoid giving a bad first impression, we should add some logic that says "wait until the user liked N tips before showing the prompt".</p><h2><a id="showing-the-review-prompt-after-n-events" href="#showing-the-review-prompt-after-n-events">Showing the Review Prompt After N Events</a></h2><p>To accomplish what we want, we can use the <code>userTotalLikesCount</code> variable I mentioned before:</p><pre><code><div class="highlight"><span></span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">updateTipLiked</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">tipIndex</span><span class="p">,</span><span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="n">isLiked</span><span class="p">)</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">isLiked</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="c1">// * Show app review prompt based on some conditional logic</span>
<span class="w">    </span><span class="kd">await</span><span class="w"> </span><span class="n">inAppRatingService</span><span class="p">.</span><span class="n">requestReviewIfNeeded</span><span class="p">(</span>
<span class="w">          </span><span class="nl">userTotalLikesCount:</span><span class="w"> </span><span class="n">userTotalLikesCount</span>
<span class="w">        </span><span class="p">);</span>
<span class="w">    </span><span class="c1">// * Log the event with analytics</span>
<span class="w">    </span><span class="n">unawaited</span><span class="p">(</span><span class="n">analyticsFacade</span><span class="p">.</span><span class="n">trackTipLiked</span><span class="p">(</span>
<span class="w">      </span><span class="nl">tipIndex:</span><span class="w"> </span><span class="n">tipIndex</span><span class="p">,</span>
<span class="w">      </span><span class="nl">userTotalLikesCount:</span><span class="w"> </span><span class="n">userTotalLikesCount</span><span class="p">,</span>
<span class="w">    </span><span class="p">));</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</div></code></pre><blockquote><p>In your apps, this variable may have a different name. You could store it locally with Shared Preferences, and increment it every time the user completes a certain action.</p></blockquote><p>Here's an updated version of the <code>InAppReviewService</code>:</p><pre><code><div class="highlight"><span></span><span class="c1">/// Helper class used to show the in-app rating prompt when a certain number of</span>
<span class="c1">/// tips has been liked</span>
<span class="kd">class</span><span class="w"> </span><span class="nc">InAppRatingService</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">const</span><span class="w"> </span><span class="n">InAppRatingService</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">ref</span><span class="p">);</span>
<span class="w">  </span><span class="kd">final</span><span class="w"> </span><span class="n">Ref</span><span class="w"> </span><span class="n">ref</span><span class="p">;</span>

<span class="w">  </span><span class="c1">// * Used to show the prompt</span>
<span class="w">  </span><span class="n">InAppReview</span><span class="w"> </span><span class="kd">get</span><span class="w"> </span><span class="n">_inAppReview</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">inAppReviewProvider</span><span class="p">);</span>
<span class="w">  </span><span class="c1">// * Used to keep track of how many times we&#39;ve requested</span>
<span class="w">  </span><span class="c1">// * a review from the user</span>
<span class="w">  </span><span class="n">SharedPreferences</span><span class="w"> </span><span class="kd">get</span><span class="w"> </span><span class="n">_sharedPreferences</span><span class="w"> </span><span class="o">=&gt;</span>
<span class="w">      </span><span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">sharedPreferencesProvider</span><span class="p">).</span><span class="n">requireValue</span><span class="p">;</span>

<span class="w">  </span><span class="kd">static</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="n">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;in_app_rating_prompt_count&#39;</span><span class="p">;</span>

<span class="w">  </span><span class="kt">int</span><span class="w"> </span><span class="kd">get</span><span class="w"> </span><span class="n">_inAppReviewRequestCount</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">_sharedPreferences</span><span class="p">.</span><span class="n">getInt</span><span class="p">(</span><span class="n">key</span><span class="p">)</span><span class="w"> </span><span class="o">??</span><span class="w"> </span><span class="m">0</span><span class="p">;</span>

<span class="w">  </span><span class="c1">/// Requests a review if certain conditions are met</span>
<span class="w">  </span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span><span class="w"> </span><span class="n">requestReviewIfNeeded</span><span class="p">({</span><span class="kd">required</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">userTotalLikesCount</span><span class="p">})</span><span class="w"> </span><span class="kd">async</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="c1">// * Don&#39;t show rating prompt on web (not supported)</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">kIsWeb</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="k">return</span><span class="p">;</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="c1">// * If we can show a review dialog</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="kd">await</span><span class="w"> </span><span class="n">_inAppReview</span><span class="p">.</span><span class="n">isAvailable</span><span class="p">())</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="c1">// * Use an exponential backoff function:</span>
<span class="w">      </span><span class="c1">// * - 1st request after 5 liked tips</span>
<span class="w">      </span><span class="c1">// * - 2nd request after another 10 liked tips</span>
<span class="w">      </span><span class="c1">// * - 3rd request after another 20 liked tips</span>
<span class="w">      </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">completedTasksCount</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="m">5</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">_inAppReviewRequestCount</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="m">0</span><span class="w"> </span><span class="o">||</span>
<span class="w">          </span><span class="n">completedTasksCount</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="m">15</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">_inAppReviewRequestCount</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="o">||</span>
<span class="w">          </span><span class="n">completedTasksCount</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="m">35</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">_inAppReviewRequestCount</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="m">2</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="c1">// * Request the review</span>
<span class="w">        </span><span class="kd">await</span><span class="w"> </span><span class="n">_inAppReview</span><span class="p">.</span><span class="n">requestReview</span><span class="p">();</span>
<span class="w">        </span><span class="c1">// * Increment the count</span>
<span class="w">        </span><span class="kd">await</span><span class="w"> </span><span class="n">_sharedPreferences</span><span class="p">.</span><span class="n">setInt</span><span class="p">(</span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="n">_inAppReviewRequestCount</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="m">1</span><span class="p">);</span>
<span class="w">      </span><span class="p">}</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</div></code></pre><hr><h3><a id="note-about-storing-the-review-request-count-with-shared-preferences" href="#note-about-storing-the-review-request-count-with-shared-preferences">Note about storing the review request count with Shared Preferences</a></h3><p>The code above uses Shared Preferences to keep track of how many times we've requested an in-app review.</p><p>This ensures that the request count is persisted locally and can be retrieved even if we quit the app and restart it. However, if we delete and reinstall the app, or clear its storage, the count will be reset, but the app stores will still remember the quota (more on this below).</p><p>If you want to persist this kind of information across app reinstalls and your app supports authentication, consider storing the event count on your remote database, for each user.</p><hr><h3><a id="reviewing-the-conditional-logic" href="#reviewing-the-conditional-logic">Reviewing the Conditional Logic</a></h3><p>The most important code in the <code>InAppReviewService</code> class is this:</p><pre><code><div class="highlight"><span></span><span class="c1">// * Use an exponential backoff function:</span>
<span class="c1">// * - 1st request after 5 liked tips</span>
<span class="c1">// * - 2nd request after another 10 liked tips</span>
<span class="c1">// * - 3rd request after another 20 liked tips</span>
<span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">completedTasksCount</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="m">5</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">_inAppReviewRequestCount</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="m">0</span><span class="w"> </span><span class="o">||</span>
<span class="w">    </span><span class="n">completedTasksCount</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="m">15</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">_inAppReviewRequestCount</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="o">||</span>
<span class="w">    </span><span class="n">completedTasksCount</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="m">35</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">_inAppReviewRequestCount</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="m">2</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="c1">// * Request the review</span>
<span class="w">  </span><span class="kd">await</span><span class="w"> </span><span class="n">_inAppReview</span><span class="p">.</span><span class="n">requestReview</span><span class="p">();</span>
<span class="w">  </span><span class="c1">// * Increment the count</span>
<span class="w">  </span><span class="kd">await</span><span class="w"> </span><span class="n">_sharedPreferences</span><span class="p">.</span><span class="n">setInt</span><span class="p">(</span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="n">_inAppReviewRequestCount</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="m">1</span><span class="p">);</span>
<span class="p">}</span>
</div></code></pre><p>By adding the conditional logic above, we can ensure the review is requested after 5, 15, and 35 liked tips, respectively.</p><p>This is in line with the iOS App Store quotas, which state that:</p><blockquote><p>The system automatically limits the display of the prompt to <strong>three occurrences per app within a 365-day period</strong> (<a href="https://developer.apple.com/design/human-interface-guidelines/ratings-and-reviews#Best-practices">source</a>).</p></blockquote><p>But hold on! <code>5</code>, <code>15</code>, and <code>35</code> are arbitrary numbers! How have I decided they were optimal for my Flutter Tips app?</p><p>The answer lies in my analytics. 👇</p><h2><a id="making-data-driven-decisions-with-analytics" href="#making-data-driven-decisions-with-analytics">Making Data-Driven Decisions with Analytics</a></h2><p>Recall that in addition to requesting the in-app review, I'm also calling this method when a user likes a tip:</p><pre><code><div class="highlight"><span></span><span class="c1">// * Log the event with analytics</span>
<span class="n">unawaited</span><span class="p">(</span><span class="n">analyticsFacade</span><span class="p">.</span><span class="n">trackTipLiked</span><span class="p">(</span>
<span class="w">  </span><span class="nl">tipIndex:</span><span class="w"> </span><span class="n">tipIndex</span><span class="p">,</span>
<span class="w">  </span><span class="nl">userTotalLikesCount:</span><span class="w"> </span><span class="n">userTotalLikesCount</span><span class="p">,</span>
<span class="p">));</span>
</div></code></pre><p>Under the hood, this sends a custom event named "Tip Liked" to <a href="https://mixpanel.com/">Mixpanel</a>.</p><p>As a result, after publishing my app, I created a custom analytics report that looks like this:</p><figure><picture><source srcset="images/mixpanel-tip-liked-percentile.webp 2x" type="image/webp"/><img class="bottom-12px" alt="75th and 90th percentiles for the user total likes in the Flutter Tips App" srcset="images/mixpanel-tip-liked-percentile.png 2x"/></picture><figcaption><center><i>75th and 90th percentiles for the user total likes in the Flutter Tips App</i></center></figcaption></figure><p>This shows the <strong>maximum</strong> number of <strong>user total likes</strong> on the <strong>75th</strong> and <strong>90th</strong> percentile across all users.</p><p>Based on the given time period, I can see that, on average:</p><ul><li>users on the 75th percentile like 6.9 tips</li><li>users on the 90th percentile like 17.1 tips</li></ul><p>In plain English, this tells me <strong>how many tips are liked by the most engaged users</strong>. As a result, <code>5</code>, <code>15</code>, and <code>35</code> seemed to be reasonable thresholds for my app:</p><pre><code><div class="highlight"><span></span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">userTotalLikesCount</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="m">5</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">inAppRatingPromptCount</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="m">0</span><span class="w"> </span><span class="o">||</span>
<span class="w">    </span><span class="n">userTotalLikesCount</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="m">15</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">inAppRatingPromptCount</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="o">||</span>
<span class="w">    </span><span class="n">userTotalLikesCount</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="m">35</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">inAppRatingPromptCount</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="m">2</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="c1">// Request in-app review</span>
<span class="p">}</span>
</div></code></pre><p>But how should you approach this in <strong>your</strong> apps?</p><h2><a id="data-driven-in-app-review-prompt" href="#data-driven-in-app-review-prompt">Data-Driven In-App Review Prompt</a></h2><p>Here are some guidelines for showing the app review prompt and maximising the number of positive reviews in your apps:</p><ol><li>Decide which is the <strong>most important event</strong> that happens when the user is <strong>most satisfied</strong> with your app (e.g. completed a task, uses a feature successfully)</li><li>Add some analytics code to <strong>track that event</strong>, as well as <strong>how many times it has happened</strong> for that user (or device)</li><li>Launch the app on the stores</li><li>Create a custom report and measure the 75th and 90th percentile for that event (this is easy to do with Mixpanel)</li><li>Once you have enough data, add the in-app review logic and choose the appropriate thresholds, as shown above</li><li>Release a new version of your app</li></ol><p>This approach has served me well, and I plan to use it in all my apps. Feel free to borrow the code in this article and tweak it for your own needs.</p><blockquote><p><strong>Caveat</strong>: if users <strong>don't</strong> enjoy using your app, you're more likely to get negative reviews. So make sure you build a good app first, and <strong>then</strong> you can optimise for getting more reviews. I recommend collecting user feedback separately with the <a href="https://pub.dev/packages/feedback">feedback</a> package, which also offers a <a href="https://pub.dev/packages/feedback_sentry">Sentry plugin</a>.</p></blockquote><h2><a id="conclusion" href="#conclusion">Conclusion</a></h2><p>The <a href="https://pub.dev/packages/in_app_review">in_app_review</a> package makes it super easy to show an in-app review prompt in your app.</p><p>But the real challenge is deciding <strong>when</strong> to show the prompt:</p><ul><li><strong>too early</strong>, and you will annoy your users</li><li><strong>too late</strong>, and hardly any users will even see it at all</li></ul><p>As we've seen, by using <strong>analytics</strong> and a <strong>data-driven</strong> approach, we can ensure the prompt is shown at the right time for the most engaged users, thus maximising our chances of getting positive reviews.</p><p>And with this, I wish you the best in launching your apps! ⭐️</p><h2><a id="new-course-flutter-in-production" href="#new-course-flutter-in-production">New course: Flutter in Production</a></h2><p>In-app reviews play an important role in the success of your apps in the stores.</p><p>But when it comes to <strong>shipping</strong> and <strong>monitoring</strong> apps in production, there are many more things to consider:</p><ul><li><strong>Preparing for release</strong>: splash screens, flavors, environments, error reporting, analytics, force update, privacy, T&amp;Cs</li><li><strong>App Submissions</strong>: app store metadata &amp; screenshots, compliance, testing vs distribution tracks, dealing with rejections</li><li><strong>Release automation:</strong> CI workflows, environment variables, custom build steps, code signing, uploading to the stores</li><li><strong>Post-release</strong>: error monitoring, bug fixes, addressing user feedback, adding new features, over-the-air updates</li></ul><p>My latest course will help you get your app to the stores faster and with fewer headaches.</p><p>If you’re interested, you can learn more and enroll here. 👇</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/timing-in-app-review-prompt/</guid><title>Timing the In-App Review Prompt</title><description>To avoid prompting users too early, track your desired event and only ask for a review after it is triggered N times.</description><link>https://codewithandrea.com/tips/timing-in-app-review-prompt/</link><pubDate>Thu, 26 Sep 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know?</p><p>The <a href="https://pub.dev/packages/in_app_review">in_app_review</a> package makes it easy to ask for reviews in your app. ⭐️</p><p>But timing is key and the App Store will only let you show up to 3 prompts per year.</p><p>To avoid prompting users too early, track your desired event and only ask for a review after it is triggered N times.</p><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Timing the In-App Review Prompt" srcset="images/twitter-card.png 2x"/></picture></figure><p>The code above only shows the main idea.</p><p>To learn more about in-app reviews, read:</p><ul><li><a href="https://codewithandrea.com/articles/flutter-in-app-review-prompt/">How to Ask for In-App Reviews in Your Flutter App</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/multiple-xcode-versions/</guid><title>Working with Multiple Xcode Versions</title><description>How to download multiple releases from xcodereleases.com, and switch between them with the xcode-select CLI.</description><link>https://codewithandrea.com/tips/multiple-xcode-versions/</link><pubDate>Tue, 24 Sep 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Did you know that you can install and use multiple Xcode versions?</p><p>Here's how:</p><ul><li>Head to <a href="https://xcodereleases.com">xcodereleases.com</a> and download your desired release</li><li>Extract and rename it</li><li>Drag it to the <code>/Applications</code> folder</li></ul><p>To switch between them, use the <code>xcode-select</code> CLI. 👇</p><figure><picture><source srcset="images/twitter-card.webp 2x" type="image/webp"/><img class="bottom-40px" alt="Working with Multiple Xcode Versions" srcset="images/twitter-card.png 2x"/></picture></figure><p>This can be useful if the latest release is <strong>cough-cough</strong> buggy, and you want to keep the old one around.</p><p>Or you just want to have multiple versions installed and easily switch between them.</p><hr><p>Note that to use the <code>xcode-select</code> CLI, you will need to install the Xcode command line tools.</p><p>You can get them from here (sign-in required):</p><ul><li><a href="https://developer.apple.com/download/all/?q=Command%20Line%20Tools%20for%20Xcode">Command Line Tools for Xcode</a></li></ul><h3><a id="bonus-xcodes-app" href="#bonus-xcodes-app">Bonus: Xcodes app</a></h3><p>If you want to manage multiple Xcode versions with a mouse click or through a CLI, you can download the <code>Xcodes</code> app:</p><ul><li><a href="https://www.xcodes.app/">Xcodes app</a></li></ul><p>Happy coding!</p>]]></content:encoded></item><item><guid isPermaLink="true">https://codewithandrea.com/tips/navigator-observer/</guid><title>Adding a Navigator Observer</title><description>By implementing a NavigatorObserver, you can track page views or add navigation breadcrumbs to your error logs.</description><link>https://codewithandrea.com/tips/navigator-observer/</link><pubDate>Mon, 23 Sep 2024 02:00:00 +0200</pubDate><content:encoded><![CDATA[<p>Ever wanted to track page views or add navigation breadcrumbs to your error logs?</p><p>This can be done by implementing a <code>NavigatorObserver</code>. ✅</p><p>Here's some sample code showing how to implement this. 👇</p><figure><picture><img class="bottom-40px" alt="Adding a Navigator Observer" srcset="images/189.1.png 2x"/></picture></figure><pre><code><div class="highlight"><span></span><span class="k">import</span><span class="w"> </span><span class="s1">&#39;package:flutter/material.dart&#39;</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">&#39;dart:developer&#39;</span><span class="p">;</span>

<span class="kd">class</span><span class="w"> </span><span class="nc">LoggerNavigatorObserver</span><span class="w"> </span><span class="kd">extends</span><span class="w"> </span><span class="n">NavigatorObserver</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="kt">void</span><span class="w"> </span><span class="n">didPush</span><span class="p">(</span><span class="n">Route</span><span class="o">&lt;</span><span class="kt">dynamic</span><span class="o">&gt;</span><span class="w"> </span><span class="n">route</span><span class="p">,</span><span class="w"> </span><span class="n">Route</span><span class="o">&lt;</span><span class="kt">dynamic</span><span class="o">&gt;?</span><span class="w"> </span><span class="n">previousRoute</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">_logNavigation</span><span class="p">(</span><span class="n">route</span><span class="p">.</span><span class="n">settings</span><span class="p">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;push&#39;</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="kt">void</span><span class="w"> </span><span class="n">didPop</span><span class="p">(</span><span class="n">Route</span><span class="o">&lt;</span><span class="kt">dynamic</span><span class="o">&gt;</span><span class="w"> </span><span class="n">route</span><span class="p">,</span><span class="w"> </span><span class="n">Route</span><span class="o">&lt;</span><span class="kt">dynamic</span><span class="o">&gt;?</span><span class="w"> </span><span class="n">previousRoute</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">_logNavigation</span><span class="p">(</span><span class="n">route</span><span class="p">.</span><span class="n">settings</span><span class="p">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;pop&#39;</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="nd">@override</span>
<span class="w">  </span><span class="kt">void</span><span class="w"> </span><span class="n">didReplace</span><span class="p">({</span><span class="n">Route</span><span class="o">&lt;</span><span class="kt">dynamic</span><span class="o">&gt;?</span><span class="w"> </span><span class="n">newRoute</span><span class="p">,</span><span class="w"> </span><span class="n">Route</span><span class="o">&lt;</span><span class="kt">dynamic</span><span class="o">&gt;?</span><span class="w"> </span><span class="n">oldRoute</span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">newRoute</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="n">_logNavigation</span><span class="p">(</span><span class="n">newRoute</span><span class="p">.</span><span class="n">settings</span><span class="p">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;replace&#39;</span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">}</span>

<span class="w">  </span><span class="kt">void</span><span class="w"> </span><span class="n">_logNavigation</span><span class="p">(</span><span class="kt">String</span><span class="o">?</span><span class="w"> </span><span class="n">routeName</span><span class="p">,</span><span class="w"> </span><span class="kt">String</span><span class="w"> </span><span class="n">action</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">routeName</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="n">log</span><span class="p">(</span><span class="s2">&quot;Screen </span><span class="si">$</span><span class="n">action</span><span class="s2">: </span><span class="si">$</span><span class="n">routeName</span><span class="s2">&quot;</span><span class="p">,</span><span class="w"> </span><span class="nl">name:</span><span class="w"> </span><span class="s1">&#39;Navigation&#39;</span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</div></code></pre><p>To use the navigator observer, simply add it to the <code>MaterialApp</code> widget.</p><p>As a result, navigation logs will show in the console.</p><figure><picture><img class="bottom-40px" alt="Adding a Navigator Observer" srcset="images/189.2.png 2x"/></picture></figure><hr><p>A couple of extra tips:</p><ul><li>some packages already offer a navigator observer (e.g. <code>SentryNavigatorObserver</code>), so you may not need to implement your own</li><li>whatever you do, DON'T track page views on the <code>build</code> method (this can be called many times when widgets rebuild, and is out of your control)</li></ul><p>I will cover analytics and error monitoring in detail in my upcoming course.</p><p>If you want to ship your apps with confidence, check it out and join the waitlist. 👇</p>]]></content:encoded></item></channel></rss>