<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Yong Shean's dev notes]]></title><description><![CDATA[Yong Shean's dev notes]]></description><link>https://yshean.com</link><generator>RSS for Node</generator><lastBuildDate>Mon, 13 Apr 2026 07:44:03 GMT</lastBuildDate><atom:link href="https://yshean.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Riverpod from the Perspective of a Long-Term Bloc User]]></title><description><![CDATA[I've been using Bloc for years. It was my go-to state management solution, and I was comfortable with it. Events go in, states come out. Simple, predictable, testable. So when people kept telling me to try Riverpod, I resisted. Why fix what isn't bro...]]></description><link>https://yshean.com/riverpod-from-the-perspective-of-a-long-term-bloc-user</link><guid isPermaLink="true">https://yshean.com/riverpod-from-the-perspective-of-a-long-term-bloc-user</guid><category><![CDATA[Flutter]]></category><category><![CDATA[BLoC]]></category><category><![CDATA[Riverpod]]></category><category><![CDATA[State Management ]]></category><category><![CDATA[Mobile Development]]></category><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Sun, 02 Nov 2025 09:49:07 GMT</pubDate><content:encoded><![CDATA[<p>I've been using Bloc for years. It was my go-to state management solution, and I was comfortable with it. Events go in, states come out. Simple, predictable, testable. So when people kept telling me to try Riverpod, I resisted. Why fix what isn't broken?</p>
<p>But after finally trying it out on a recent project, I learned that Riverpod isn't just "another state management solution", it's a completely different way of thinking. Here's what I found as a Bloc user.</p>
<h2 id="heading-the-mental-model-shift-from-events-to-reactivity">The Mental Model Shift: From Events to Reactivity</h2>
<p>This was the biggest adjustment for me.</p>
<p><strong>In Bloc, you think in terms of user actions:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// User taps button → dispatch event → state changes</span>
context.read&lt;CounterBloc&gt;().add(CounterIncremented());
</code></pre>
<p><strong>In Riverpod, you think in terms of reactive values:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// Just change the value</span>
ref.read(counterProvider.notifier).increment();
</code></pre>
<p>At first, this felt wrong. Where are my events? How do I know what happened? But then it clicked: <strong>Riverpod removes the middleman</strong>. You don't need an event class for every single action. You just call methods on notifiers.</p>
<h3 id="heading-what-i-thought-vs-what-i-found">What I Thought vs. What I Found</h3>
<p><strong>I thought:</strong> "Without events, I'll lose clarity about what's happening in my app."</p>
<p><strong>I found:</strong> Most events in Bloc are just ceremony. Do you really need a <code>CounterIncremented</code> event class? Or a <code>LoadingStarted</code> event? Riverpod lets you skip that boilerplate.</p>
<p><strong>But Bloc has this too:</strong> If you use Cubit instead of Bloc, you get the same direct state modification without events. So this isn't really Riverpod vs Bloc, it's more about choosing the right tool. Bloc gives you the choice between explicit events (Bloc) and direct methods (Cubit). Riverpod only offers the Cubit-style approach.</p>
<h2 id="heading-architecture-similar-to-cubits">Architecture: Similar to Cubits</h2>
<p>If you've used Cubits (Bloc without events), Riverpod will feel familiar.</p>
<p><strong>Here's a typical Bloc setup:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// Bloc</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CounterBloc</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Bloc</span>&lt;<span class="hljs-title">CounterEvent</span>, <span class="hljs-title">int</span>&gt; </span>{
  CounterBloc() : <span class="hljs-keyword">super</span>(<span class="hljs-number">0</span>) {
    <span class="hljs-keyword">on</span>&lt;CounterIncremented&gt;((event, emit) =&gt; emit(state + <span class="hljs-number">1</span>));
    <span class="hljs-keyword">on</span>&lt;CounterDecremented&gt;((event, emit) =&gt; emit(state - <span class="hljs-number">1</span>));
  }
}

<span class="hljs-comment">// Somewhere in your widget tree</span>
BlocProvider(
  create: (context) =&gt; CounterBloc(),
  child: MyApp(),
)
</code></pre>
<p><strong>Here's the Riverpod equivalent (as of version 3.0):</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CounterNotifier</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Notifier</span>&lt;<span class="hljs-title">int</span>&gt; </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">int</span> build() =&gt; <span class="hljs-number">0</span>;

  <span class="hljs-keyword">void</span> increment() =&gt; state++;
  <span class="hljs-keyword">void</span> decrement() =&gt; state--;
}

<span class="hljs-keyword">final</span> counterProvider = NotifierProvider&lt;CounterNotifier, <span class="hljs-built_in">int</span>&gt;(
  CounterNotifier.<span class="hljs-keyword">new</span>,
);

<span class="hljs-comment">// No provider widget needed in your tree</span>
</code></pre>
<p><strong>What I noticed:</strong> No more provider widgets cluttering my widget tree. Providers are global by default. You can still override them for testing.</p>
<p><strong>Bloc's approach:</strong> Bloc requires provider widgets in your tree, which some see as clutter. However, this has benefits, it makes the dependency tree explicit and visible. You can see exactly where each Bloc is provided. The <code>BlocProvider</code> also handles disposal automatically when the widget is removed. With Riverpod, providers are global, which is cleaner but less explicit. You can achieve scoped providers in Riverpod using <code>ProviderScope</code> overrides, but it's less common.</p>
<p><strong>Important note about Riverpod 3.0:</strong> As of Riverpod 3.0, <code>StateNotifierProvider</code> and <code>StateProvider</code> are now considered "legacy" providers. They still work but require importing from <code>package:flutter_riverpod/legacy.dart</code>. The recommended approach is <code>NotifierProvider</code> and <code>AsyncNotifierProvider</code>, which I'm showing in these examples.</p>
<h2 id="heading-dependencies-different-approach">Dependencies: Different Approach</h2>
<p>Here's where Riverpod differs significantly. In Bloc, injecting dependencies is manual and requires you to pass them through the widget tree:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserBloc</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Bloc</span>&lt;<span class="hljs-title">UserEvent</span>, <span class="hljs-title">UserState</span>&gt; </span>{
  <span class="hljs-keyword">final</span> UserRepository repository;

  UserBloc({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.repository}) : <span class="hljs-keyword">super</span>(UserInitial());

  <span class="hljs-comment">// ... use repository</span>
}

<span class="hljs-comment">// In your app, using RepositoryProvider (the official Bloc way)</span>
RepositoryProvider(
  create: (context) =&gt; UserRepository(),
  child: BlocProvider(
    create: (context) =&gt; UserBloc(
      repository: context.read&lt;UserRepository&gt;(),
    ),
    child: MyWidget(),
  ),
)
</code></pre>
<p>In Riverpod, dependencies are automatic:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> userRepositoryProvider = Provider((ref) =&gt; UserRepository());

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserNotifier</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Notifier</span>&lt;<span class="hljs-title">AsyncValue</span>&lt;<span class="hljs-title">User</span>&gt;&gt; </span>{
  <span class="hljs-meta">@override</span>
  AsyncValue&lt;User&gt; build() {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">const</span> AsyncValue.loading();
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; loadUser() <span class="hljs-keyword">async</span> {
    <span class="hljs-comment">// Just read the dependency you need</span>
    <span class="hljs-keyword">final</span> repository = ref.read(userRepositoryProvider);
    state = <span class="hljs-keyword">await</span> AsyncValue.guard(() =&gt; repository.getUser());
  }
}
</code></pre>
<p><strong>What I noticed:</strong> Dependencies are tracked automatically. If <code>userRepositoryProvider</code> rebuilds, any provider that depends on it rebuilds too.</p>
<p><strong>Bloc's approach:</strong> Bloc provides <code>RepositoryProvider</code> (part of the <code>flutter_bloc</code> package) to inject dependencies through the widget tree:</p>
<pre><code class="lang-dart">MultiRepositoryProvider(
  providers: [
    RepositoryProvider(create: (context) =&gt; UserRepository()),
    RepositoryProvider(create: (context) =&gt; AuthRepository()),
  ],
  child: MultiBlocProvider(
    providers: [
      BlocProvider(
        create: (context) =&gt; UserBloc(
          repository: context.read&lt;UserRepository&gt;(),
        ),
      ),
      BlocProvider(
        create: (context) =&gt; AuthBloc(
          repository: context.read&lt;AuthRepository&gt;(),
        ),
      ),
    ],
    child: MyApp(),
  ),
)
</code></pre>
<p>This works and keeps everything explicit in the widget tree. However, there are some limitations:</p>
<ol>
<li><p><strong>No automatic reactivity:</strong> If a repository needs to be recreated, Blocs that depend on it won't automatically rebuild, you'd need to manually handle this</p>
</li>
<li><p><strong>More boilerplate:</strong> You need to wire up RepositoryProvider → BlocProvider → Widget for each dependency</p>
</li>
<li><p><strong>Tree-based access only:</strong> Repositories are only available below their RepositoryProvider in the widget tree</p>
</li>
</ol>
<p><strong>Alternative: Service Locators:</strong> Some Bloc users prefer service locator patterns like <code>get_it</code>:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Setup once at app startup</span>
<span class="hljs-keyword">final</span> getIt = GetIt.instance;
getIt.registerSingleton&lt;UserRepository&gt;(UserRepository());

<span class="hljs-comment">// Use anywhere</span>
BlocProvider(
  create: (context) =&gt; UserBloc(
    repository: getIt&lt;UserRepository&gt;(),
  ),
)
</code></pre>
<p>This avoids nesting providers but loses the widget tree context and automatic disposal. It's also a different dependency injection philosophy (service locator vs dependency injection container).</p>
<p><strong>Riverpod's difference:</strong> Riverpod's approach is built-in and reactive. Dependencies automatically track changes, rebuild dependents when needed, and work anywhere without widget tree constraints. You don't need extra packages or decide between tree-based vs global approaches, it just works.</p>
<h2 id="heading-async-state-asyncvalue-reduces-boilerplate">Async State: AsyncValue Reduces Boilerplate</h2>
<p>In Bloc, handling loading/error/success states means creating state classes:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserState</span> </span>{}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserInitial</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">UserState</span> </span>{}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserLoading</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">UserState</span> </span>{}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserLoaded</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">UserState</span> </span>{
  <span class="hljs-keyword">final</span> User user;
  UserLoaded(<span class="hljs-keyword">this</span>.user);
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserError</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">UserState</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> message;
  UserError(<span class="hljs-keyword">this</span>.message);
}

<span class="hljs-comment">// In your widget</span>
BlocBuilder&lt;UserBloc, UserState&gt;(
  builder: (context, state) {
    <span class="hljs-keyword">if</span> (state <span class="hljs-keyword">is</span> UserLoading) <span class="hljs-keyword">return</span> CircularProgressIndicator();
    <span class="hljs-keyword">if</span> (state <span class="hljs-keyword">is</span> UserError) <span class="hljs-keyword">return</span> Text(state.message);
    <span class="hljs-keyword">if</span> (state <span class="hljs-keyword">is</span> UserLoaded) <span class="hljs-keyword">return</span> Text(state.user.name);
    <span class="hljs-keyword">return</span> SizedBox();
  },
)
</code></pre>
<p>In Riverpod, <code>AsyncValue</code> does this for you:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> userProvider = FutureProvider((ref) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> ref.read(userRepositoryProvider).getUser();
});

<span class="hljs-comment">// In your widget</span>
ref.watch(userProvider).when(
  loading: () =&gt; CircularProgressIndicator(),
  error: (error, stack) =&gt; Text(<span class="hljs-string">'Error: <span class="hljs-subst">$error</span>'</span>),
  data: (user) =&gt; Text(user.name),
);
</code></pre>
<p><strong>What I noticed:</strong> <code>AsyncValue</code> eliminates the boilerplate. No more creating four state classes for every async operation.</p>
<p><strong>Workaround with Bloc:</strong> You can reduce boilerplate in Bloc using generic base classes or packages. Some teams use patterns like this:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Generic async state</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AsyncState</span>&lt;<span class="hljs-title">T</span>&gt; </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool</span> isLoading;
  <span class="hljs-keyword">final</span> T? data;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String?</span> error;

  AsyncState({<span class="hljs-keyword">this</span>.isLoading = <span class="hljs-keyword">false</span>, <span class="hljs-keyword">this</span>.data, <span class="hljs-keyword">this</span>.error});

  AsyncState&lt;T&gt; copyWith({<span class="hljs-built_in">bool?</span> isLoading, T? data, <span class="hljs-built_in">String?</span> error}) {
    <span class="hljs-keyword">return</span> AsyncState(
      isLoading: isLoading ?? <span class="hljs-keyword">this</span>.isLoading,
      data: data ?? <span class="hljs-keyword">this</span>.data,
      error: error ?? <span class="hljs-keyword">this</span>.error,
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserBloc</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Bloc</span>&lt;<span class="hljs-title">UserEvent</span>, <span class="hljs-title">AsyncState</span>&lt;<span class="hljs-title">User</span>&gt;&gt; </span>{
  UserBloc() : <span class="hljs-keyword">super</span>(AsyncState(isLoading: <span class="hljs-keyword">true</span>));
  <span class="hljs-comment">// ...</span>
}
</code></pre>
<h2 id="heading-testing-different-approach">Testing: Different Approach</h2>
<p>In Bloc, testing meant mocking and pumping events:</p>
<pre><code class="lang-dart">blocTest&lt;CounterBloc, <span class="hljs-built_in">int</span>&gt;(
  <span class="hljs-string">'increments counter'</span>,
  build: () =&gt; CounterBloc(),
  act: (bloc) =&gt; bloc.add(CounterIncremented()),
  expect: () =&gt; [<span class="hljs-number">1</span>],
);
</code></pre>
<p>In Riverpod (3.0+), you use <code>ProviderContainer.test()</code>:</p>
<pre><code class="lang-dart">test(<span class="hljs-string">'increments counter'</span>, () {
  <span class="hljs-comment">// ProviderContainer.test() automatically disposes after the test</span>
  <span class="hljs-keyword">final</span> container = ProviderContainer.test();

  expect(container.read(counterProvider), <span class="hljs-number">0</span>);
  container.read(counterProvider.notifier).increment();
  expect(container.read(counterProvider), <span class="hljs-number">1</span>);

  <span class="hljs-comment">// No need to manually dispose - test() handles it</span>
});
</code></pre>
<p>For dependencies:</p>
<pre><code class="lang-dart">test(<span class="hljs-string">'loads user'</span>, () <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> container = ProviderContainer.test(
    overrides: [
      userRepositoryProvider.overrideWithValue(MockUserRepository()),
    ],
  );

  <span class="hljs-keyword">final</span> user = <span class="hljs-keyword">await</span> container.read(userProvider.future);
  expect(user.name, <span class="hljs-string">'Test User'</span>);
});
</code></pre>
<p><strong>What I noticed:</strong> Testing is more direct. You're testing the actual provider logic, not orchestrating events and states. The new <code>ProviderContainer.test()</code> utility in 3.0 handles disposal automatically.</p>
<p><strong>Bloc's advantage:</strong> The <code>bloc_test</code> package makes testing Blocs straightforward. You get to test the full event → state transformation pipeline, and you can assert on state streams. This is actually more comprehensive than basic Riverpod tests:</p>
<pre><code class="lang-dart">blocTest&lt;UserBloc, UserState&gt;(
  <span class="hljs-string">'loads user successfully'</span>,
  build: () =&gt; UserBloc(repository: mockRepository),
  act: (bloc) =&gt; bloc.add(UserLoadRequested()),
  expect: () =&gt; [
    UserLoading(),
    UserLoaded(testUser),
  ],
  verify: (_) {
    verify(() =&gt; mockRepository.getUser()).called(<span class="hljs-number">1</span>);
  },
);
</code></pre>
<p>The <code>bloc_test</code> package's <code>expect</code> parameter lets you assert on the entire sequence of states, which catches more bugs than testing the final state alone. In Riverpod, you'd need to manually listen to state changes to get similar verification. However, Riverpod's override system is simpler for dependency injection in tests, no need for constructor injection, just override the provider directly.</p>
<h2 id="heading-the-learning-curve-it-takes-time">The Learning Curve: It Takes Time</h2>
<p>Riverpod has a steeper learning curve. Here's what tripped me up:</p>
<h3 id="heading-1-too-many-provider-types">1. Too Many Provider Types</h3>
<p>Even with Riverpod 3.0 moving legacy providers out, you still have: <code>Provider</code>, <code>FutureProvider</code>, <code>StreamProvider</code>, <code>NotifierProvider</code>, <code>AsyncNotifierProvider</code>. It's confusing at first.</p>
<p><strong>Bloc's simplicity:</strong> Bloc has two types: Bloc and Cubit. That's it. The choice is clear: use Bloc if you want events, Cubit if you don't. This is easier when onboarding new developers.</p>
<h3 id="heading-2-refwatchhttprefwatch-vs-refreadhttprefread-vs-reflisten">2. <a target="_blank" href="http://ref.watch"><code>ref.watch</code></a> vs <a target="_blank" href="http://ref.read"><code>ref.read</code></a> vs <code>ref.listen</code></h3>
<ul>
<li><p><a target="_blank" href="http://ref.watch"><code>ref.watch</code></a>: Rebuilds when value changes (use in <code>build</code>)</p>
</li>
<li><p><a target="_blank" href="http://ref.read"><code>ref.read</code></a>: One-time read (use in callbacks)</p>
</li>
<li><p><code>ref.listen</code>: Side effects (use for navigation, snackbars)</p>
</li>
</ul>
<p><strong>Bloc's equivalent:</strong> Bloc has similar concepts but with different names:</p>
<ul>
<li><p><code>BlocBuilder</code>: Like <a target="_blank" href="http://ref.watch"><code>ref.watch</code></a>, rebuilds on state changes</p>
</li>
<li><p><a target="_blank" href="http://context.read"><code>context.read</code></a><code>()</code>: Like <a target="_blank" href="http://ref.read"><code>ref.read</code></a>, one-time access</p>
</li>
<li><p><code>BlocListener</code>: Like <code>ref.listen</code>, for side effects</p>
</li>
</ul>
<p>So the concepts exist in both, just with different APIs.</p>
<h3 id="heading-3-code-generation-confusion">3. Code Generation Confusion</h3>
<p>Riverpod has two styles: runtime and code generation. The docs push code generation with the <code>@riverpod</code> annotation, but it adds complexity. I started with runtime and was fine. But it may be interesting for reducing the boilerplate further.</p>
<p><strong>Bloc doesn't have this problem:</strong> There's no code generation debate with Bloc. You write your code, and it runs. This is simpler. Riverpod's code generation offers benefits (type safety, less boilerplate) but adds build complexity with <code>build_runner</code>. With Bloc, what you write is what runs, no build step to configure.</p>
<h2 id="heading-when-to-use-bloc-vs-riverpod">When to Use Bloc vs Riverpod</h2>
<p>After using both, here's my take:</p>
<p><strong>Use Bloc when:</strong></p>
<ul>
<li><p>You want explicit events for debugging (event logs are useful)</p>
</li>
<li><p>Your team is already comfortable with Bloc</p>
</li>
<li><p>You prefer simpler tooling (no code generation decisions)</p>
</li>
<li><p>You want to see the full history of state changes in tests</p>
</li>
<li><p>You like having dependencies explicit in the widget tree</p>
</li>
</ul>
<p><strong>Use Riverpod when:</strong></p>
<ul>
<li><p>You want less boilerplate (though Cubit narrows this gap)</p>
</li>
<li><p>You need automatic dependency management and reactivity</p>
</li>
<li><p>You want better performance with granular rebuilds (though Bloc can achieve this with proper structure)</p>
</li>
<li><p>You prefer global providers over widget-tree-based injection</p>
</li>
<li><p>You want dependencies to automatically trigger rebuilds when they change</p>
</li>
</ul>
<h2 id="heading-what-bloc-does-better">What Bloc Does Better</h2>
<p>Bloc has some advantages:</p>
<ol>
<li><p><strong>Event logs:</strong> Seeing every event that flows through your app is useful for debugging. Riverpod has no equivalent, you'd need to add logging manually to each notifier method.</p>
</li>
<li><p><strong>Explicit transitions:</strong> <code>on&lt;Event&gt;</code> makes it clear what triggers what. In Riverpod, methods can be called from anywhere, making it harder to trace data flow.</p>
</li>
<li><p><strong>BlocObserver:</strong> One place to log all events and state changes across your entire app. Riverpod has <code>ProviderObserver</code>, but it's less informative, it only tells you which providers changed, not why or what methods were called.</p>
</li>
<li><p><strong>Mature ecosystem:</strong> More tutorials, more Stack Overflow answers, more proven patterns in production apps.</p>
</li>
<li><p><strong>Testing streams:</strong> <code>bloc_test</code> can verify the entire sequence of state emissions, catching race conditions and intermediate states. Riverpod tests typically only check final states.</p>
</li>
<li><p><strong>Simpler mental model:</strong> Two choices (Bloc or Cubit), clear documentation, no code generation decisions.</p>
</li>
<li><p><strong>Explicit dependency tree:</strong> With <code>RepositoryProvider</code> and <code>BlocProvider</code>, you can see exactly where dependencies are provided in the widget tree. This makes the app structure more visible.</p>
</li>
</ol>
<h2 id="heading-what-riverpod-does-better">What Riverpod Does Better</h2>
<ol>
<li><p><strong>Less code:</strong> Even compared to Cubit, Riverpod typically requires fewer lines. No provider widgets, no disposal logic.</p>
</li>
<li><p><strong>No provider widgets:</strong> My widget tree is cleaner. Though Bloc's explicit tree does make dependencies more visible.</p>
</li>
<li><p><strong>Automatic dependencies:</strong> No manual injection needed. Bloc's <code>RepositoryProvider</code> works but requires more setup and nesting.</p>
</li>
<li><p><strong>AsyncValue:</strong> Built-in loading/error/data handling that's more convenient than Bloc's patterns. Though Bloc can build similar abstractions.</p>
</li>
<li><p><strong>Flexibility:</strong> Easy to split and combine providers. You can compose complex state from simple providers.</p>
</li>
<li><p><strong>Reactive dependencies:</strong> When a dependency changes, dependent providers automatically rebuild. In Bloc, you'd need to manually recreate the Bloc or emit new states.</p>
</li>
<li><p><strong>Performance by default:</strong> Granular rebuilds out of the box. Bloc can achieve this with <code>BlocSelector</code> but requires more manual optimization.</p>
</li>
<li><p><strong>Simpler dependency testing:</strong> Just override providers in tests. No need to mock constructors or pass dependencies through multiple layers.</p>
</li>
</ol>
<h2 id="heading-my-migration-strategy">My Migration Strategy</h2>
<p>If you're considering switching, here's what worked for me:</p>
<ol>
<li><p><strong>Start small:</strong> Convert one feature, not the whole app</p>
</li>
<li><p><strong>Learn NotifierProvider first:</strong> It's closest to Cubit and is the recommended approach in Riverpod 3.0</p>
</li>
<li><p><strong>Don't use code generation initially:</strong> Add it later if needed</p>
</li>
<li><p><strong>Read the docs:</strong> Riverpod's docs are dense but thorough</p>
</li>
<li><p><strong>Accept you'll lose some things:</strong> Event logs and BlocObserver are useful. Decide if you can live without them.</p>
</li>
<li><p><strong>Be aware of version changes:</strong> If using older tutorials, know that Riverpod 3.0 moved StateNotifierProvider to legacy and introduced new testing utilities like <code>ProviderContainer.test()</code></p>
</li>
</ol>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Would I choose Riverpod for my next project? Yes.</p>
<p>Would I rewrite all my Bloc projects? No.</p>
<p>Riverpod isn't strictly better than Bloc, it's different. It trades explicit events for less boilerplate. It trades centralized event logs for automatic dependency management. It trades simplicity for power.</p>
<p>The key takeaway: <strong>Most of Riverpod's advantages over Bloc are really advantages over Bloc's event pattern specifically.</strong> If you compare Riverpod to Cubit (Bloc without events), the differences narrow significantly. Both let you modify state directly, both have simple APIs, both work well for most use cases.</p>
<p>Where Riverpod wins is dependency management and composition. Where Bloc wins is debuggability and explicitness.</p>
<p>As a long-term Bloc user, learning Riverpod changed how I think about state management. Even if you stick with Bloc, understanding Riverpod's reactive approach is useful.</p>
<p>The Flutter ecosystem is better for having both options.</p>
]]></content:encoded></item><item><title><![CDATA[Flutter Performance: What Actually Makes a Difference]]></title><description><![CDATA[I've dealt with my share of performance issues in production Flutter apps. Janky scrolling that makes users think the app is broken. Memory leaks that crash the app after 10 minutes of use. The kind of problems that make you question your life choice...]]></description><link>https://yshean.com/flutter-performance-what-actually-makes-a-difference</link><guid isPermaLink="true">https://yshean.com/flutter-performance-what-actually-makes-a-difference</guid><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Fri, 08 Aug 2025 22:00:00 GMT</pubDate><content:encoded><![CDATA[<p>I've dealt with my share of performance issues in production Flutter apps. Janky scrolling that makes users think the app is broken. Memory leaks that crash the app after 10 minutes of use. The kind of problems that make you question your life choices.</p>
<p>This article serves as a personal reminder of what to do in my new and existing Flutter projects, so that I don’t make the same mistakes again.</p>
<h2 id="heading-common-performance-issues">Common Performance Issues</h2>
<p>This is how performance issues look like in real apps:</p>
<p><strong>Janky scrolling:</strong></p>
<ul>
<li><p>User scrolls through a list</p>
</li>
<li><p>App stutters, drops frames</p>
</li>
<li><p>Looks broken, unprofessional</p>
</li>
</ul>
<p><strong>Memory leaks:</strong></p>
<ul>
<li><p>App works fine initially</p>
</li>
<li><p>Gets slower over time</p>
</li>
<li><p>Eventually crashes</p>
</li>
<li><p>Hard to reproduce, harder to debug</p>
</li>
</ul>
<h2 id="heading-what-actually-makes-a-difference">What Actually Makes a Difference</h2>
<p>After many instances of dealing with performance issues, here's what actually moves the needle.</p>
<h3 id="heading-1-const-constructors-the-easiest-win">1. Const Constructors: The Easiest Win</h3>
<p>This is the lowest-hanging fruit in Flutter performance. Use <code>const</code> constructors wherever you can.</p>
<p><strong>Bad:</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyWidget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Column(
      children: [
        Text(<span class="hljs-string">'Hello'</span>),
        SizedBox(height: <span class="hljs-number">16</span>),
        Text(<span class="hljs-string">'World'</span>),
      ],
    );
  }
}
</code></pre>
<p><strong>Good:</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyWidget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Column(
      children: [
        <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Hello'</span>),
        <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">16</span>),
        <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'World'</span>),
      ],
    );
  }
}
</code></pre>
<p><strong>Why it matters:</strong></p>
<p>When you use <code>const</code>, Flutter doesn't create new widget instances on every rebuild. It reuses the same instance. This means:</p>
<ul>
<li><p>Less memory allocation</p>
</li>
<li><p>Less garbage collection</p>
</li>
<li><p>Faster rebuilds</p>
</li>
<li><p>Better performance</p>
</li>
</ul>
<p><strong>Enable the lint rules:</strong></p>
<p>Add these to your <code>analysis_options.yaml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">include:</span> <span class="hljs-string">package:flutter_lints/flutter.yaml</span> <span class="hljs-comment"># actually this line would most likely suffice</span>

<span class="hljs-attr">linter:</span>
  <span class="hljs-attr">rules:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">prefer_const_constructors</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">prefer_const_constructors_in_immutables</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">prefer_const_declarations</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">prefer_const_literals_to_create_immutables</span>
</code></pre>
<p>These lint rules will warn you when you forget <code>const</code>. Follow the warnings. The <code>flutter_lints</code> package is included by default in Flutter projects created with Flutter 2.3+.</p>
<h3 id="heading-2-listview-performance-this-actually-matters">2. ListView Performance: This Actually Matters</h3>
<p>If your app has lists (and it probably does), this is important.</p>
<p><strong>Bad: Building all items upfront</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// DON'T DO THIS</span>
Widget build(BuildContext context) {
  <span class="hljs-keyword">return</span> ListView(
    children: items.map((item) =&gt; ItemWidget(item)).toList(),
  );
}
</code></pre>
<p><strong>Why it's bad:</strong></p>
<ul>
<li><p>Creates ALL widgets immediately, even items not visible on screen</p>
</li>
<li><p>With 1000 items = 1000 widgets in memory</p>
</li>
<li><p>Causes jank and memory issues</p>
</li>
</ul>
<p><strong>Good: Use ListView.builder</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// DO THIS</span>
Widget build(BuildContext context) {
  <span class="hljs-keyword">return</span> ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) {
      <span class="hljs-keyword">return</span> ItemWidget(items[index]);
    },
  );
}
</code></pre>
<p><strong>Why it's good:</strong></p>
<ul>
<li><p>Creates widgets on demand, so only visible items are built</p>
</li>
<li><p>Automatically recycles widgets</p>
</li>
<li><p>Handles 10,000 items without issues</p>
</li>
</ul>
<p><strong>For GridView, same principle:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// Good</span>
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: <span class="hljs-number">2</span>,
  ),
  itemCount: items.length,
  itemBuilder: (context, index) {
    <span class="hljs-keyword">return</span> ItemWidget(items[index]);
  },
)
</code></pre>
<p><strong>Separated lists:</strong></p>
<p>If you need to add dividers, use <code>ListView.separated</code>:</p>
<pre><code class="lang-dart">ListView.separated(
  itemCount: items.length,
  itemBuilder: (context, index) =&gt; ItemWidget(items[index]),
  separatorBuilder: (context, index) =&gt; <span class="hljs-keyword">const</span> Divider(),
)
</code></pre>
<p><strong>Custom ScrollView for complex layouts:</strong></p>
<p>For mixed content (lists, grids, single items), use <code>CustomScrollView</code> with slivers:</p>
<pre><code class="lang-dart">CustomScrollView(
  slivers: [
    SliverAppBar(
      title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Title'</span>),
    ),
    SliverToBoxAdapter(
      child: HeaderWidget(),
    ),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) =&gt; ItemWidget(items[index]),
        childCount: items.length,
      ),
    ),
    SliverGrid(
      delegate: SliverChildBuilderDelegate(
        (context, index) =&gt; GridItem(gridItems[index]),
        childCount: gridItems.length,
      ),
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: <span class="hljs-number">2</span>,
      ),
    ),
  ],
)
</code></pre>
<p>This keeps everything lazy and efficient.</p>
<h3 id="heading-3-images-dont-load-everything-at-once">3. Images: Don't Load Everything at Once</h3>
<p>Images are memory hogs. We need to be careful with large-sized or huge number of images.</p>
<p><strong>Bad: Loading large images</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// DON'T DO THIS with large images</span>
Image.asset(<span class="hljs-string">'assets/large_image.png'</span>)
</code></pre>
<p>If the image is 4000x3000 and you display it at 400x300, you're wasting memory.</p>
<p><strong>Good: Use appropriate sizes</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// For network images, use cacheWidth/cacheHeight</span>
Image.network(
  imageUrl,
  cacheWidth: <span class="hljs-number">400</span>,
  cacheHeight: <span class="hljs-number">300</span>,
)

<span class="hljs-comment">// For asset images, provide multiple resolutions</span>
<span class="hljs-comment">// assets/</span>
<span class="hljs-comment">//   image.png      (1x)</span>
<span class="hljs-comment">//   2.0x/image.png (2x)</span>
<span class="hljs-comment">//   3.0x/image.png (3x)</span>
Image.asset(<span class="hljs-string">'assets/image.png'</span>)
</code></pre>
<p><strong>In lists, always specify dimensions:</strong></p>
<pre><code class="lang-dart">ListView.builder(
  itemBuilder: (context, index) {
    <span class="hljs-keyword">return</span> Image.network(
      items[index].imageUrl,
      width: <span class="hljs-number">100</span>,
      height: <span class="hljs-number">100</span>,
      cacheWidth: <span class="hljs-number">100</span>,
      cacheHeight: <span class="hljs-number">100</span>,
      fit: BoxFit.cover,
    );
  },
)
</code></pre>
<p><strong>Use a proper image caching library:</strong></p>
<p>For production apps, use <code>cached_network_image</code>:</p>
<pre><code class="lang-dart">CachedNetworkImage(
  imageUrl: imageUrl,
  width: <span class="hljs-number">100</span>,
  height: <span class="hljs-number">100</span>,
  memCacheWidth: <span class="hljs-number">100</span>,
  memCacheHeight: <span class="hljs-number">100</span>,
  placeholder: (context, url) =&gt; <span class="hljs-keyword">const</span> CircularProgressIndicator(),
  errorWidget: (context, url, error) =&gt; <span class="hljs-keyword">const</span> Icon(Icons.error),
)
</code></pre>
<p>This handles caching, memory management, and loading states for you.</p>
<h3 id="heading-4-memory-leaks">4. Memory Leaks</h3>
<p><strong>Problem 1: Not disposing controllers</strong></p>
<p><strong>Bad:</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyWidget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  State&lt;MyWidget&gt; createState() =&gt; _MyWidgetState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MyWidgetState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">MyWidget</span>&gt; </span>{
  <span class="hljs-keyword">final</span> _controller = TextEditingController();
  <span class="hljs-keyword">final</span> _scrollController = ScrollController();
  <span class="hljs-keyword">final</span> _animationController = AnimationController(
    vsync: <span class="hljs-keyword">this</span>,
    duration: <span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>),
  );

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> TextField(controller: _controller);
  }

  <span class="hljs-comment">// Forgot to dispose!</span>
}
</code></pre>
<p><strong>Good:</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MyWidgetState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">MyWidget</span>&gt; 
    <span class="hljs-title">with</span> <span class="hljs-title">SingleTickerProviderStateMixin</span> </span>{
  <span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> TextEditingController _controller;
  <span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> ScrollController _scrollController;
  <span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> AnimationController _animationController;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    _controller = TextEditingController();
    _scrollController = ScrollController();
    _animationController = AnimationController(
      vsync: <span class="hljs-keyword">this</span>,
      duration: <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>),
    );
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _controller.dispose();
    _scrollController.dispose();
    _animationController.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> TextField(controller: _controller);
  }
}
</code></pre>
<p><strong>Controllers that need disposal:</strong></p>
<ul>
<li><p><code>TextEditingController</code></p>
</li>
<li><p><code>ScrollController</code></p>
</li>
<li><p><code>TabController</code></p>
</li>
<li><p><code>AnimationController</code></p>
</li>
<li><p><code>PageController</code></p>
</li>
<li><p><code>VideoPlayerController</code></p>
</li>
<li><p>Any controller with a <code>dispose()</code> method</p>
</li>
</ul>
<p><strong>Enable the lint rule:</strong></p>
<p>Add this to catch missing disposal:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">linter:</span>
  <span class="hljs-attr">rules:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">close_sinks</span>
</code></pre>
<p>Note: The built-in <code>close_sinks</code> rule catches <code>Sink</code> instances but doesn't catch all controllers. For more comprehensive coverage, consider using third-party linter packages like <code>dart_code_linter</code> (DCM) which has rules like <code>dispose-fields</code> that specifically check for undisposed controllers in StatefulWidgets.</p>
<p><strong>Problem 2: Not canceling streams</strong></p>
<p><strong>Bad:</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MyWidgetState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">MyWidget</span>&gt; </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();

    <span class="hljs-comment">// This stream keeps running even after widget is disposed</span>
    Stream.periodic(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>)).listen((event) {
      setState(() {
        <span class="hljs-comment">// Update state</span>
      });
    });
  }
}
</code></pre>
<p><strong>Good:</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MyWidgetState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">MyWidget</span>&gt; </span>{
  StreamSubscription? _subscription;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();

    _subscription = Stream.periodic(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>)).listen((event) {
      <span class="hljs-keyword">if</span> (mounted) {
        setState(() {
          <span class="hljs-comment">// Update state</span>
        });
      }
    });
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _subscription?.cancel();
    <span class="hljs-keyword">super</span>.dispose();
  }
}
</code></pre>
<p><strong>Enable the lint rule:</strong></p>
<pre><code class="lang-yaml"><span class="hljs-attr">linter:</span>
  <span class="hljs-attr">rules:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">cancel_subscriptions</span>
</code></pre>
<p>This rule warns you when you create a <code>StreamSubscription</code> but don't cancel it. It's part of the recommended Flutter lints.</p>
<p><strong>Problem 3: Not removing listeners</strong></p>
<p><strong>Bad:</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MyWidgetState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">MyWidget</span>&gt; </span>{
  <span class="hljs-keyword">final</span> scrollController = ScrollController();

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();

    scrollController.addListener(() {
      <span class="hljs-comment">// Do something</span>
    });
    <span class="hljs-comment">// Listener never removed!</span>
  }
}
</code></pre>
<p><strong>Good:</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MyWidgetState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">MyWidget</span>&gt; </span>{
  <span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> ScrollController scrollController;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    scrollController = ScrollController();
    scrollController.addListener(_onScroll);
  }

  <span class="hljs-keyword">void</span> _onScroll() {
    <span class="hljs-comment">// Do something</span>
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    scrollController.removeListener(_onScroll);
    scrollController.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }
}
</code></pre>
<p>For listener removal, DCM provides the <code>always-remove-listener</code> rule that catches this pattern.</p>
<p><strong>Problem 4: Timers and Future callbacks</strong></p>
<p><strong>Bad:</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MyWidgetState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">MyWidget</span>&gt; </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();

    <span class="hljs-comment">// Timer keeps running after widget is disposed</span>
    Timer.periodic(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>), (timer) {
      setState(() {}); <span class="hljs-comment">// Crash! Widget is disposed</span>
    });
  }
}
</code></pre>
<p><strong>Good:</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MyWidgetState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">MyWidget</span>&gt; </span>{
  Timer? _timer;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();

    _timer = Timer.periodic(<span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>), (timer) {
      <span class="hljs-keyword">if</span> (mounted) {
        setState(() {});
      }
    });
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _timer?.cancel();
    <span class="hljs-keyword">super</span>.dispose();
  }
}
</code></pre>
<p><strong>Use the</strong> <code>mounted</code> <strong>check:</strong></p>
<p>Always check <code>mounted</code> before calling <code>setState</code> in async callbacks:</p>
<pre><code class="lang-dart">Future&lt;<span class="hljs-keyword">void</span>&gt; fetchData() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> data = <span class="hljs-keyword">await</span> api.getData();

  <span class="hljs-keyword">if</span> (mounted) {
    setState(() {
      _data = data;
    });
  }
}
</code></pre>
<p>In fact, Flutter will warn you if you do not include a <code>mounted</code> check after an async callback before calling <code>setState</code>.</p>
<h3 id="heading-5-widget-rebuilds-break-it-down">5. Widget Rebuilds: Break It Down</h3>
<p>Flutter rebuilds the entire widget subtree when you call <code>setState()</code>. Minimize what rebuilds.</p>
<p><strong>Bad: Rebuilding everything</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomePage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  State&lt;HomePage&gt; createState() =&gt; _HomePageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_HomePageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">HomePage</span>&gt; </span>{
  <span class="hljs-built_in">int</span> _counter = <span class="hljs-number">0</span>;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: Text(<span class="hljs-string">'Home'</span>),
      ),
      body: Column(
        children: [
          ExpensiveWidget(), <span class="hljs-comment">// Rebuilds every time setState is called, unless this is marked as const</span>
          Text(<span class="hljs-string">'Counter: <span class="hljs-subst">$_counter</span>'</span>),
          AnotherExpensiveWidget(), <span class="hljs-comment">// Rebuilds every time setState is called, unless this is const</span>
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () =&gt; setState(() =&gt; _counter++),
      ),
    );
  }
}
</code></pre>
<p><strong>Good: Extract stateful widget</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomePage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Home'</span>),
      ),
      body: Column(
        children: [
          <span class="hljs-keyword">const</span> ExpensiveWidget(), <span class="hljs-comment">// Does not rebuild</span>
          <span class="hljs-keyword">const</span> CounterWidget(), <span class="hljs-comment">// Only this rebuilds</span>
          <span class="hljs-keyword">const</span> AnotherExpensiveWidget(), <span class="hljs-comment">// Does not rebuild</span>
        ],
      ),
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CounterWidget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> CounterWidget({Key? key}) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-meta">@override</span>
  State&lt;CounterWidget&gt; createState() =&gt; _CounterWidgetState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_CounterWidgetState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">CounterWidget</span>&gt; </span>{
  <span class="hljs-built_in">int</span> _counter = <span class="hljs-number">0</span>;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Column(
      children: [
        Text(<span class="hljs-string">'Counter: <span class="hljs-subst">$_counter</span>'</span>),
        FloatingActionButton(
          onPressed: () =&gt; setState(() =&gt; _counter++),
          child: <span class="hljs-keyword">const</span> Icon(Icons.add),
        ),
      ],
    );
  }
}
</code></pre>
<p><strong>Or use a builder:</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomePage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  State&lt;HomePage&gt; createState() =&gt; _HomePageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_HomePageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">HomePage</span>&gt; </span>{
  <span class="hljs-built_in">int</span> _counter = <span class="hljs-number">0</span>;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Home'</span>),
      ),
      body: Column(
        children: [
          <span class="hljs-keyword">const</span> ExpensiveWidget(),
          StatefulBuilder(
            builder: (context, setState) {
              <span class="hljs-keyword">return</span> Column(
                children: [
                  Text(<span class="hljs-string">'Counter: <span class="hljs-subst">$_counter</span>'</span>),
                  FloatingActionButton(
                    onPressed: () =&gt; setState(() =&gt; _counter++),
                    child: <span class="hljs-keyword">const</span> Icon(Icons.add),
                  ),
                ],
              );
            },
          ),
          <span class="hljs-keyword">const</span> AnotherExpensiveWidget(),
        ],
      ),
    );
  }
}
</code></pre>
<p><strong>Key principle:</strong> Keep stateful widgets small. Only the parts that change should be stateful.</p>
<h3 id="heading-6-keys">6. Keys</h3>
<p>There are specific cases where they are critical.</p>
<p><strong>You need keys when:</strong></p>
<ol>
<li><strong>Reordering lists:</strong></li>
</ol>
<pre><code class="lang-dart"><span class="hljs-built_in">List</span>&lt;Widget&gt; items = [
  ItemWidget(key: ValueKey(<span class="hljs-string">'item1'</span>), data: data1),
  ItemWidget(key: ValueKey(<span class="hljs-string">'item2'</span>), data: data2),
  ItemWidget(key: ValueKey(<span class="hljs-string">'item3'</span>), data: data3),
];

<span class="hljs-comment">// Without keys, Flutter might reuse the wrong widget state</span>
<span class="hljs-comment">// when items are reordered</span>
</code></pre>
<ol start="2">
<li><strong>Preserving state when parent rebuilds:</strong></li>
</ol>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ParentWidget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  State&lt;ParentWidget&gt; createState() =&gt; _ParentWidgetState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_ParentWidgetState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">ParentWidget</span>&gt; </span>{
  <span class="hljs-built_in">bool</span> _showFirst = <span class="hljs-keyword">true</span>;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Column(
      children: [
        <span class="hljs-keyword">if</span> (_showFirst)
          StatefulChild(key: ValueKey(<span class="hljs-string">'first'</span>))
        <span class="hljs-keyword">else</span>
          StatefulChild(key: ValueKey(<span class="hljs-string">'second'</span>)),
      ],
    );
  }
}
</code></pre>
<ol start="3">
<li><strong>In PageView or TabView with stateful children:</strong></li>
</ol>
<pre><code class="lang-dart">PageView(
  children: [
    Page1(key: PageStorageKey(<span class="hljs-string">'page1'</span>)),
    Page2(key: PageStorageKey(<span class="hljs-string">'page2'</span>)),
    Page3(key: PageStorageKey(<span class="hljs-string">'page3'</span>)),
  ],
)
</code></pre>
<p><strong>Types of keys:</strong></p>
<ul>
<li><p><code>ValueKey</code>: When you have a unique value (ID, string)</p>
</li>
<li><p><code>ObjectKey</code>: When you have a unique object</p>
</li>
<li><p><code>UniqueKey</code>: When you need a unique key every time</p>
</li>
<li><p><code>GlobalKey</code>: When you need to access widget state from outside (rare)</p>
</li>
<li><p><code>PageStorageKey</code>: For preserving scroll position</p>
</li>
</ul>
<p><strong>Enable the lint rule:</strong></p>
<pre><code class="lang-yaml"><span class="hljs-attr">linter:</span>
  <span class="hljs-attr">rules:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">use_key_in_widget_constructors</span>
</code></pre>
<p>This reminds you to add keys to your custom widgets when they might need them.</p>
<h2 id="heading-performance-patterns-to-follow-from-day-1">Performance Patterns to Follow From Day 1</h2>
<p>These are patterns you should always follow. They are not premature optimization, they're just good defaults.</p>
<h3 id="heading-1-always-use-const">1. Always Use Const</h3>
<p>If a widget can be <code>const</code>, make it <code>const</code>.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Good habit</span>
<span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Hello'</span>)
<span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">16</span>)
<span class="hljs-keyword">const</span> Icon(Icons.add)
<span class="hljs-keyword">const</span> Padding(padding: EdgeInsets.all(<span class="hljs-number">8</span>))
</code></pre>
<h3 id="heading-2-always-use-builder-for-lists">2. Always Use Builder for Lists</h3>
<p>Never use <code>.toList()</code> on a map in a ListView.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Bad</span>
ListView(children: items.map((item) =&gt; Widget(item)).toList())

<span class="hljs-comment">// Good</span>
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) =&gt; Widget(items[index]),
)
</code></pre>
<h3 id="heading-3-extract-widgets-early">3. Extract Widgets Early</h3>
<p>Don't wait for performance issues to extract widgets. Do it as you write.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Instead of this</span>
Widget build(BuildContext context) {
  <span class="hljs-keyword">return</span> Column(
    children: [
      Container(
        <span class="hljs-comment">// 50 lines of widget code</span>
      ),
      Container(
        <span class="hljs-comment">// Another 50 lines</span>
      ),
    ],
  );
}

<span class="hljs-comment">// Also don't do this</span>
Widget build(BuildContext context) {
  <span class="hljs-keyword">return</span> Column(
    children: [
      _buildFirstSection(),
      _buildSecondSection(),
    ],
  );
}

Widget _buildFirstSection() {
  <span class="hljs-keyword">return</span> Container(
    <span class="hljs-comment">// Widget code</span>
  );
}

Widget _buildSecondSection() {
  <span class="hljs-keyword">return</span> Container(
    <span class="hljs-comment">// Widget code</span>
  );
}
</code></pre>
<p>Instead, create separate widget classes:</p>
<pre><code class="lang-dart">Widget build(BuildContext context) {
  <span class="hljs-keyword">return</span> Column(
    children: [
      <span class="hljs-keyword">const</span> FirstSection(),
      <span class="hljs-keyword">const</span> SecondSection(),
    ],
  );
}
</code></pre>
<h3 id="heading-4-avoid-unnecessary-containers">4. Avoid Unnecessary Containers</h3>
<p>Don't wrap widgets in <code>Container</code> when you don't need to.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Bad</span>
Container(
  child: Text(<span class="hljs-string">'Hello'</span>),
)

<span class="hljs-comment">// Good</span>
Text(<span class="hljs-string">'Hello'</span>)

<span class="hljs-comment">// If you need padding</span>
Padding(
  padding: EdgeInsets.all(<span class="hljs-number">8</span>),
  child: Text(<span class="hljs-string">'Hello'</span>),
)

<span class="hljs-comment">// If you need size</span>
SizedBox(
  width: <span class="hljs-number">100</span>,
  height: <span class="hljs-number">100</span>,
  child: Text(<span class="hljs-string">'Hello'</span>),
)
</code></pre>
<p><strong>Enable the lint rule:</strong></p>
<pre><code class="lang-yaml"><span class="hljs-attr">linter:</span>
  <span class="hljs-attr">rules:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">avoid_unnecessary_containers</span>
</code></pre>
<p>This warns you when a <code>Container</code> isn't doing anything useful.</p>
<h3 id="heading-5-always-dispose">5. Always Dispose</h3>
<p>Create a checklist for every StatefulWidget:</p>
<ul>
<li><p>Created a controller? Add a dispose.</p>
</li>
<li><p>Added a listener? Remove it in dispose.</p>
</li>
<li><p>Started a stream? Cancel it in dispose.</p>
</li>
<li><p>Started a timer? Cancel it in dispose.</p>
</li>
</ul>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MyWidgetState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">MyWidget</span>&gt; </span>{
  <span class="hljs-comment">// Controllers</span>
  <span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> _controller = TextEditingController();

  <span class="hljs-comment">// Subscriptions</span>
  StreamSubscription? _subscription;

  <span class="hljs-comment">// Timers</span>
  Timer? _timer;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    <span class="hljs-comment">// Setup</span>
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    <span class="hljs-comment">// Clean up EVERYTHING</span>
    _controller.dispose();
    _subscription?.cancel();
    _timer?.cancel();
    <span class="hljs-keyword">super</span>.dispose();
  }
}
</code></pre>
<h3 id="heading-6-specify-image-sizes">6. Specify Image Sizes</h3>
<p>Whenever you use images, specify dimensions:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Always specify size</span>
Image.network(
  url,
  width: <span class="hljs-number">100</span>,
  height: <span class="hljs-number">100</span>,
  cacheWidth: <span class="hljs-number">100</span>,
  cacheHeight: <span class="hljs-number">100</span>,
)
</code></pre>
<h3 id="heading-7-use-lazy-loading">7. Use Lazy Loading</h3>
<p>Use builder pattern for any list or grid:</p>
<ul>
<li><p><code>ListView.builder</code></p>
</li>
<li><p><code>GridView.builder</code></p>
</li>
<li><p><code>ListView.separated</code></p>
</li>
<li><p><code>CustomScrollView</code> with slivers</p>
</li>
</ul>
<p>Never use regular <code>ListView()</code> or <code>GridView()</code> with a list of children.</p>
<h2 id="heading-other-good-optimizations">Other Good Optimizations</h2>
<p><strong>Optimize later when you measure:</strong></p>
<ul>
<li><p>Using <code>RepaintBoundary</code> for expensive widgets</p>
</li>
<li><p>Using <code>ListView.builder</code> with <code>cacheExtent</code></p>
</li>
<li><p>Implementing <code>shouldRebuild</code> in custom widgets</p>
</li>
<li><p>Using <code>compute()</code> for heavy calculations</p>
</li>
</ul>
<p><strong>How to measure:</strong></p>
<p>Flutter has built-in tools:</p>
<ol>
<li><p><strong>Performance overlay:</strong></p>
<p> You can toggle display of the performance overlay on your app using the Performance Overlay button in the Flutter inspector. If you prefer to do it in code, add the following:</p>
</li>
</ol>
<pre><code class="lang-dart">MaterialApp(
  showPerformanceOverlay: <span class="hljs-keyword">true</span>,
  <span class="hljs-comment">// ...</span>
)
</code></pre>
<ol start="2">
<li><strong>DevTools:</strong></li>
</ol>
<ul>
<li><p>Open DevTools in your IDE</p>
</li>
<li><p>Go to Performance tab</p>
</li>
<li><p>Record while you interact with your app</p>
</li>
<li><p>Look for frame drops (red bars)</p>
</li>
</ul>
<ol start="3">
<li><strong>Timeline:</strong></li>
</ol>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:developer'</span>;

Timeline.startSync(<span class="hljs-string">'expensive_operation'</span>);
<span class="hljs-comment">// Your code</span>
Timeline.finishSync();
</code></pre>
<p>Then view in DevTools.</p>
<p><strong>What to look for:</strong></p>
<ul>
<li><p>Frame render time &gt; 16ms (for a 60Hz display) → causes jank</p>
</li>
<li><p>Excessive rebuilds</p>
</li>
<li><p>Memory growing over time (leak)</p>
</li>
<li><p>Long build times for specific widgets</p>
</li>
</ul>
<h2 id="heading-common-mistakes">Common Mistakes</h2>
<p><strong>Mistake 1: Premature compute()</strong></p>
<p>Don't throw everything in <code>compute()</code> for isolate processing. Isolates have overhead. Most operations are fast enough on the main thread.</p>
<p>Use <code>compute()</code> when:</p>
<ul>
<li><p>Parsing large JSON (1000+ items)</p>
</li>
<li><p>Image processing</p>
</li>
<li><p>Complex calculations (&gt;100ms)</p>
</li>
</ul>
<p>Don't use <code>compute()</code> for:</p>
<ul>
<li><p>Simple calculations</p>
</li>
<li><p>Small JSON parsing</p>
</li>
<li><p>Database queries (already async)</p>
</li>
</ul>
<p><strong>Mistake 2: Overusing setState()</strong></p>
<p>Every <code>setState()</code> rebuilds the widget. If you're calling it in a loop, you're killing performance.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Bad</span>
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> items) {
  setState(() {
    <span class="hljs-comment">// Update for each item</span>
  });
}

<span class="hljs-comment">// Good</span>
setState(() {
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> items) {
    <span class="hljs-comment">// Update all items</span>
  }
});
</code></pre>
<p><strong>Mistake 3: Not testing on real devices, with profile mode</strong></p>
<p>Your M1 Mac can handle anything. Your user's budget phone might not.</p>
<p>Always test on:</p>
<ul>
<li><p>A mid-range Android/iOS device (3-4 years old)</p>
</li>
<li><p>A low-end device if your users are price-sensitive</p>
</li>
<li><p>Real network conditions</p>
</li>
</ul>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Performance in Flutter is not about knowing obscure tricks. It's about following good patterns consistently.</p>
<p>The biggest performance issues I've dealt with came from:</p>
<ul>
<li><p>Not using builders for lists (trying to render 1000 widgets)</p>
</li>
<li><p>Not disposing controllers (memory leaks)</p>
</li>
<li><p>Not using const (unnecessary rebuilds)</p>
</li>
<li><p>Loading huge images (memory issues)</p>
</li>
</ul>
<p>All of these are preventable with good defaults and the right linter rules.</p>
<p>And when you do have performance issues, don't guess. Measure (with profile mode), find the bottleneck, fix it, measure again.</p>
<p>That's it. No magic, no tricks. Just consistent good practices.</p>
]]></content:encoded></item><item><title><![CDATA[Flutter Architecture Patterns: Clean Architecture vs Feature-First]]></title><description><![CDATA[I've been building Flutter apps for a team of around 5 developers for the past few years. When I started, everyone talked about Clean Architecture like it was the only "professional" way to build apps. So naturally, I tried it.
After months of writin...]]></description><link>https://yshean.com/flutter-architecture-patterns-clean-architecture-vs-feature-first</link><guid isPermaLink="true">https://yshean.com/flutter-architecture-patterns-clean-architecture-vs-feature-first</guid><category><![CDATA[Flutter]]></category><category><![CDATA[architecture]]></category><category><![CDATA[clean code]]></category><category><![CDATA[Clean Architecture]]></category><category><![CDATA[Dart]]></category><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Sun, 08 Jun 2025 22:00:00 GMT</pubDate><content:encoded><![CDATA[<p>I've been building Flutter apps for a team of around 5 developers for the past few years. When I started, everyone talked about Clean Architecture like it was the only "professional" way to build apps. So naturally, I tried it.</p>
<p>After months of writing mappers between entities and models, creating use cases for simple API calls, and watching new team members struggle to add basic features, I switched to Feature-First. I haven't looked back.</p>
<p>This isn't to say Clean Architecture is bad. It's just that for most apps, it's overkill. Here's what I learned.</p>
<h2 id="heading-what-is-clean-architecture">What is Clean Architecture?</h2>
<p>Clean Architecture organizes code into layers with strict dependency rules:</p>
<pre><code class="lang-plaintext">lib/
  core/
    error/
    usecases/
    network/
  features/
    auth/
      data/
        datasources/
          auth_remote_datasource.dart
        models/
          user_model.dart
        repositories/
          auth_repository_impl.dart
      domain/
        entities/
          user.dart
        repositories/
          auth_repository.dart
        usecases/
          login_usecase.dart
          logout_usecase.dart
      presentation/
        bloc/
          auth_bloc.dart
          auth_event.dart
          auth_state.dart
        pages/
          login_page.dart
</code></pre>
<p><strong>The rules:</strong></p>
<ul>
<li><p>Domain layer (entities, repository interfaces, use cases) has no dependencies on other layers</p>
</li>
<li><p>Data layer (models, repository implementations, data sources) depends on domain</p>
</li>
<li><p>Presentation layer (UI, Bloc/Cubit) depends on domain</p>
</li>
<li><p>Dependencies point inward (presentation → domain ← data)</p>
</li>
</ul>
<p><strong>The goal:</strong></p>
<ul>
<li><p>Business logic is independent of frameworks</p>
</li>
<li><p>Testable</p>
</li>
<li><p>Easy to swap implementations (change API, change database, etc.)</p>
</li>
</ul>
<h2 id="heading-what-is-feature-first">What is Feature-First?</h2>
<p>Feature-First organizes code by features, not layers:</p>
<pre><code class="lang-plaintext">lib/
  core/
    network/
      api_client.dart
    auth/
      auth_service.dart
    models/
      user.dart
  features/
    auth/
      login_screen.dart
      auth_bloc.dart
      auth_repository.dart
    profile/
      profile_screen.dart
      profile_bloc.dart
      profile_repository.dart
    settings/
      settings_screen.dart
      settings_cubit.dart
</code></pre>
<p><strong>The rules:</strong></p>
<ul>
<li><p>Group by feature, not technical role</p>
</li>
<li><p>Shared code goes in <code>core/</code></p>
</li>
<li><p>Each feature is self-contained</p>
</li>
<li><p>No strict layer rules</p>
</li>
</ul>
<p><strong>The goal:</strong></p>
<ul>
<li><p>Easy to find related code</p>
</li>
<li><p>Fast to add new features</p>
</li>
<li><p>Less ceremony</p>
</li>
</ul>
<p>Notice the difference? Clean Architecture: 3 folders deep before you write code. Feature-First: 1 folder deep.</p>
<h2 id="heading-auth-flow-clean-architecture-style">Auth Flow: Clean Architecture Style</h2>
<p>Let me show you a login feature in Clean Architecture:</p>
<p><strong>1. Domain Entity:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// domain/entities/user.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> id;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> email;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> name;

  <span class="hljs-keyword">const</span> User({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.id,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.email,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.name,
  });
}
</code></pre>
<p><strong>2. Data Model:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// data/models/user_model.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserModel</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">User</span> </span>{
  <span class="hljs-keyword">const</span> UserModel({
    <span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> id,
    <span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> email,
    <span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> name,
  }) : <span class="hljs-keyword">super</span>(id: id, email: email, name: name);

  <span class="hljs-keyword">factory</span> UserModel.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) {
    <span class="hljs-keyword">return</span> UserModel(
      id: json[<span class="hljs-string">'id'</span>],
      email: json[<span class="hljs-string">'email'</span>],
      name: json[<span class="hljs-string">'name'</span>],
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'id'</span>: id,
      <span class="hljs-string">'email'</span>: email,
      <span class="hljs-string">'name'</span>: name,
    };
  }
}
</code></pre>
<p><strong>3. Repository Interface:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// domain/repositories/auth_repository.dart</span>
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthRepository</span> </span>{
  Future&lt;Either&lt;Failure, User&gt;&gt; login(<span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password);
  Future&lt;Either&lt;Failure, <span class="hljs-keyword">void</span>&gt;&gt; logout();
  Future&lt;Either&lt;Failure, User&gt;&gt; getCurrentUser();
}
</code></pre>
<p><strong>4. Repository Implementation:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// data/repositories/auth_repository_impl.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthRepositoryImpl</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">AuthRepository</span> </span>{
  <span class="hljs-keyword">final</span> AuthRemoteDataSource remoteDataSource;
  <span class="hljs-keyword">final</span> AuthLocalDataSource localDataSource;
  <span class="hljs-keyword">final</span> NetworkInfo networkInfo;

  AuthRepositoryImpl({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.remoteDataSource,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.localDataSource,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.networkInfo,
  });

  <span class="hljs-meta">@override</span>
  Future&lt;Either&lt;Failure, User&gt;&gt; login(<span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">await</span> networkInfo.isConnected) {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">final</span> userModel = <span class="hljs-keyword">await</span> remoteDataSource.login(email, password);
        <span class="hljs-keyword">await</span> localDataSource.cacheUser(userModel);
        <span class="hljs-keyword">return</span> Right(userModel);
      } <span class="hljs-keyword">on</span> ServerException {
        <span class="hljs-keyword">return</span> Left(ServerFailure());
      }
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">return</span> Left(NetworkFailure());
    }
  }
}
</code></pre>
<p><strong>5. Use Case:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// domain/usecases/login_usecase.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginUseCase</span> </span>{
  <span class="hljs-keyword">final</span> AuthRepository repository;

  LoginUseCase(<span class="hljs-keyword">this</span>.repository);

  Future&lt;Either&lt;Failure, User&gt;&gt; call(LoginParams params) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> repository.login(params.email, params.password);
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginParams</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> email;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> password;

  LoginParams({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.email, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.password});
}
</code></pre>
<p><strong>6. Bloc:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// presentation/bloc/auth_bloc.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthBloc</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Bloc</span>&lt;<span class="hljs-title">AuthEvent</span>, <span class="hljs-title">AuthState</span>&gt; </span>{
  <span class="hljs-keyword">final</span> LoginUseCase loginUseCase;
  <span class="hljs-keyword">final</span> LogoutUseCase logoutUseCase;
  <span class="hljs-keyword">final</span> GetCurrentUserUseCase getCurrentUserUseCase;

  AuthBloc({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.loginUseCase,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.logoutUseCase,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.getCurrentUserUseCase,
  }) : <span class="hljs-keyword">super</span>(AuthInitial()) {
    <span class="hljs-keyword">on</span>&lt;LoginRequested&gt;(_onLoginRequested);
    <span class="hljs-keyword">on</span>&lt;LogoutRequested&gt;(_onLogoutRequested);
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _onLoginRequested(
    LoginRequested event,
    Emitter&lt;AuthState&gt; emit,
  ) <span class="hljs-keyword">async</span> {
    emit(AuthLoading());

    <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> loginUseCase(
      LoginParams(email: event.email, password: event.password),
    );

    result.fold(
      (failure) =&gt; emit(AuthError(message: _mapFailureToMessage(failure))),
      (user) =&gt; emit(AuthAuthenticated(user: user)),
    );
  }
}
</code></pre>
<p><strong>7. UI:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// presentation/pages/login_page.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginPage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> BlocProvider(
      create: (context) =&gt; AuthBloc(
        loginUseCase: getIt&lt;LoginUseCase&gt;(),
        logoutUseCase: getIt&lt;LogoutUseCase&gt;(),
        getCurrentUserUseCase: getIt&lt;GetCurrentUserUseCase&gt;(),
      ),
      child: LoginView(),
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginView</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> BlocConsumer&lt;AuthBloc, AuthState&gt;(
      listener: (context, state) {
        <span class="hljs-keyword">if</span> (state <span class="hljs-keyword">is</span> AuthError) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(state.message)),
          );
        }
      },
      builder: (context, state) {
        <span class="hljs-keyword">if</span> (state <span class="hljs-keyword">is</span> AuthLoading) {
          <span class="hljs-keyword">return</span> Center(child: CircularProgressIndicator());
        }

        <span class="hljs-keyword">return</span> LoginForm(
          onSubmit: (email, password) {
            context.read&lt;AuthBloc&gt;().add(
              LoginRequested(email: email, password: password),
            );
          },
        );
      },
    );
  }
}
</code></pre>
<p>Count the files for a simple login: <strong>11 files</strong> (Entity, Model, Repository Interface, Repository Impl, Data Source Interface, Data Source Impl, Use Case, Bloc, Event, State, Page).</p>
<h2 id="heading-auth-flow-feature-first-style">Auth Flow: Feature-First Style</h2>
<p>Now let me show you the same login in Feature-First:</p>
<p><strong>1. Model (in core):</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// core/models/user.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> id;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> email;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> name;

  <span class="hljs-keyword">const</span> User({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.id,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.email,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.name,
  });

  <span class="hljs-keyword">factory</span> User.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) {
    <span class="hljs-keyword">return</span> User(
      id: json[<span class="hljs-string">'id'</span>],
      email: json[<span class="hljs-string">'email'</span>],
      name: json[<span class="hljs-string">'name'</span>],
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'id'</span>: id,
      <span class="hljs-string">'email'</span>: email,
      <span class="hljs-string">'name'</span>: name,
    };
  }
}
</code></pre>
<p><strong>2. Repository:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// features/auth/auth_repository.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthRepository</span> </span>{
  <span class="hljs-keyword">final</span> ApiClient _apiClient;

  AuthRepository(<span class="hljs-keyword">this</span>._apiClient);

  Future&lt;User&gt; login(<span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _apiClient.post(
        <span class="hljs-string">'/auth/login'</span>,
        body: {<span class="hljs-string">'email'</span>: email, <span class="hljs-string">'password'</span>: password},
      );
      <span class="hljs-keyword">return</span> User.fromJson(response);
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Login failed: <span class="hljs-subst">$e</span>'</span>);
    }
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; logout() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> _apiClient.post(<span class="hljs-string">'/auth/logout'</span>);
  }

  Future&lt;User&gt; getCurrentUser() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _apiClient.<span class="hljs-keyword">get</span>(<span class="hljs-string">'/auth/me'</span>);
    <span class="hljs-keyword">return</span> User.fromJson(response);
  }
}
</code></pre>
<p><strong>3. Bloc:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// features/auth/auth_bloc.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthBloc</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Bloc</span>&lt;<span class="hljs-title">AuthEvent</span>, <span class="hljs-title">AuthState</span>&gt; </span>{
  <span class="hljs-keyword">final</span> AuthRepository _repository;

  AuthBloc(<span class="hljs-keyword">this</span>._repository) : <span class="hljs-keyword">super</span>(AuthInitial()) {
    <span class="hljs-keyword">on</span>&lt;LoginRequested&gt;(_onLoginRequested);
    <span class="hljs-keyword">on</span>&lt;LogoutRequested&gt;(_onLogoutRequested);
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _onLoginRequested(
    LoginRequested event,
    Emitter&lt;AuthState&gt; emit,
  ) <span class="hljs-keyword">async</span> {
    emit(AuthLoading());

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> user = <span class="hljs-keyword">await</span> _repository.login(event.email, event.password);
      emit(AuthAuthenticated(user: user));
    } <span class="hljs-keyword">catch</span> (e) {
      emit(AuthError(message: e.toString()));
    }
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _onLogoutRequested(
    LogoutRequested event,
    Emitter&lt;AuthState&gt; emit,
  ) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> _repository.logout();
    emit(AuthInitial());
  }
}

<span class="hljs-comment">// Events and States (same as Clean Architecture)</span>
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthEvent</span> </span>{}
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginRequested</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AuthEvent</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> email;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> password;
  LoginRequested({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.email, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.password});
}
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LogoutRequested</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AuthEvent</span> </span>{}

<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthState</span> </span>{}
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthInitial</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AuthState</span> </span>{}
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthLoading</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AuthState</span> </span>{}
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthAuthenticated</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AuthState</span> </span>{
  <span class="hljs-keyword">final</span> User user;
  AuthAuthenticated({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.user});
}
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthError</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AuthState</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> message;
  AuthError({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.message});
}
</code></pre>
<p><strong>4. UI:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// features/auth/login_screen.dart</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> BlocProvider(
      create: (context) =&gt; AuthBloc(
        context.read&lt;AuthRepository&gt;(),
      ),
      child: BlocConsumer&lt;AuthBloc, AuthState&gt;(
        listener: (context, state) {
          <span class="hljs-keyword">if</span> (state <span class="hljs-keyword">is</span> AuthError) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(state.message)),
            );
          }
        },
        builder: (context, state) {
          <span class="hljs-keyword">if</span> (state <span class="hljs-keyword">is</span> AuthLoading) {
            <span class="hljs-keyword">return</span> Center(child: CircularProgressIndicator());
          }

          <span class="hljs-keyword">return</span> LoginForm(
            onSubmit: (email, password) {
              context.read&lt;AuthBloc&gt;().add(
                LoginRequested(email: email, password: password),
              );
            },
          );
        },
      ),
    );
  }
}
</code></pre>
<p>Count the files: <strong>3 files</strong> (Model in core, Repository, Bloc + Screen).</p>
<p>Same functionality. 8 fewer files. No mappers. No use cases. No interfaces.</p>
<h2 id="heading-repository-pattern-the-differences">Repository Pattern: The Differences</h2>
<p><strong>Clean Architecture says:</strong></p>
<ul>
<li><p>Repository interface in domain</p>
</li>
<li><p>Repository implementation in data</p>
</li>
<li><p>Abstracts data sources (remote, local, cache)</p>
</li>
<li><p>Returns <code>Either&lt;Failure, T&gt;</code> for error handling</p>
</li>
</ul>
<p><strong>Feature-First says:</strong></p>
<ul>
<li><p>Repository is just a class</p>
</li>
<li><p>No interface needed (YAGNI - You Aren't Gonna Need It)</p>
</li>
<li><p>Throws exceptions for errors</p>
</li>
<li><p>Let the caller handle errors</p>
</li>
</ul>
<p><strong>My take:</strong> The interface adds indirection without benefit for most apps. You're not going to swap your Firebase implementation for a different backend. And if you do, search and replace works fine.</p>
<p>The <code>Either&lt;Failure, T&gt;</code> pattern from <code>dartz</code> or <code>fpdart</code> looks nice but adds cognitive overhead. Try/catch is simpler and everyone understands it.</p>
<h2 id="heading-the-entity-vs-model-debate">The Entity vs Model Debate</h2>
<p>This is where Clean Architecture frustrates me most.</p>
<p><strong>Clean Architecture:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// Domain entity (no dependencies)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> id;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> name;
  <span class="hljs-keyword">const</span> User({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.id, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.name});
}

<span class="hljs-comment">// Data model (extends entity, has JSON methods)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserModel</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">User</span> </span>{
  <span class="hljs-keyword">const</span> UserModel({<span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> id, <span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> name})
      : <span class="hljs-keyword">super</span>(id: id, name: name);

  <span class="hljs-keyword">factory</span> UserModel.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) =&gt; UserModel(
        id: json[<span class="hljs-string">'id'</span>],
        name: json[<span class="hljs-string">'name'</span>],
      );

  <span class="hljs-comment">// Now map everywhere:</span>
  User toEntity() =&gt; User(id: id, name: name);
}

<span class="hljs-comment">// In repository:</span>
<span class="hljs-keyword">final</span> userModel = <span class="hljs-keyword">await</span> dataSource.getUser();
<span class="hljs-keyword">return</span> Right(userModel.toEntity()); <span class="hljs-comment">// Map model to entity</span>

<span class="hljs-comment">// In use case, it's already an entity</span>

<span class="hljs-comment">// In Bloc:</span>
<span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> useCase.call();
<span class="hljs-comment">// Use the entity</span>
</code></pre>
<p><strong>The problem:</strong></p>
<ul>
<li><p>You're mapping identical data structures back and forth</p>
</li>
<li><p>The entity has no behavior (it's just data)</p>
</li>
<li><p>The model extends the entity, so they're not really separate anyway</p>
</li>
<li><p>You write <code>.toEntity()</code> everywhere</p>
</li>
</ul>
<p><strong>Feature-First:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// Just one class</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> id;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> name;

  <span class="hljs-keyword">const</span> User({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.id, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.name});

  <span class="hljs-keyword">factory</span> User.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) =&gt; User(
        id: json[<span class="hljs-string">'id'</span>],
        name: json[<span class="hljs-string">'name'</span>],
      );

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() =&gt; {<span class="hljs-string">'id'</span>: id, <span class="hljs-string">'name'</span>: name};
}
</code></pre>
<p>Done. No mapping. No confusion about which one to use where.</p>
<p><strong>"But what if the API response is different from your domain model?"</strong></p>
<p>Then make two classes:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> id;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> displayName;

  <span class="hljs-keyword">const</span> User({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.id, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.displayName});
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserResponse</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> userId;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> firstName;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> lastName;

  UserResponse.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json)
      : userId = json[<span class="hljs-string">'user_id'</span>],
        firstName = json[<span class="hljs-string">'first_name'</span>],
        lastName = json[<span class="hljs-string">'last_name'</span>];

  User toUser() =&gt; User(
    id: userId,
    displayName: <span class="hljs-string">'<span class="hljs-subst">$firstName</span> <span class="hljs-subst">$lastName</span>'</span>,
  );
}
</code></pre>
<p>Now you map only when the structures actually differ. Not as a ceremony.</p>
<h2 id="heading-use-cases-do-you-need-them">Use Cases: Do You Need Them?</h2>
<p>Clean Architecture creates a use case for every action:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginUseCase</span> </span>{
  <span class="hljs-keyword">final</span> AuthRepository repository;
  LoginUseCase(<span class="hljs-keyword">this</span>.repository);

  Future&lt;Either&lt;Failure, User&gt;&gt; call(LoginParams params) {
    <span class="hljs-keyword">return</span> repository.login(params.email, params.password);
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginParams</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> email;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> password;
  LoginParams({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.email, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.password});
}
</code></pre>
<p>This use case just calls the repository. It adds no logic. It's a pass-through.</p>
<p><strong>When use cases make sense:</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginUseCase</span> </span>{
  <span class="hljs-keyword">final</span> AuthRepository repository;
  <span class="hljs-keyword">final</span> AnalyticsService analytics;
  <span class="hljs-keyword">final</span> CacheService cache;

  Future&lt;User&gt; call(<span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password) <span class="hljs-keyword">async</span> {
    <span class="hljs-comment">// Clear old cache</span>
    <span class="hljs-keyword">await</span> cache.clear();

    <span class="hljs-comment">// Login</span>
    <span class="hljs-keyword">final</span> user = <span class="hljs-keyword">await</span> repository.login(email, password);

    <span class="hljs-comment">// Track analytics</span>
    <span class="hljs-keyword">await</span> analytics.logLogin(user.id);

    <span class="hljs-comment">// Cache user</span>
    <span class="hljs-keyword">await</span> cache.saveUser(user);

    <span class="hljs-keyword">return</span> user;
  }
}
</code></pre>
<p>Now the use case coordinates multiple services. That's useful.</p>
<p><strong>Feature-First approach:</strong> If your use case is just calling the repository, call the repository directly from Bloc:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthBloc</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Bloc</span>&lt;<span class="hljs-title">AuthEvent</span>, <span class="hljs-title">AuthState</span>&gt; </span>{
  <span class="hljs-keyword">final</span> AuthRepository _repository;

  Future&lt;<span class="hljs-keyword">void</span>&gt; _onLoginRequested(LoginRequested event, Emitter emit) <span class="hljs-keyword">async</span> {
    emit(AuthLoading());
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> user = <span class="hljs-keyword">await</span> _repository.login(event.email, event.password);
      emit(AuthAuthenticated(user: user));
    } <span class="hljs-keyword">catch</span> (e) {
      emit(AuthError(e.toString()));
    }
  }
}
</code></pre>
<p>If you need to coordinate multiple services, add a method to your repository:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthRepository</span> </span>{
  <span class="hljs-keyword">final</span> ApiClient _api;
  <span class="hljs-keyword">final</span> AnalyticsService _analytics;
  <span class="hljs-keyword">final</span> CacheService _cache;

  Future&lt;User&gt; login(<span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> _cache.clear();
    <span class="hljs-keyword">final</span> user = <span class="hljs-keyword">await</span> _api.login(email, password);
    <span class="hljs-keyword">await</span> _analytics.logLogin(user.id);
    <span class="hljs-keyword">await</span> _cache.saveUser(user);
    <span class="hljs-keyword">return</span> user;
  }
}
</code></pre>
<p>Or if it's complex, make a separate service:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthService</span> </span>{
  <span class="hljs-keyword">final</span> AuthRepository _repository;
  <span class="hljs-keyword">final</span> AnalyticsService _analytics;
  <span class="hljs-keyword">final</span> CacheService _cache;

  Future&lt;User&gt; login(<span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> _cache.clear();
    <span class="hljs-keyword">final</span> user = <span class="hljs-keyword">await</span> _repository.login(email, password);
    <span class="hljs-keyword">await</span> _analytics.logLogin(user.id);
    <span class="hljs-keyword">await</span> _cache.saveUser(user);
    <span class="hljs-keyword">return</span> user;
  }
}
</code></pre>
<p>Same functionality. No ceremony.</p>
<h2 id="heading-testing-differences">Testing Differences</h2>
<p><strong>Clean Architecture:</strong></p>
<pre><code class="lang-dart">test(<span class="hljs-string">'should return User when login is successful'</span>, () <span class="hljs-keyword">async</span> {
  <span class="hljs-comment">// Arrange</span>
  <span class="hljs-keyword">final</span> tUserModel = UserModel(id: <span class="hljs-string">'1'</span>, email: <span class="hljs-string">'test@test.com'</span>, name: <span class="hljs-string">'Test'</span>);
  <span class="hljs-keyword">final</span> tUser = User(id: <span class="hljs-string">'1'</span>, email: <span class="hljs-string">'test@test.com'</span>, name: <span class="hljs-string">'Test'</span>);

  when(mockRemoteDataSource.login(any, any))
      .thenAnswer((_) <span class="hljs-keyword">async</span> =&gt; tUserModel);
  when(mockLocalDataSource.cacheUser(any))
      .thenAnswer((_) <span class="hljs-keyword">async</span> =&gt; Future.value());
  when(mockNetworkInfo.isConnected).thenAnswer((_) <span class="hljs-keyword">async</span> =&gt; <span class="hljs-keyword">true</span>);

  <span class="hljs-comment">// Act</span>
  <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> repository.login(<span class="hljs-string">'test@test.com'</span>, <span class="hljs-string">'password'</span>);

  <span class="hljs-comment">// Assert</span>
  expect(result, Right(tUser));
  verify(mockRemoteDataSource.login(<span class="hljs-string">'test@test.com'</span>, <span class="hljs-string">'password'</span>));
  verify(mockLocalDataSource.cacheUser(tUserModel));
});
</code></pre>
<p><strong>Feature-First:</strong></p>
<pre><code class="lang-dart">test(<span class="hljs-string">'should return User when login is successful'</span>, () <span class="hljs-keyword">async</span> {
  <span class="hljs-comment">// Arrange</span>
  <span class="hljs-keyword">final</span> mockApi = MockApiClient();
  <span class="hljs-keyword">final</span> repository = AuthRepository(mockApi);

  when(mockApi.post(<span class="hljs-string">'/auth/login'</span>, body: any))
      .thenAnswer((_) <span class="hljs-keyword">async</span> =&gt; {<span class="hljs-string">'id'</span>: <span class="hljs-string">'1'</span>, <span class="hljs-string">'email'</span>: <span class="hljs-string">'test@test.com'</span>, <span class="hljs-string">'name'</span>: <span class="hljs-string">'Test'</span>});

  <span class="hljs-comment">// Act</span>
  <span class="hljs-keyword">final</span> user = <span class="hljs-keyword">await</span> repository.login(<span class="hljs-string">'test@test.com'</span>, <span class="hljs-string">'password'</span>);

  <span class="hljs-comment">// Assert</span>
  expect(user.id, <span class="hljs-string">'1'</span>);
  expect(user.email, <span class="hljs-string">'test@test.com'</span>);
  verify(mockApi.post(<span class="hljs-string">'/auth/login'</span>, body: any)).called(<span class="hljs-number">1</span>);
});
</code></pre>
<p>Same test coverage. Less setup. Less mocking.</p>
<p><strong>For Bloc tests:</strong></p>
<p>Both approaches use <code>bloc_test</code> the same way:</p>
<pre><code class="lang-dart">blocTest&lt;AuthBloc, AuthState&gt;(
  <span class="hljs-string">'emits [AuthLoading, AuthAuthenticated] when login succeeds'</span>,
  build: () {
    when(() =&gt; mockRepository.login(any(), any()))
        .thenAnswer((_) <span class="hljs-keyword">async</span> =&gt; testUser);
    <span class="hljs-keyword">return</span> AuthBloc(mockRepository);
  },
  act: (bloc) =&gt; bloc.add(LoginRequested(
    email: <span class="hljs-string">'test@test.com'</span>,
    password: <span class="hljs-string">'password'</span>,
  )),
  expect: () =&gt; [
    AuthLoading(),
    AuthAuthenticated(user: testUser),
  ],
);
</code></pre>
<p>The testing approach doesn't change much between architectures.</p>
<h2 id="heading-when-to-use-clean-architecture">When to Use Clean Architecture</h2>
<p>Clean Architecture makes sense when:</p>
<p>1. <strong>Your business logic is really complex</strong></p>
<ul>
<li><p>Not just CRUD operations</p>
</li>
<li><p>Many domain rules (e.g., financial calculations, workflow engines)</p>
</li>
<li><p>Multiple ways data can be manipulated</p>
</li>
</ul>
<p>2. <strong>You need to swap implementations frequently</strong></p>
<ul>
<li><p>Actually switching between different backends</p>
</li>
<li><p>A/B testing different data sources</p>
</li>
<li><p>Migrating from one service to another incrementally</p>
</li>
</ul>
<p>3. <strong>Your team is large (10+ developers)</strong></p>
<ul>
<li><p>Strict boundaries help prevent conflicts</p>
</li>
<li><p>Clear contracts between layers</p>
</li>
<li><p>Multiple teams working on different layers</p>
</li>
</ul>
<p>4. <strong>You're building a long-term enterprise app</strong></p>
<ul>
<li><p>5+ year timeline</p>
</li>
<li><p>Formal requirements documentation</p>
</li>
</ul>
<p>5. <strong>Your domain experts are not developers</strong></p>
<ul>
<li><p>Domain layer becomes a communication tool</p>
</li>
<li><p>Entities represent business concepts</p>
</li>
<li><p>Use cases map to business processes</p>
<ul>
<li><strong>Example:</strong> A banking app with loan calculations, fraud detection, transaction rules, multiple payment gateways, and regulatory requirements.</li>
</ul>
</li>
</ul>
<h2 id="heading-when-to-use-feature-first">When to Use Feature-First</h2>
<p>Feature-First makes sense when:</p>
<p>1. <strong>You're building a typical mobile app</strong></p>
<ul>
<li><p>Fetch data from API → Show it on screen</p>
</li>
<li><p>Submit forms</p>
</li>
<li><p>Handle auth</p>
</li>
</ul>
<p>2. <strong>Your team is small to medium (2-8 developers)</strong></p>
<ul>
<li><p>Everyone can see the whole codebase</p>
</li>
<li><p>Less overhead in coordination</p>
</li>
<li><p>Faster onboarding</p>
</li>
</ul>
<p>3. <strong>You need to move fast</strong></p>
<ul>
<li><p>Startup environment</p>
</li>
<li><p>Frequent pivots</p>
</li>
<li><p>MVP development</p>
</li>
</ul>
<p>4. <strong>Your business logic is simple</strong></p>
<ul>
<li><p>Backend does the heavy lifting</p>
</li>
<li><p>Client is mostly a view layer</p>
</li>
</ul>
<p>5. <strong>You want pragmatic, not dogmatic architecture</strong></p>
<ul>
<li><p>Add structure when needed</p>
</li>
<li><p>Start simple, refactor when it hurts - YAGNI principle</p>
</li>
</ul>
<p><strong>Example:</strong> A social media app, e-commerce app, content app, fitness tracker, most CRUD apps. This covers 90% of mobile apps.</p>
<h2 id="heading-what-i-actually-use">What I Actually Use</h2>
<p>For my team of 5 developers, we use Feature-First with these additions:</p>
<p><strong>Folder structure:</strong></p>
<pre><code class="lang-plaintext">lib/
  core/
    network/
      api_client.dart
      api_exception.dart
    models/
      user.dart
      pagination.dart
    services/
      auth_service.dart
      storage_service.dart
    widgets/
      loading_indicator.dart
      error_view.dart
  features/
    auth/
      login_screen.dart
      register_screen.dart
      auth_bloc.dart
      auth_repository.dart
    home/
      home_screen.dart
      home_bloc.dart
      home_repository.dart
    profile/
      profile_screen.dart
      profile_bloc.dart
      profile_repository.dart
  main.dart
  app.dart
</code></pre>
<p><strong>Our patterns:</strong></p>
<ol>
<li><strong>Repositories handle API calls</strong></li>
</ol>
<pre><code class="lang-dart">   <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomeRepository</span> </span>{
     <span class="hljs-keyword">final</span> ApiClient _api;

     Future&lt;<span class="hljs-built_in">List</span>&lt;Post&gt;&gt; getPosts() <span class="hljs-keyword">async</span> {
       <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _api.<span class="hljs-keyword">get</span>(<span class="hljs-string">'/posts'</span>);
       <span class="hljs-keyword">return</span> (response <span class="hljs-keyword">as</span> <span class="hljs-built_in">List</span>).map((e) =&gt; Post.fromJson(e)).toList();
     }
   }
</code></pre>
<ol start="2">
<li><strong>Blocs handle state and business logic</strong></li>
</ol>
<pre><code class="lang-dart">   <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomeBloc</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Bloc</span>&lt;<span class="hljs-title">HomeEvent</span>, <span class="hljs-title">HomeState</span>&gt; </span>{
     <span class="hljs-keyword">final</span> HomeRepository _repository;

     HomeBloc(<span class="hljs-keyword">this</span>._repository) : <span class="hljs-keyword">super</span>(HomeInitial()) {
       <span class="hljs-keyword">on</span>&lt;HomePostsRequested&gt;(_onPostsRequested);
     }

     Future&lt;<span class="hljs-keyword">void</span>&gt; _onPostsRequested(
       HomePostsRequested event,
       Emitter&lt;HomeState&gt; emit,
     ) <span class="hljs-keyword">async</span> {
       emit(HomeLoading());
       <span class="hljs-keyword">try</span> {
         <span class="hljs-keyword">final</span> posts = <span class="hljs-keyword">await</span> _repository.getPosts();
         emit(HomeLoaded(posts: posts));
       } <span class="hljs-keyword">catch</span> (e) {
         emit(HomeError(message: e.toString()));
       }
     }
   }
</code></pre>
<ol start="3">
<li><p><strong>Shared code in core/</strong></p>
<ul>
<li><p>Network client</p>
</li>
<li><p>Common models (User, pagination)</p>
</li>
<li><p>Services used across features (auth, storage, analytics)</p>
</li>
<li><p>Reusable widgets</p>
</li>
</ul>
</li>
<li><p><strong>Features are self-contained</strong></p>
<ul>
<li><p>Feature-specific models stay in the feature</p>
</li>
<li><p>Only promote to core/ when shared by 2+ features</p>
</li>
<li><p>Each feature has its own repository and bloc</p>
</li>
</ul>
</li>
<li><p><strong>No interfaces unless needed</strong></p>
<ul>
<li><p>If we actually need to mock something, we add an interface</p>
</li>
<li><p>Most of the time, we just mock the class directly in tests</p>
</li>
</ul>
</li>
</ol>
<p><strong>This works because:</strong></p>
<ul>
<li><p>Features are isolated, easy to find</p>
</li>
<li><p>New developers can contribute on day 1</p>
</li>
<li><p>Adding a feature is much faster</p>
</li>
<li><p>Refactoring is straightforward</p>
</li>
<li><p>Tests are simple to write</p>
</li>
</ul>
<h2 id="heading-when-we-add-structure">When We Add Structure</h2>
<p>We don't have strict rules. We add structure when we feel pain:</p>
<p><strong>"This repository is getting too big"</strong> → Split it into multiple repositories or services</p>
<p><strong>"Multiple features use the same model"</strong> → Move the model to <code>core/models/</code></p>
<p><strong>"This business logic is complex and needs testing"</strong> → Extract to a separate service class</p>
<p><strong>"We need to swap implementations for testing"</strong> → Add an interface for just that class</p>
<p>We don't add these things preemptively. We add them when they solve a real problem.</p>
<h2 id="heading-common-objections-to-feature-first">Common Objections to Feature-First</h2>
<p><strong>"But Feature-First doesn't scale!"</strong></p>
<p>It scales fine for small to medium teams. We've built apps with 50+ features and it's still easy to navigate. If you're at Google scale, sure, use Clean Architecture. Most of us aren't.</p>
<p><strong>"But you're tightly coupling your code!"</strong></p>
<p>Sure, that’s a tradeoff, but real decoupling comes from good boundaries (features don't depend on each other, shared code is in core/), not from layers.</p>
<p><strong>"But you can't test your business logic!"</strong></p>
<p>Yes you can. Your Bloc and Repository are testable. You mock the repository when testing Bloc. You mock the API client when testing repository. Same as Clean Architecture, just fewer files.</p>
<p><strong>"But what about SOLID principles?"</strong></p>
<p>SOLID is about good design, not about layers. You can follow SOLID with Feature-First:</p>
<ul>
<li><p>Single Responsibility: Each class has one job</p>
</li>
<li><p>Open/Closed: Use composition, not inheritance</p>
</li>
<li><p>Liskov Substitution: Not really relevant (we're not doing deep inheritance)</p>
</li>
<li><p>Interface Segregation: Create interfaces when you need them</p>
</li>
<li><p>Dependency Inversion: Depend on abstractions (e.g. ApiClient interface, not Dio directly)</p>
</li>
</ul>
<p>You don't need domain/data/presentation layers to follow SOLID.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Clean Architecture is a good pattern. It's well-documented, battle-tested, and has clear benefits in the right context.</p>
<p>But for most Flutter apps, it's overkill.</p>
<p>Your app probably doesn't need:</p>
<ul>
<li><p>Three layers with strict dependency rules</p>
</li>
<li><p>Separate entities and models for identical data</p>
</li>
<li><p>Use cases that just call repositories</p>
</li>
<li><p>Interfaces for every repository</p>
</li>
</ul>
<p>Your app probably does need:</p>
<ul>
<li><p>Clear feature boundaries</p>
</li>
<li><p>Shared code in a common module</p>
</li>
<li><p>Testable business logic</p>
</li>
<li><p>Easy onboarding for new developers</p>
</li>
</ul>
<p>Feature-First gives you these without the boilerplate.</p>
<p>Start simple. Add complexity when it solves a problem. Not before.</p>
]]></content:encoded></item><item><title><![CDATA[End-to-end testing on mobile apps with Maestro]]></title><description><![CDATA[Recently I talked about how to perform automated end-to-end testing on any mobile apps (yes, any, even those you don’t own) with Maestro.Deck: https://excalidraw.com/#json=YBaTDvWm8yTdshL8Z2IOL,7h7736K_Ey7wg08QljkbPg]]></description><link>https://yshean.com/end-to-end-testing-on-mobile-apps-with-maestro</link><guid isPermaLink="true">https://yshean.com/end-to-end-testing-on-mobile-apps-with-maestro</guid><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Fri, 16 May 2025 13:04:08 GMT</pubDate><content:encoded><![CDATA[<p>Recently I talked about how to perform automated end-to-end testing on any mobile apps (yes, any, even those you don’t own) with Maestro.<br />Deck: <a target="_blank" href="https://excalidraw.com/#json=YBaTDvWm8yTdshL8Z2IOL,7h7736K_Ey7wg08QljkbPg">https://excalidraw.com/#json=YBaTDvWm8yTdshL8Z2IOL,7h7736K_Ey7wg08QljkbPg</a></p>
]]></content:encoded></item><item><title><![CDATA[Asynchronous and reactive Dart (Flutter)]]></title><description><![CDATA[Recently I gave a talk at Google DevFest Georgetown 2024, about asynchronous Dart features (now with quizzes built in!). Here are the slides I promised:]]></description><link>https://yshean.com/asynchronous-and-reactive-dart-flutter</link><guid isPermaLink="true">https://yshean.com/asynchronous-and-reactive-dart-flutter</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[devfest]]></category><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Mon, 16 Dec 2024 13:11:27 GMT</pubDate><content:encoded><![CDATA[<p>Recently I gave a talk at <a target="_blank" href="https://gdg.community.dev/events/details/google-gdg-kuala-lumpur-presents-devfest-2024-kuala-lumpur/"><strong>Google DevFest Georgetown 2024</strong></a>, about asynchronous Dart features (now with quizzes built in!). Here are the slides I promised:</p>
<iframe src="https://docs.google.com/presentation/d/e/2PACX-1vTojUjvBR5HH093__ca-rtE1EJTzZVuJ9Rf1A5XNTjcijc0AMI8f-3s2gq297qaf0vykiFNNRQ3Lx9Z/embed?start=false&amp;loop=false&amp;delayms=3000" width="800" height="400"></iframe>]]></content:encoded></item><item><title><![CDATA[A quick look at async Dart features]]></title><description><![CDATA[Recently I gave a talk at Google DevFest Kuala Lumpur 2024, about asynchronous Dart features. Here are the slides:]]></description><link>https://yshean.com/a-quick-look-at-async-dart-features</link><guid isPermaLink="true">https://yshean.com/a-quick-look-at-async-dart-features</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[asynchronous]]></category><category><![CDATA[async]]></category><category><![CDATA[Streams]]></category><category><![CDATA[async/await]]></category><category><![CDATA[Futures]]></category><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Tue, 10 Dec 2024 02:22:35 GMT</pubDate><content:encoded><![CDATA[<p>Recently I gave a talk at <a target="_blank" href="https://gdg.community.dev/events/details/google-gdg-kuala-lumpur-presents-devfest-2024-kuala-lumpur/">Google DevFest Kuala Lumpur 2024</a>, about asynchronous Dart features. Here are the slides:</p>
<iframe src="https://docs.google.com/presentation/d/e/2PACX-1vS-nLLGDJkjm-ONk8dSyJ5HnvNuaFLnJ6ZoVsmg6xbZzHSZpzo3yt_sWZzNZ4XQPP9nBAgOn4XtJYoI/embed?start=false&amp;loop=false&amp;delayms=3000" width="800" height="400"></iframe>]]></content:encoded></item><item><title><![CDATA[Manage Flutter form states with Formz]]></title><description><![CDATA[I had the opportunity to speak about Flutter forms at Flutter KL and Google Cloud Munich meetup in October 2024, and thanks to the organisers and participants for making it great! Here are the slides:]]></description><link>https://yshean.com/manage-flutter-form-states-with-formz</link><guid isPermaLink="true">https://yshean.com/manage-flutter-form-states-with-formz</guid><category><![CDATA[formz]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[forms]]></category><category><![CDATA[Dart]]></category><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Sat, 26 Oct 2024 16:00:00 GMT</pubDate><content:encoded><![CDATA[<p>I had the opportunity to speak about Flutter forms at Flutter KL and Google Cloud Munich meetup in October 2024, and thanks to the organisers and participants for making it great! Here are the slides:</p>
<iframe src="https://docs.google.com/presentation/d/e/2PACX-1vSsqL9sRTzFZELlS9t2PX1EgW__h6o1heQ1U658LptnFOsoUPMC8QdgaeVjDZnzbLvUWH1plpyvBDfe/embed?start=false&amp;loop=false&amp;delayms=3000" width="800" height="400"></iframe>]]></content:encoded></item><item><title><![CDATA[How I use generative AI tools as a Flutter developer]]></title><description><![CDATA[Recently I have given a talk about how to improve developer productivity in building apps with Flutter and Generative AI (mainly Gemini). Here are the slides as promised:


If you have difficulty viewing the slides, click here to open in Google Slide...]]></description><link>https://yshean.com/developer-productivity-ai</link><guid isPermaLink="true">https://yshean.com/developer-productivity-ai</guid><category><![CDATA[Flutter]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[gemini]]></category><category><![CDATA[Dart]]></category><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Sat, 29 Jun 2024 05:49:42 GMT</pubDate><content:encoded><![CDATA[<p>Recently I have given a talk about how to improve developer productivity in building apps with Flutter and Generative AI (mainly Gemini). Here are the slides as promised:</p>
<iframe src="https://docs.google.com/presentation/d/1rC8Bk4T7eRqSOy-wom5_ZmXKIt4liAc2ACcwCWDB9b0/embed?start=false&amp;loop=false&amp;delayms=60000" width="960" height="569"></iframe>

<p>If you have difficulty viewing the slides, <a target="_blank" href="https://docs.google.com/presentation/d/1rC8Bk4T7eRqSOy-wom5_ZmXKIt4liAc2ACcwCWDB9b0/edit?usp=sharing">click here</a> to open in Google Slides.</p>
]]></content:encoded></item><item><title><![CDATA[The annoying Cocoapods error - A troubleshooting guide]]></title><description><![CDATA[I lost count of how many times I encountered a Cocoapods error whenever I build a Flutter app for iOS or macOS, especially through VS Code. It looks like this:
Warning: CocoaPods is installed but broken. Skipping pod install.
  You appear to have Coc...]]></description><link>https://yshean.com/the-annoying-cocoapods-error</link><guid isPermaLink="true">https://yshean.com/the-annoying-cocoapods-error</guid><category><![CDATA[cocoapods]]></category><category><![CDATA[iOS]]></category><category><![CDATA[macOS]]></category><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Thu, 14 Mar 2024 07:31:57 GMT</pubDate><content:encoded><![CDATA[<p>I lost count of how many times I encountered a Cocoapods error whenever I build a Flutter app for iOS or macOS, especially through VS Code. It looks like this:</p>
<pre><code class="lang-bash">Warning: CocoaPods is installed but broken. Skipping pod install.
  You appear to have CocoaPods installed but it is not working.
  This can happen <span class="hljs-keyword">if</span> the version of Ruby that CocoaPods was installed with is different from the one being used to invoke it.
  This can usually be fixed by re-installing CocoaPods. For more info, see https://github.com/flutter/flutter/issues/14293.
To re-install:
  sudo gem install cocoapods

CocoaPods not installed or not <span class="hljs-keyword">in</span> valid state.
</code></pre>
<p>The error message looks almost the same, even sometimes it was due to different reasons. I will share a couple ways that worked for me:</p>
<ol>
<li><p>Try to exit VS Code completely (not just reload) and start again, sometimes this is all it needs to work.</p>
</li>
<li><p>Otherwise, uninstall the Flutter extension, reload, reinstall the Flutter extension, reload again.</p>
</li>
<li><p>If it still does not work, uninstall Cocoapods by <code>sudo gem uninstall cocoapods</code> , then install again with <code>sudo gem install cocoapods</code> . Make sure you only have one version of Cocoapods installed.</p>
</li>
<li><p>Sometimes it is because of incompatible dependencies in your <code>Podfile</code>. In that case delete <code>Podfile.lock</code> , then run <code>pod install --repo-update</code> in the <code>ios</code> directory of your Flutter project.</p>
</li>
</ol>
<p>Hope this helps. I'm sure the future me will also visit this page from time to time 😅</p>
]]></content:encoded></item><item><title><![CDATA[Dio is your best friend (in Flutter projects)]]></title><description><![CDATA[Recently I have given a talk about the dio package for Google's International Women's Day celebration. Here are the slides as promised:]]></description><link>https://yshean.com/dio-is-your-best-friend-in-flutter-projects</link><guid isPermaLink="true">https://yshean.com/dio-is-your-best-friend-in-flutter-projects</guid><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Mon, 11 Mar 2024 07:52:32 GMT</pubDate><content:encoded><![CDATA[<p>Recently I have given a talk about the <a target="_blank" href="https://pub.dev/packages/dio">dio package</a> for <a target="_blank" href="https://gdg.community.dev/events/details/google-gdg-cloud-kl-presents-international-womens-day-2024-impact-the-future/cohost-gdg-cloud-kl">Google's International Women's Day celebration</a>. Here are the slides as promised:</p>
<iframe src="https://docs.google.com/presentation/d/e/2PACX-1vTiu6TiyX7c3UpMLC3io0oU3xgzwQoyofOUlTloJ9ofcbByLrFk1qYHkCzuNhtPrg/embed?start=false&amp;loop=false&amp;delayms=3000" width="960" height="569"></iframe>]]></content:encoded></item><item><title><![CDATA[How I handle errors in Flutter]]></title><description><![CDATA[I've seen many different approaches on handling errors in Flutter projects, some even involves functional programming paradigm... Here I will show my take on how I handle errors while taking care of internationalisation (nope I'm not using easy_local...]]></description><link>https://yshean.com/error-handling-in-flutter</link><guid isPermaLink="true">https://yshean.com/error-handling-in-flutter</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[error handling]]></category><category><![CDATA[exceptionhandling]]></category><category><![CDATA[BLoC]]></category><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Thu, 07 Mar 2024 16:06:35 GMT</pubDate><content:encoded><![CDATA[<p>I've seen many different approaches on handling errors in Flutter projects, some even involves functional programming paradigm... Here I will show my take on how I handle errors while taking care of internationalisation (nope I'm not using <code>easy_localization</code>, I'm using the official way, with <code>BuildContext</code>). In this article I may be using the term <em>error</em> and <em>exception</em> interchangeably, though there are <a target="_blank" href="https://www.geeksforgeeks.org/errors-v-s-exceptions-in-java/">some distinct differences between them</a>, so just be aware.</p>
<p>My tech stack in a Flutter project usually involves <code>bloc</code>, but I think this approach can also apply to other state management approaches. Here is how it looks at the presentation layer:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CategoriesPageGridView</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> CategoriesPageGridView({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> BlocBuilder&lt;CategoriesCubit, CategoriesState&gt;(
      builder: (context, state) =&gt; <span class="hljs-keyword">switch</span> (state) {
        CategoriesInitial() ||
        CategoriesLoading() =&gt;
          CategoriesPageGrid.loading(context),
        CategoriesError() =&gt; Center(
            child: Text(state.getMessage(context)),
          ),
        CategoriesLoaded() =&gt; CategoriesPageGrid(
            items: state.currentData!.map((category) {
              <span class="hljs-keyword">return</span> CategoryGridItem(
                category: category,
                onTap: () {},
              );
            }).toList(),
          ),
      },
    );
  }
}
</code></pre>
<p>The <code>state.getMessage(context)</code> function would return a <code>String</code> that contains the localised error message (e.g. <code>context.l10n.networkError</code>), and it would be rendered when the error state <code>CategoriesError</code> is emitted by the <code>CategoriesCubit</code>. Let's have a look how our <code>CategoriesCubit</code> and <code>CategoriesState</code> look:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CategoriesCubit</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Cubit</span>&lt;<span class="hljs-title">CategoriesState</span>&gt; </span>{
    CategoriesCubit({
        <span class="hljs-keyword">required</span> BooksRepository booksRepository,
    })  : _booksRepository = booksRepository,
            <span class="hljs-keyword">super</span>(<span class="hljs-keyword">const</span> CategoriesInitial(<span class="hljs-keyword">null</span>));

    <span class="hljs-keyword">final</span> BooksRepository _booksRepository;

    <span class="hljs-keyword">void</span> fetchCategories() <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">try</span> {
          emit(CategoriesLoading(state.currentData));
          <span class="hljs-keyword">final</span> categories = <span class="hljs-keyword">await</span> _booksRepository.fetchCategories();
          emit(CategoriesLoaded(categories));
        } <span class="hljs-keyword">on</span> AppError <span class="hljs-keyword">catch</span> (error) {
          emit(
            CategoriesError(
              getMessage: error.getMessage,
              currentData: state.currentData,
            ),
          );
        }
      }
    }
}
</code></pre>
<pre><code class="lang-dart">sealed <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CategoriesState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Equatable</span> </span>{
  <span class="hljs-keyword">const</span> CategoriesState(<span class="hljs-keyword">this</span>.currentData);

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Iterable</span>&lt;Category&gt;? currentData;

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">Object?</span>&gt; <span class="hljs-keyword">get</span> props =&gt; [currentData];
}

<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CategoriesInitial</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CategoriesState</span> </span>{
  <span class="hljs-keyword">const</span> CategoriesInitial(<span class="hljs-keyword">super</span>.currentData);
}

<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CategoriesLoading</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CategoriesState</span> </span>{
  <span class="hljs-keyword">const</span> CategoriesLoading(<span class="hljs-keyword">super</span>.currentData);
}

<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CategoriesLoaded</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CategoriesState</span> </span>{
  <span class="hljs-keyword">const</span> CategoriesLoaded(<span class="hljs-keyword">super</span>.currentData);
}

<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CategoriesError</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CategoriesState</span> </span>{
  <span class="hljs-keyword">const</span> CategoriesError({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.getMessage,
    <span class="hljs-keyword">required</span> <span class="hljs-built_in">Iterable</span>&lt;Category&gt;? currentData,
  }) : <span class="hljs-keyword">super</span>(currentData);

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> <span class="hljs-built_in">Function</span>(BuildContext) getMessage;

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">Object?</span>&gt; <span class="hljs-keyword">get</span> props =&gt; [getMessage, currentData];
}
</code></pre>
<p>Let's put the focus on the <code>CategoriesError</code> for now. It contains a <code>getMessage</code> function that returns a <code>String</code> with a provided <code>BuildContext</code> . From the <code>fetchCategories()</code> function in the <code>CategoriesCubit</code> , you can see that it is passing the <code>error.getMessage</code> function from <code>AppError</code> to the <code>CategoriesError</code> state. But what is this <code>AppError</code> and where does it come from?</p>
<p>Before I get into what is <code>AppError</code>, let's see who is throwing it and how was it thrown. In the <code>fetchCategories()</code> function, the culprit who can throw the <code>AppError</code> is <code>await _booksRepository.fetchCategories()</code> , so let's have a look at the repository:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BooksRepository</span> </span>{
  BooksRepository({
    <span class="hljs-keyword">required</span> BackendApiClient apiClient,
  }) : _apiClient = apiClient;

  <span class="hljs-keyword">final</span> BackendApiClient _apiClient;

  BooksApi <span class="hljs-keyword">get</span> _api =&gt; _apiClient.http.getBooksApi();

Future&lt;<span class="hljs-built_in">Iterable</span>&lt;Category&gt;&gt; fetchCategories() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> _api.getCategories();
      <span class="hljs-keyword">return</span> response.data!;
    } <span class="hljs-keyword">catch</span> (error, stackTrace) {
      AppError.throwWithStackTrace(FetchCategoriesException(error), stackTrace);
    }
  }
}
</code></pre>
<p>As you can see, an <code>AppError</code> is being thrown at a repository method whenever an exception is caught. Ideally, I will do the same for every repository method so that I can transform those exceptions into <code>AppError</code> before it was caught by the callers (in our case, it's usually the blocs/cubits).</p>
<p>And if you're wondering, here is the definition of <code>FetchCategoriesException</code> :</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FetchCategoriesException</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CustomException</span> </span>{
  <span class="hljs-keyword">const</span> FetchCategoriesException(<span class="hljs-keyword">super</span>.error);

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">String</span> getMessage(BuildContext context) {
    <span class="hljs-keyword">return</span> context.l10n.failedToLoadCategoriesErrorMessage;
  }
}
</code></pre>
<p>Wait... what is this <code>CustomException</code> ?? Let me explain together with the <code>AppError</code> class:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppError</span> </span>{
  AppError({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.error,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.getMessage,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Object</span> error;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> <span class="hljs-built_in">Function</span>(BuildContext) getMessage;

  <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> <span class="hljs-built_in">Function</span>(BuildContext)? _getClientErrorMessage(<span class="hljs-built_in">Object</span> error) {
    <span class="hljs-keyword">switch</span> (error) {
      <span class="hljs-keyword">case</span> HandshakeException:
      <span class="hljs-keyword">case</span> HttpException:
      <span class="hljs-keyword">case</span> SocketException:
        <span class="hljs-keyword">return</span> (context) =&gt; context.l10n.networkErrorMessage;

      <span class="hljs-keyword">case</span> <span class="hljs-keyword">final</span> DioException e:
        <span class="hljs-keyword">switch</span> (e.error) {
          <span class="hljs-keyword">case</span> <span class="hljs-built_in">Object</span> _ when e.type == DioExceptionType.connectionTimeout:
          <span class="hljs-keyword">case</span> <span class="hljs-built_in">Object</span> _ when e.type == DioExceptionType.connectionError:
          <span class="hljs-keyword">case</span> SocketException:
            <span class="hljs-keyword">return</span> (context) =&gt; context.l10n.networkErrorMessage;
        }
        <span class="hljs-keyword">switch</span> (e.response) {
          <span class="hljs-keyword">case</span> <span class="hljs-built_in">Object</span> _ when e.response?.statusCode == <span class="hljs-number">401</span>:
            <span class="hljs-keyword">final</span> isTokenSentInRequest =
                e.requestOptions.headers[HttpHeaders.authorizationHeader] !=
                    <span class="hljs-keyword">null</span>;
            <span class="hljs-keyword">if</span> (isTokenSentInRequest) {
              <span class="hljs-keyword">throw</span> SessionExpiredError(e);
            }
            <span class="hljs-keyword">throw</span> UnauthorizedError(e);

          <span class="hljs-keyword">case</span> <span class="hljs-built_in">Object</span> _ when e.response?.statusCode == <span class="hljs-number">404</span>:
            <span class="hljs-keyword">return</span> (context) =&gt; context.l10n.serverUnavailableErrorMessage;

          <span class="hljs-comment">// additional cases here if you want to parse server responses</span>
          <span class="hljs-comment">// e.g. server validation errors</span>
        }
      <span class="hljs-keyword">default</span>:
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>; <span class="hljs-comment">// leave it to CustomException to fill this up</span>
    }
  }

  <span class="hljs-keyword">static</span> <span class="hljs-built_in">Never</span> throwWithStackTrace(<span class="hljs-built_in">Object</span> error, StackTrace stackTrace) {
    <span class="hljs-keyword">final</span> getMessage =
        _getClientErrorMessage(error <span class="hljs-keyword">is</span> CustomException ? error.error : error);
    Error.throwWithStackTrace(
      AppError(
        error: error <span class="hljs-keyword">is</span> CustomException ? error.error : error,
        getMessage: getMessage ?? (context) =&gt; context.l10n.defaultErrorMessage,
      ),
      stackTrace,
    );
  }
}
</code></pre>
<p>It looks quite lengthy, because I was trying to map the exceptions thrown by both <code>http</code> and <code>dio</code> package to a user-friendly error message, plus wrapping the default <code>Error.throwWithStackTrace(...)</code> with our own so the catcher would receive the <code>getMessage</code> function that then allows our error messages to be displayed in translated languages.</p>
<p>Now there are still two missing pieces - what is <code>CustomException</code> and why were <code>SessionExpiredError</code> and <code>UnauthorizedError</code> thrown?</p>
<pre><code class="lang-dart"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomException</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Exception</span> </span>{
  <span class="hljs-keyword">const</span> CustomException(<span class="hljs-keyword">this</span>.error);

  <span class="hljs-comment">/// <span class="markdown">The error which was caught.</span></span>
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Object</span> error;

  <span class="hljs-comment">/// <span class="markdown">User-friendly message to show.</span></span>
  <span class="hljs-built_in">String</span> getMessage(BuildContext context);
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UnauthorizedError</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CustomException</span> </span>{
  <span class="hljs-keyword">const</span> UnauthorizedError(<span class="hljs-built_in">Object</span> error) : <span class="hljs-keyword">super</span>(error);

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">String</span> getMessage(BuildContext context) {
    <span class="hljs-keyword">return</span> context.l10n.unauthorizedErrorMessage;
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SessionExpiredError</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CustomException</span> </span>{
  <span class="hljs-keyword">const</span> SessionExpiredError(<span class="hljs-built_in">Object</span> error) : <span class="hljs-keyword">super</span>(error);

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">String</span> getMessage(BuildContext context) {
    <span class="hljs-keyword">return</span> context.l10n.sessionExpiredErrorMessage;
  }
}
</code></pre>
<p>Turned out <code>CustomException</code> is just a base class I created, that implements the <code>Exception</code> class, so that it contains a localised message getter <code>getMessage(BuildContext context)</code>. The <code>UnauthorizedError</code> and <code>SessionExpiredError</code> were created the same way as the <code>FetchCategoriesException</code> .</p>
<p>These two errors were intentionally thrown by <code>AppError</code> because I intend to handle them at the global level:</p>
<pre><code class="lang-dart">PlatformDispatcher.instance.onError = (error, stack) {
    <span class="hljs-keyword">if</span> (error <span class="hljs-keyword">is</span> UnauthorizedError) {
      appBloc.add(<span class="hljs-keyword">const</span> UnauthorizedEvent());
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (error <span class="hljs-keyword">is</span> SessionExpiredError) {
      appBloc.add(<span class="hljs-keyword">const</span> SessionExpiredEvent());
    } <span class="hljs-keyword">else</span> {
      log(<span class="hljs-string">'🔥 <span class="hljs-subst">$error</span>'</span>, stackTrace: stack);
      <span class="hljs-comment">// or call Sentry for error reporting</span>
    }
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
  };
</code></pre>
<p>Tada! I let the <code>PlatformDispatcher</code> catch the two uncaught errors so that I can add the corresponding event to my top-level <code>AppBloc</code> , which is responsible for handling top-level events in the whole app. I can then use a <code>BlocListener</code> to show a snackbar, dialog, and/or perform navigations. The possibilities are endless.</p>
<p>So far this is working pretty well for me... What do you think? Share your thoughts and maybe your approach with me in the comments :)</p>
]]></content:encoded></item><item><title><![CDATA[Using the OpenAPI generator Gradle plugin]]></title><description><![CDATA[I'm not a fan of Gradle, but for enterprise projects sometimes you need to use them. There are many choices of installing the OpenAPI generator, why among all did I choose to use the Gradle plugin?
Well, the reason for me is when using the Gradle plu...]]></description><link>https://yshean.com/using-the-openapi-generator-gradle-plugin</link><guid isPermaLink="true">https://yshean.com/using-the-openapi-generator-gradle-plugin</guid><category><![CDATA[gradle]]></category><category><![CDATA[OpenApi]]></category><category><![CDATA[generators]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Fri, 01 Mar 2024 10:00:31 GMT</pubDate><content:encoded><![CDATA[<p>I'm not a fan of <a target="_blank" href="https://gradle.org/">Gradle</a>, but for enterprise projects sometimes you need to use them. There are many choices of installing the <a target="_blank" href="https://github.com/OpenAPITools/openapi-generator">OpenAPI generator</a>, why among all did I choose to use the <a target="_blank" href="https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator-gradle-plugin/README.adoc">Gradle plugin</a>?</p>
<p>Well, the reason for me is when using the Gradle plugin, the configurations can be contained in a <code>build.gradle</code> file, which is easy to be versioned. Think of Gradle as a powerful CLI, you can use it for other tasks that you'd normally need to write a script for that. The downside? The documentation can be a bit confusing (both OpenAPI and Gradle itself), I had to read multiple times and in multiple articles to understand what it does and how it works.</p>
<p>Another good thing? You can use Gradle for projects of any tech stack - in my case I usually use for Flutter projects. Hence in this article I would show the configurations I apply for my Flutter projects.</p>
<p>Pre-requisites:</p>
<ul>
<li><p>Java JDK version 8 or higher</p>
</li>
<li><p><a target="_blank" href="https://gradle.org/install/">Install Gradle</a> (no actually you don't need Gradle installed everywhere you need to run the script, but we do need Gradle to generate the Gradle wrapper, which is used to distribute Gradle binary that can run our OpenAPI plugin)</p>
</li>
</ul>
<p>What you'd learn by the end of this guide:</p>
<ul>
<li><p>Generate <a target="_blank" href="https://www.baeldung.com/gradle-wrapper">Gradle wrapper</a> (a one-time task for each project)</p>
</li>
<li><p>Create <code>build.gradle</code> file in your project</p>
</li>
<li><p>Run the gradle task defined in <code>build.gradle</code></p>
</li>
</ul>
<h1 id="heading-generate-gradle-wrapper">Generate Gradle Wrapper</h1>
<p>This is easy. Run <code>gradle wrapper</code> and a couple of files will be generated for you: <code>gradlew</code> and <code>gradlew.bat</code>. <code>gradlew</code> will be used to execute gradle tasks in Unix systems, and <code>gradlew.bat</code> on Windows machines. Generally you would want to check these into the source control system in your project.</p>
<h1 id="heading-create-buildgradle">Create <code>build.gradle</code></h1>
<p>If you don't have an existing <code>build.gradle</code> at the root of your project (not the <code>build.gradle</code> inside the <code>android</code> folder nor the one inside the <code>android/app</code> folder), create one. And then paste the following content:</p>
<pre><code class="lang-json">plugins {
  id <span class="hljs-attr">"org.openapi.generator"</span> version <span class="hljs-attr">"7.3.0"</span>
}

openApiGenerate {
    <span class="hljs-comment">// replace remoteInputSpec with inputSpec</span>
    <span class="hljs-comment">// if the api.yaml file is not remotely hosted</span>
    remoteInputSpec.set(&lt;url_to_api_yaml&gt;)
    <span class="hljs-comment">// The name of the generator which will handle codegen.</span>
    generatorName = 'dart-dio'
    <span class="hljs-comment">// The output target directory into which code will be generated.</span>
    outputDir = <span class="hljs-attr">"packages/backend_api"</span>
    <span class="hljs-comment">// Sets specified global properties.</span>
    globalProperties = [
        browserClient: '<span class="hljs-literal">false</span>'
        hideGenerationTimestamp: '<span class="hljs-literal">true</span>'
    ]
    <span class="hljs-comment">// Defines whether or not model-related test files should be generated.</span>
    generateModelTests = <span class="hljs-literal">false</span>
    <span class="hljs-comment">// Defines whether or not api-related test files should be generated.</span>
    generateApiTests = <span class="hljs-literal">false</span>
    <span class="hljs-comment">// Defines whether or not model-related documentation files should be generated.</span>
    generateModelDocumentation = <span class="hljs-literal">false</span>
    <span class="hljs-comment">// Defines whether or not api-related documentation files should be generated.</span>
    generateApiDocumentation = <span class="hljs-literal">false</span>
    <span class="hljs-comment">// A map of options specific to a generator.</span>
    <span class="hljs-comment">// see https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/dart-dio.md#config-options</span>
    <span class="hljs-comment">// for the full set of available options</span>
    configOptions = [
        <span class="hljs-comment">// Name in generated pubspec</span>
        pubName: 'backend_api'
    ]
    <span class="hljs-comment">// see https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator-gradle-plugin/README.adoc#openapigenerate</span>
    <span class="hljs-comment">// for the full set of available options</span>
}
</code></pre>
<p>Once this is saved, we can proceed to the final step - run it!</p>
<h1 id="heading-run-the-openapi-generator">Run the OpenAPI generator</h1>
<p>Now we are ready to execute it. Running <code>./gradlew openApiGenerate</code> (or <code>./gradlew.bat openApiGenerate</code> in Windows) will execute the OpenAPI generator task defined in the <code>build.gradle</code> . If everything goes well you will see a generated package in the specified <code>outputDir</code>.</p>
<h1 id="heading-alternative-way-if-you-dont-want-to-use-gradle">Alternative way if you don't want to use Gradle</h1>
<p>If you have read through the above steps and you still can't wrap your head over the trouble of maintaining a Gradle file, it is worth mentioning that you can download the <code>.jar</code> binary directly and then execute in your terminal (you still need Java, of course):</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

java -jar openapi-generator-cli.jar generate \ 
            -i ./api.yaml \
            -g dart-dio \ 
            -o ./packages/backend_api \
            --additional-properties hideGenerationTimestamp=<span class="hljs-literal">true</span>,browserClient=<span class="hljs-literal">false</span> \
            <span class="hljs-comment"># other properties ...</span>
</code></pre>
<p>Some people prefer using this way, so I just leave the option open, feel free to choose whichever option that suits you and your team.</p>
]]></content:encoded></item><item><title><![CDATA[The missing guide to deep linking in Flutter apps - Part 2, iOS]]></title><description><![CDATA[If you've missed the first part, you can read here.
What you need to do:

Adjust Info.plist

Add "Associated Domains" capability to each bundle ID

Host the apple-app-site-association file

Test with a simulator or a real device, or via the XCode CLI...]]></description><link>https://yshean.com/deep-linking-in-flutter-apps-part-2</link><guid isPermaLink="true">https://yshean.com/deep-linking-in-flutter-apps-part-2</guid><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[DeepLinking]]></category><category><![CDATA[universallinks]]></category><category><![CDATA[iOS]]></category><category><![CDATA[Mobile Development]]></category><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Thu, 29 Feb 2024 13:00:38 GMT</pubDate><content:encoded><![CDATA[<p>If you've missed the first part, you can <a target="_blank" href="https://yshean.com/deep-linking-in-flutter-apps-part-1">read here</a>.</p>
<p>What you need to do:</p>
<ul>
<li><p>Adjust <code>Info.plist</code></p>
</li>
<li><p>Add "Associated Domains" capability to each bundle ID</p>
</li>
<li><p>Host the <code>apple-app-site-association</code> file</p>
</li>
<li><p>Test with a simulator or a real device, or via the XCode CLI</p>
</li>
</ul>
<h3 id="heading-edit-4-march-2023-additional-info-from-reddit-comment">Edit 4 March 2023: Additional info from Reddit comment</h3>
<p>User <a target="_blank" href="https://www.reddit.com/user/walker_Jayce/">walker_Jayce</a> shared his gotchas when he was implementing deeplink, which I have now included in this article - <a target="_blank" href="https://www.reddit.com/r/FlutterDev/comments/1b32yl1/comment/kt87spn/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button">see original comment here</a>. Do check out <a target="_blank" href="https://github.com/DanWlker/flutter_concepts">his other Flutter notes on Github</a> too.</p>
<h1 id="heading-adjust-infoplist">Adjust <code>Info.plist</code></h1>
<p>Overall, the setup for iOS has relatively less quirks and <a target="_blank" href="https://docs.flutter.dev/cookbook/navigation/set-up-universal-links">the official guide</a> covers almost everything you need. However, when you are updating the <code>Info.plist</code> file, take note if you are using <code>app_links</code> package you should <strong>not</strong> include the <code>FlutterDeepLinkingEnabled</code> property.</p>
<h1 id="heading-add-associated-domains-capability-to-each-bundle-id">Add "Associated Domains" capability to each bundle ID</h1>
<p><a target="_blank" href="https://docs.flutter.dev/cookbook/navigation/set-up-universal-links#2-adjust-ios-build-settings">Follow the official guide</a>. Apart from setting the associated domain(s) in XCode, you should also remember to set the same in <a target="_blank" href="https://developer.apple.com/">Apple Developer Portal</a> (Certificates, Identifiers &amp; Profiles &gt; Identifiers &gt; Select an identifier &gt; Capabilities)<strong>.</strong></p>
<h1 id="heading-host-the-apple-app-site-association-file">Host the <code>apple-app-site-association</code> file</h1>
<p>Similar to <code>assetlinks.json</code> , you would also need to host the Apple-equivalent file on your web server. However, unlike Android, <a target="_blank" href="https://stackoverflow.com/a/42190104">iOS universal links must be in https, which some internal test servers may not be</a>.</p>
<p>The directory to host is the same (<code>.well-known</code>), only the file name is different: <code>&lt;webdomain&gt;/.well-known/apple-app-site-association</code> . You can apply the same tip to host it with Firebase Hosting if you don't already have a web server and update the web domain accordingly.</p>
<p>You can also associate a single web domain with multiple app IDs, e.g. when you have different flavours of the app, like so:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"applinks"</span>: {
      <span class="hljs-attr">"apps"</span>: [],
      <span class="hljs-attr">"details"</span>: [
      {
        <span class="hljs-attr">"appID"</span>: <span class="hljs-string">"&lt;team_id&gt;.com.example.myapp.dev"</span>,
        <span class="hljs-attr">"paths"</span>: [<span class="hljs-string">"*"</span>]
      },
      {
        <span class="hljs-attr">"appID"</span>: <span class="hljs-string">"&lt;team_id&gt;.com.example.myapp.qa"</span>,
        <span class="hljs-attr">"paths"</span>: [<span class="hljs-string">"*"</span>]
      },
      {
        <span class="hljs-attr">"appID"</span>: <span class="hljs-string">"&lt;team_id&gt;.com.example.myapp"</span>,
        <span class="hljs-attr">"paths"</span>: [<span class="hljs-string">"*"</span>]
      }
    ]
  }
}
</code></pre>
<p>You can configure the domain association to only match a specific path of the web domain, especially when you only want a certain paths to be redirected to your app. It can also support advanced matching, <a target="_blank" href="https://developer.apple.com/documentation/xcode/supporting-associated-domains">as showcased here</a>.</p>
<h1 id="heading-testing-if-it-works">Testing if it works</h1>
<p>Similar to that in Android, make sure you have reinstalled the app after you have done the above steps, you can type a URL in a separate app (e.g. in a Note app, or send an email to yourself, or type in a Google Doc) and then tap on the URL (note: typing or pasting the URL in the browser would not work!). It should navigate to the app and open the specific screen correspondingly.</p>
<p>Alternatively, you can run this command to trigger: <code>xcrun simctl openurl booted https://&lt;web domain&gt;/details</code> . There is also a warning in the official guide that it might take up to 24 hours before Apple’s <a target="_blank" href="https://en.wikipedia.org/wiki/Content_delivery_network">Content Delivery Network (CDN) requests the appl</a>e-app-site-association (AASA) file from your web domain, so the app link won’t work until the CDN requests the file.</p>
<p>Here are some additional troubleshooting tips (thanks <a target="_blank" href="https://www.reddit.com/user/walker_Jayce/">walker_Jayce</a>!):</p>
<ol>
<li><p>When testing, check if your link ends with a <code>.</code>, if yes you will have to use apps like <a target="_blank" href="https://apps.apple.com/my/app/deeplink-checker/id6448803970">this</a> and <a target="_blank" href="https://play.google.com/store/apps/details?id=com.app.deeplinktester">this</a> to launch the links, since most text editors will not include the <code>.</code> when clicking and open the link.</p>
</li>
<li><p>If you would like to test deep links on a server not reachable by Apple CDN, you can use Associated Domains Development to force your device to fetch the json file directly. <a target="_blank" href="https://developer.apple.com/videos/play/wwdc2020/10098">Apple's video discussing this, start at 18:22</a></p>
<ol>
<li><p>Enable Developer Mode (Settings &gt; Privacy &amp; Security &gt; Developer Mode)</p>
</li>
<li><p>Enable Associated Domains Development (Settings &gt; Developer &gt; Section: Universal Links &gt; Associated Domains Development) and insert internal link in Diagnostics to fetch the apple-app-site-association file. (the Developer section is quite low, so scroll lower, should be just before your 3rd party apps section)</p>
</li>
</ol>
</li>
<li><p><a target="_blank" href="https://mac6classi.medium.com/universal-links-issue-on-ios-14-fd0aa8ae75f8">Guide to troubleshoot deeplinks</a></p>
</li>
<li><p>You can check if your file is hosted correctly on Apple's CDN by launching this link, replace <code>&lt;your website link&gt;</code> with your own link: <a target="_blank" href="https://app-site-association.cdn-apple.com/a/v1/"><code>https://app-site-association.cdn-apple.com/a/v1/</code></a><code>&lt;your website link&gt;</code></p>
</li>
<li><p>If your rootViewController is not a <code>FlutterViewController</code> (if you somehow overwritten it in your <code>AppDelegate.swift</code> file), you may need to <a target="_blank" href="https://github.com/DanWlker/flutter_concepts/blob/master/other_readmes/deep_link_troubleshooting.md">manually add code</a> to capture the request from iOS side.</p>
</li>
</ol>
<p>Alright, that's all for the deep linking. Let me know if I missed something, leave a comment if you find this kind of guide useful, so I'd be motivated to write more :D</p>
]]></content:encoded></item><item><title><![CDATA[The missing guide to deep linking in Flutter apps - Part 1, Android]]></title><description><![CDATA[This guide is meant to complement other resources out there. I will include the links to those resources useful to me in this article, but I would try not to repeat what was already out there (unless it was confusing).
What you can achieve by the end...]]></description><link>https://yshean.com/deep-linking-in-flutter-apps-part-1</link><guid isPermaLink="true">https://yshean.com/deep-linking-in-flutter-apps-part-1</guid><category><![CDATA[Flutter]]></category><category><![CDATA[DeepLinking]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[mobile app deep linking]]></category><category><![CDATA[Dart]]></category><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Thu, 29 Feb 2024 12:00:54 GMT</pubDate><content:encoded><![CDATA[<p>This guide is meant to complement other resources out there. I will include the links to those resources useful to me in this article, but I would try not to repeat what was already out there (unless it was confusing).</p>
<p>What you can achieve by the end of this guide:</p>
<ul>
<li><p>Tapping on a link like <code>https://your-domain.com/details</code> would lead to opening a specific screen of your app.</p>
</li>
<li><p>If you are looking to use a custom scheme like <code>myscheme123://details</code> then this guide is not for you. You may check out the <a target="_blank" href="https://pub.dev/packages/app_links"><code>app_links</code></a> package.</p>
</li>
</ul>
<p>What you need to do:</p>
<ul>
<li><p>Configure the routes in your Flutter project</p>
</li>
<li><p>Configure <code>AndroidManifest.xml</code></p>
</li>
<li><p>Host <code>assetlinks.json</code> file on your web server (for <code>http</code>/<code>https</code> scheme)</p>
</li>
<li><p>Test it with <code>adb</code> commands (you can already validate your setup with <code>adb</code> commands, even without hosting the file)</p>
</li>
</ul>
<p>What you need to know:</p>
<ul>
<li><p>There are two ways to configure deep linking or app linking in Flutter: <code>go_router</code> or <code>app_links</code> (or <code>uni_links</code>), each of them has their specific behaviour and setups (see section below)</p>
</li>
<li><p>The fingerprints in the <code>assetlinks.json</code> file matter - this would determine whether tapping on your custom link will automatically open your app</p>
</li>
</ul>
<p>Now let's get started.</p>
<h1 id="heading-gorouterhttpspubdevpackagesgorouter-or-applinkshttpspubdevpackagesapplinks-or-both"><a target="_blank" href="https://pub.dev/packages/go_router"><code>go_router</code></a> or <a target="_blank" href="https://pub.dev/packages/app_links"><code>app_links</code></a> ? (or both)</h1>
<p>This depends on your use case. I like how easy it is to set up with <code>go_router</code> , but it comes with a huge limitation (which is often critical to me). So when you use <code>go_router</code>, you <a target="_blank" href="https://docs.flutter.dev/cookbook/navigation/set-up-app-links#1-customize-a-flutter-application">set up the <code>GoRouter</code> instance as usual</a>, plus <a target="_blank" href="https://docs.flutter.dev/cookbook/navigation/set-up-app-links#2-modify-androidmanifestxml">a one-line setting in <code>AndroidManifest.xml</code></a> and then the deep linking setup on the Flutter part is done - <em>however</em>, the underlying behaviour of the navigation with this approach is equivalent to <code>context.go(&lt;path&gt;)</code>, which means <a target="_blank" href="https://github.com/flutter/flutter/issues/134373">you lose your navigation stack the moment you're navigated to the <code>&lt;path&gt;</code> you specified</a>. Which is often a dealbreaker for me, ugh. (See <a target="_blank" href="https://codewithandrea.com/articles/flutter-navigation-gorouter-go-vs-push/">this article</a> for the difference between <code>go</code> and <code>push</code>, though I don't agree with the author saying we should avoid using <code>push</code> as much as possible, <code>push</code> is still useful for mobile-only apps, and some cases like dialogs)</p>
<p>For setup with <code>app_links</code>, it's a bit more complicated, <a target="_blank" href="https://pub.dev/packages/app_links">see here</a> for detailed setup. I would avoid <code>uni_links</code> and Firebase Dynamic Links by the way - <code>uni_links</code> has not been updated since two years ago, and <a target="_blank" href="https://firebase.google.com/support/dynamic-links-faq">Firebase Dynamic Links is officially deprecated</a>. Here is my setup:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// At a top-level widget</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">App</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
    <span class="hljs-keyword">const</span> App({<span class="hljs-keyword">super</span>.key});

    <span class="hljs-meta">@override</span>
    State&lt;App&gt; createState() =&gt; _AppState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_AppState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">App</span>&gt; </span>{
    <span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> AppLinks _appLinks;
    StreamSubscription&lt;<span class="hljs-built_in">String</span>&gt;? _linkSubscription;

    <span class="hljs-meta">@override</span>
    <span class="hljs-keyword">void</span> initState() {
        _initDeepLinks();
        <span class="hljs-keyword">super</span>.initState();
    }

    <span class="hljs-meta">@override</span>
    <span class="hljs-keyword">void</span> dispose() {
        _linkSubscription?.cancel();
        <span class="hljs-keyword">super</span>.dispose();
    }

    Future&lt;<span class="hljs-keyword">void</span>&gt; _initDeepLinks() <span class="hljs-keyword">async</span> {
        _appLinks = AppLinks();
        _linkSubscription = _appLinks.allStringLinkStream.listen((url) {
            router.push(url); <span class="hljs-comment">// with your GoRouter instance</span>
        });
    }

    <span class="hljs-meta">@override</span>
    Widget build(BuildContext context) {
        <span class="hljs-keyword">return</span> MaterialApp.router(
            routerConfig: router,
            builder: ...        
        );
    }
}
</code></pre>
<p>In my projects I usually use both <code>go_router</code> and <code>app_links</code> - because I like the declarative routing API in <code>go_router</code>, and <code>app_links</code> I need for achieving <code>context.push(&lt;my_deeplink_path&gt;)</code> . (I really wish <code>go_router</code> could let us customise this behaviour so I can get rid of the <code>app_links</code> dependency...)</p>
<h1 id="heading-configure-androidmanifestxml">Configure <code>AndroidManifest.xml</code></h1>
<p>This is the key component to configure for Android deep links to work. Here is what the <a target="_blank" href="https://docs.flutter.dev/cookbook/navigation/set-up-app-links">official Flutter guide</a> ask you to do:</p>
<pre><code class="lang-xml"> <span class="hljs-tag">&lt;<span class="hljs-name">meta-data</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"flutter_deeplinking_enabled"</span> <span class="hljs-attr">android:value</span>=<span class="hljs-string">"true"</span> /&gt;</span>
 <span class="hljs-tag">&lt;<span class="hljs-name">intent-filter</span> <span class="hljs-attr">android:autoVerify</span>=<span class="hljs-string">"true"</span>&gt;</span>
     <span class="hljs-tag">&lt;<span class="hljs-name">action</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.action.VIEW"</span> /&gt;</span>
     <span class="hljs-tag">&lt;<span class="hljs-name">category</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.category.DEFAULT"</span> /&gt;</span>
     <span class="hljs-tag">&lt;<span class="hljs-name">category</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.category.BROWSABLE"</span> /&gt;</span>
     <span class="hljs-tag">&lt;<span class="hljs-name">data</span> <span class="hljs-attr">android:scheme</span>=<span class="hljs-string">"http"</span> <span class="hljs-attr">android:host</span>=<span class="hljs-string">"example.com"</span> /&gt;</span>
     <span class="hljs-tag">&lt;<span class="hljs-name">data</span> <span class="hljs-attr">android:scheme</span>=<span class="hljs-string">"https"</span> /&gt;</span>
 <span class="hljs-tag">&lt;/<span class="hljs-name">intent-filter</span>&gt;</span>
</code></pre>
<p>Put this under the main <code>&lt;Activity&gt;</code> (you should only have one <code>Activity</code> for your Flutter app anyway), replace the <code>android:host</code> value accordingly, and you're good to go. <strong>Note that if you are relying on</strong> <code>app_links</code> <strong>to do the navigation you should remove the line with the</strong> <code>flutter_deeplinking_enabled</code> <strong>.</strong> The <code>autoVerify=true</code> is to make sure that Android will automatically try to verify your app with the host you specified, so that the app association with the host is established. Users can still manually override this, of course. You can later check that behaviour in your Android device / emulator when you have the app installed after this is configured (select your app, go to "App Info", then go to "Open by default"):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709199132022/6b212321-cd51-420e-b03c-e7cd17ae76a8.png" alt="Select your app &gt; App info &gt; Open by default" class="image--center mx-auto" /></p>
<p>Only after completing all the steps in this guide you would see your domain being specified in the "verified links". You can manually add link, of course, but you cannot expect your users to do that. So be patient and follow through the rest of the setup.</p>
<p>Side note: The default <code>android:launchMode</code> of Flutter activity is <code>singleTop</code> , however, as <a target="_blank" href="https://pub.dev/packages/app_links#getting-started">pointed out here</a>, opening the app link would end up triggering another instance of your app, which is sub-optimal. Setting the launch mode to <code>singleInstance</code> is better, plus avoiding <a target="_blank" href="https://blog.dixitaditya.com/android-task-hijacking?x-host=blog.dixitaditya.com">task hijacking</a> altogether when you also set <code>android:taskAffinity=""</code> in the <code>&lt;application&gt;</code> tag.</p>
<h1 id="heading-create-assetlinksjson-and-host-it-on-your-web-server">Create <code>assetlinks.json</code> and host it on your web server</h1>
<p><a target="_blank" href="https://docs.flutter.dev/cookbook/navigation/set-up-app-links#3-hosting-assetlinksjson-file">Steps listed here</a> is good enough, but there are a few things worth mentioning:</p>
<ul>
<li>The SHA256 fingerprints from the Google Play Developer Console is good enough for builds that are released via Google Play. This is due to the fact that the fingerprints are generated from the keystore used to sign the APK - you might be relying on Google Play's own provided keystore, or you may have uploaded your own. Either way, if you also want to test locally in the debug mode, you need to also add the fingerprint of your debug keystore (often located in <code>~/.android/debug.keystore</code> in Mac systems) into the <code>assetlinks.json</code> file. To get the signature of your keystore, run this in your terminal: <code>keytool -list -v -keystore &lt;path to your keystore&gt;</code> .</li>
</ul>
<p>You can use a single <code>assetlinks.json</code> file to support multiple app bundles, e.g. if you have multiple app flavours, you can do:</p>
<pre><code class="lang-xml">[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp.dev",
      "sha256_cert_fingerprints": [
        "<span class="hljs-tag">&lt;<span class="hljs-name">fingerprint_one</span>&gt;</span>",
        "<span class="hljs-tag">&lt;<span class="hljs-name">fingerprint_two</span>&gt;</span>"
      ]
    }
  },
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp.qa",
      "sha256_cert_fingerprints": [
        "<span class="hljs-tag">&lt;<span class="hljs-name">fingerprint_one</span>&gt;</span>",
        "<span class="hljs-tag">&lt;<span class="hljs-name">fingerprint_two</span>&gt;</span>"
      ]
    }
  },
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp",
      "sha256_cert_fingerprints": [
        "<span class="hljs-tag">&lt;<span class="hljs-name">fingerprint_one</span>&gt;</span>",
        "<span class="hljs-tag">&lt;<span class="hljs-name">fingerprint_two</span>&gt;</span>"
      ]
    }
  }
]
</code></pre>
<ul>
<li><p>Allow some time after reinstalling the app for Google to pick up the association. Google suggests <a target="_blank" href="https://developer.android.com/training/app-links/verify-android-applinks#auto-verification">waiting at least 20 seconds</a> for the asynchronous verification to complete.</p>
</li>
<li><p>Tips if you don't have a web server: You can use <a target="_blank" href="https://firebase.google.com/docs/hosting">Firebase Hosting</a> and upload the <code>assetlinks.json</code> as a static file (make sure the final path is <code>.well-known/assetlinks.json</code> and the domain is what you created in Firebase Hosting).</p>
</li>
</ul>
<h1 id="heading-test-with-adb-commands">Test with ADB commands</h1>
<p>Now if you have done everything right, you can type a URL in a separate app (e.g. in a Note app, or send an email to yourself, or type in a Google Doc) and then tap on the URL (note: typing or pasting the URL in the browser would not work!), you will see the auto-navigation to the specific screen of your app. But, if something is not right, you can debug and test with <code>adb</code> commands:</p>
<ul>
<li><p><code>adb shell 'am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://dev.example.com/product-details"' com.example.myapp.dev</code> to test if it opens the correct screen of your app (would still work even if the host association is not done)</p>
</li>
<li><p><code>adb shell dumpsys package com.example.myapp.dev</code> to check everything about this package including the host association and auto-verification status</p>
</li>
</ul>
<p>And that is it! I will write about the setup for iOS in the next part.</p>
]]></content:encoded></item><item><title><![CDATA[Welcome to my dev notes]]></title><description><![CDATA[I would like to use this space to record my learning from time to time, mostly software development related. My current day-to-day activities revolve around Flutter, Dart, and AI (because, FOMO). I like to keep my notes short and simple, and to the p...]]></description><link>https://yshean.com/welcome-to-my-dev-notes</link><guid isPermaLink="true">https://yshean.com/welcome-to-my-dev-notes</guid><dc:creator><![CDATA[Yong Shean]]></dc:creator><pubDate>Wed, 31 Jan 2024 10:37:19 GMT</pubDate><content:encoded><![CDATA[<p>I would like to use this space to record my learning from time to time, mostly software development related. My current day-to-day activities revolve around Flutter, Dart, and AI (because, FOMO). I like to keep my notes short and simple, and to the point without beating around the bush (you could generate the details with AI nowadays anyways). Stay tuned :)</p>
]]></content:encoded></item></channel></rss>