<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged flutterdev</title>
<link>https://coderlegion.com/tag/flutterdev</link>
<description>Powered by Question2Answer</description>
<item>
<title>Automate Unused Asset Cleanup in Flutter</title>
<link>https://coderlegion.com/26143/automate-unused-asset-cleanup-in-flutter</link>
<description>&lt;p&gt;Let's be realistic, every Flutter project accumulates dead weight over time, big images not deleted, a logo variant from a rebrand, a placeholder image a dev forgot to delete.&lt;br&gt;
Individually they're small but they add up to real APK/IPA size.&lt;/p&gt;
&lt;p&gt;I built &lt;code&gt;cleanbev&lt;/code&gt; to fix that locally, and I now a new upgrade is available a GitHub Action so the check runs automatically on every pull request, instead of running it localy.&lt;/p&gt;
&lt;h3 id=&quot;what-cleanbev-actually-does&quot;&gt;What cleanbev actually does&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cleanbev&lt;/code&gt; is a command-line tool for Dart and Flutter projects. It:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reads your &lt;code&gt;pubspec.yaml&lt;/code&gt; to find every declared asset&lt;/li&gt;
&lt;li&gt;Scans your Dart code for references to each one&lt;/li&gt;
&lt;li&gt;Reports anything declared but never referenced&lt;/li&gt;
&lt;li&gt;Optionally deletes what you confirm&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Locally, it's interactive:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dart pub global activate cleanbev
dart pub global run cleanbev --assets-path assets/images
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;✓ Asset assets/images/logo_white_bg.png is not used in the project. Do you want to delete it? · no
Asset assets/images/logo_white_bg.png has not been deleted.

✓ Asset assets/images/download_image-car.jpeg is not used in the project. Do you want to delete it? · yes
Asset assets/images/download_image-car.jpeg has been deleted.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's also available as an MCP server, so editor-integrated agents (VS Code, Cursor, Visual Studio) can call it directly while you're working.&lt;/p&gt;
&lt;h3 id=&quot;the-gap-someone-could-forgot-to-cleanup-some-assets-before-pushing&quot;&gt;The gap: someone could forgot to cleanup some assets before pushing&lt;/h3&gt;
&lt;p&gt;A local CLI tool only helps if someone remembers to run it but in practice, unused-asset cleanup falls into the same bucket as &quot;update the changelog&quot; or &quot;check for outdated dependencies&quot; but under&lt;br&gt;
deadline pressure this could be skipped.&lt;br&gt;
&lt;strong&gt;The only way it reliably happens is if CI enforces it.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That's what the new &lt;strong&gt;Cleanbev Asset Check&lt;/strong&gt; action does.&lt;/p&gt;
&lt;h3 id=&quot;using-it&quot;&gt;Using it&lt;/h3&gt;
&lt;p&gt;Add this into any Flutter repo's &lt;code&gt;.github/workflows/&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: Check Unused Assets

on:
  pull_request:
    branches: [&quot;main&quot;]

jobs:
  cleanbev-asset-check:
    name: Cleanbev Asset Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Cleanbev
        uses: koukibadr/cleanbev-action@v1
        with:
          assets-path: &quot;assets&quot;
          blocking: &quot;true&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;inputs&quot;&gt;Inputs&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Input &lt;/th&gt;&lt;th&gt;Description &lt;/th&gt;&lt;th&gt;Default&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;assets-path&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Path to the assets directory to scan &lt;/td&gt;&lt;td&gt;&lt;code&gt;assets&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;blocking&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Fail the job when unused assets are found &lt;/td&gt;&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;two-ways-to-run-it&quot;&gt;Two ways to run it&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;blocking: &quot;false&quot;&lt;/code&gt;&lt;/strong&gt; (default) — reports what it finds, cleans it up, never fails the build. Good for a low-friction &quot;just keep things tidy&quot; job on a schedule or on pushes to &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;blocking: &quot;true&quot;&lt;/code&gt;&lt;/strong&gt; — turns it into a real PR gate. If a PR introduces an asset that's never referenced, the check fails until it's cleaned up or actually used. This is what I run on pull requests.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;try-it&quot;&gt;Try it&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Action: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/marketplace/actions/cleanbev-asset-check&quot;&gt;github.com/marketplace/actions/cleanbev-asset-check&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CLI on pub.dev: &lt;a rel=&quot;nofollow&quot; href=&quot;https://pub.dev/packages/cleanbev&quot;&gt;pub.dev/packages/cleanbev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MCP server on PyPI: &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/cleanbev-mcp&quot;&gt;pypi.org/project/cleanbev-mcp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's a small tool solving a small, annoying problem. If you try it on a real project and hit a bug please send me your issues and PRs.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26143/automate-unused-asset-cleanup-in-flutter</guid>
<pubDate>Thu, 03 Sep 2026 09:23:34 +0000</pubDate>
</item>
<item>
<title>flutter_skin — Live Remote Skin Engine Demo | Change Flutter Colors Without Rebu</title>
<link>https://coderlegion.com/21977/flutterskin-live-remote-skin-engine-demo-change-flutter-colors-without-rebu</link>
<description>&lt;p&gt;Live Remote Skin Engine Demo | Change Flutter Colors Without Rebuilding.&lt;br&gt;
This is the first demo of flutter_skin — a remote skin engine &lt;br&gt;
for Flutter I've been building for the past few months.&lt;/p&gt;
&lt;p&gt;This is the alpha. Color tokens only for now. Typography, &lt;br&gt;
spacing, border radii, and A/B testing are coming.&lt;/p&gt;
&lt;p&gt;Built by Badr Kouki&lt;br&gt;
Flutter contributor | pub.dev publisher&lt;/p&gt;
</description>
<category>Videos</category>
<guid isPermaLink="true">https://coderlegion.com/21977/flutterskin-live-remote-skin-engine-demo-change-flutter-colors-without-rebu</guid>
<pubDate>Mon, 06 Jul 2026 06:20:40 +0000</pubDate>
</item>
<item>
<title>Fskin alpha version is Live</title>
<link>https://coderlegion.com/21354/fskin-alpha-version-is-live</link>
<description>&lt;p&gt;Every Flutter dev has been here.&lt;br&gt;
Design changed the primary color.&lt;br&gt;
You open the codebase.&lt;br&gt;
You update the hex.&lt;br&gt;
You build.&lt;br&gt;
You submit. &lt;br&gt;
You wait. a full day for a color.&lt;/p&gt;
&lt;p&gt;There’s a better solution now: Fskin&lt;/p&gt;
&lt;p&gt;I built flutter_skin. Publish from a dashboard and the App updates on realtime No build. No review. No restart.&lt;/p&gt;
&lt;p&gt;Alpha is live today.&lt;/p&gt;
</description>
<category>Videos</category>
<guid isPermaLink="true">https://coderlegion.com/21354/fskin-alpha-version-is-live</guid>
<pubDate>Thu, 25 Jun 2026 08:39:15 +0000</pubDate>
</item>
<item>
<title>Eunify: Cross-Platform Continuity System for Android and MacOS</title>
<link>https://coderlegion.com/18178/eunify-cross-platform-continuity-system-for-android-and-macos</link>
<description>&lt;blockquote&gt;&lt;p&gt;I Was Just Solving a Problem that I Face as an Android Phone User&lt;br&gt;
Working With MacOS. I Am Sure There's Someone Else Out There With&lt;br&gt;
Similar Challenges&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I developed Eunify, a continuity platform engineered to create a seamless, native-feeling ecosystem between macOS and Android. By bridging these two distinct operating systems, Eunify eliminates workflow friction and ensures uninterrupted cross-device synchronization for productivity and communication.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Core Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Universal Clipboard: Instantly synchronize your clipboard across operating systems. Text or media copied on your macOS machine is immediately available to paste on your Android device.&lt;/p&gt;
&lt;p&gt;Fluid Browser Handoff: Maintain uninterrupted browsing sessions across devices. You can drag and drop an active tab into Eunify on macOS to instantly launch it on your Android device, or push a tab from Android to pop up seamlessly on your Mac.&lt;/p&gt;
&lt;p&gt;Bidirectional File Transfer: Securely and rapidly send files directly back and forth between your macOS and Android devices without relying on external cloud services.&lt;/p&gt;
&lt;p&gt;Biometric Authentication Integration: Enhance your security workflow by leveraging your Android device's native biometric sensors to unlock your macOS laptop.&lt;/p&gt;
&lt;p&gt;Video Call Continuity: Effortlessly hand off active video calls from your macOS machine to your Android device, allowing you to transition from your desk to mobile without dropping the connection.&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=6697081181013665602&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=7464064703061912304&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5071500663947463985&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=7189078171734645109&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=7666383216313576231&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=11077430319582350407&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12703508908033091367&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=783774772349424263&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5679720936373908046&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18178/eunify-cross-platform-continuity-system-for-android-and-macos</guid>
<pubDate>Sun, 17 May 2026 22:12:01 +0000</pubDate>
</item>
<item>
<title>The Coordinated BLoC Pattern in Flutter — A Practical Guide</title>
<link>https://coderlegion.com/17135/the-coordinated-bloc-pattern-in-flutter-a-practical-guide</link>
<description>&lt;h2 id=&quot;the-coordinated-bloc-pattern-in-flutter-a-practical-guide&quot;&gt;The Coordinated BLoC Pattern in Flutter — A Practical Guide&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;This post is based on real production code. Every mistake documented here was hit in the wild, debugged, and understood the hard way.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;At first, coordinating multiple BLoCs from &lt;code&gt;initState&lt;/code&gt; feels harmless.&lt;/p&gt;
&lt;p&gt;Then the screen grows.&lt;/p&gt;
&lt;p&gt;Now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;some data depends on the authenticated user&lt;/li&gt;
&lt;li&gt;some sections support silent refresh&lt;/li&gt;
&lt;li&gt;some should show loading spinners&lt;/li&gt;
&lt;li&gt;some should skip fetching if cached&lt;/li&gt;
&lt;li&gt;pull-to-refresh has different behaviour than initial load&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And suddenly your screen becomes the orchestration layer for half the app.&lt;/p&gt;
&lt;p&gt;This article covers a pattern for extracting that orchestration into its own BLoC: the &lt;strong&gt;Coordination BLoC pattern&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;More importantly, it covers the production mistakes this pattern introduces — especially the ones that fail silently.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-problem-does-this-solve-&quot;&gt;What Problem Does This Solve?&lt;/h2&gt;
&lt;p&gt;Imagine a home screen that needs data from three different sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;meditation groups&lt;/li&gt;
&lt;li&gt;saadhna progress&lt;/li&gt;
&lt;li&gt;user favourites&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each has its own BLoC.&lt;br&gt;
Each BLoC is independent.&lt;br&gt;
But they all need to be triggered together after the user's profile loads.&lt;/p&gt;
&lt;p&gt;You also want smart refresh behaviour:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;if data is fresh → silently refresh in background&lt;/li&gt;
&lt;li&gt;if data is missing → show loading state&lt;/li&gt;
&lt;li&gt;pull-to-refresh should force reload everything&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Naively, you handle all of this inside &lt;code&gt;initState&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;@override
void initState() {
  super.initState();

  final profileState = context.read&amp;lt;ProfileBloc&amp;gt;().state;

  if (profileState is ProfileLoaded) {
    context.read&amp;lt;HomeMeditationGroupsBloc&amp;gt;()
        .add(FetchHomeMeditationGroups());

    context.read&amp;lt;SaadhnaBloc&amp;gt;()
        .add(FetchAllSaadhnaGroups());

    context.read&amp;lt;UserFavouritesBloc&amp;gt;()
        .add(FetchUserFavouriteMeditations(profileState.id));
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first this works fine.&lt;/p&gt;
&lt;p&gt;Then more conditions appear:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;freshness checks&lt;/li&gt;
&lt;li&gt;silent refreshes&lt;/li&gt;
&lt;li&gt;auth dependencies&lt;/li&gt;
&lt;li&gt;feature flags&lt;/li&gt;
&lt;li&gt;retry behaviour&lt;/li&gt;
&lt;li&gt;analytics&lt;/li&gt;
&lt;li&gt;cross-bloc coordination&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And now your screen knows too much about how loading works.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Coordination BLoC pattern&lt;/strong&gt; extracts orchestration logic into its own BLoC.&lt;/p&gt;
&lt;p&gt;The screen fires one high-level event.&lt;br&gt;
The coordination bloc decides what happens.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;mental-model&quot;&gt;Mental Model&lt;/h2&gt;
&lt;p&gt;The coordination bloc owns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;when&lt;/strong&gt; things load&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;why&lt;/strong&gt; they load&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;which events&lt;/strong&gt; should fire&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The feature blocs still own:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;data&lt;/li&gt;
&lt;li&gt;business logic&lt;/li&gt;
&lt;li&gt;rendering state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The coordination bloc orchestrates.&lt;br&gt;
It does not aggregate UI state.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;architecture&quot;&gt;Architecture&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                    ProfileBloc
                         │
                         ▼
             HomeCoordinationBloc
                         │
      ┌──────────────────┼──────────────────┐
      ▼                  ▼                  ▼
HomeMeditationBloc   SaadhnaBloc   UserFavouritesBloc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The coordination bloc becomes the single orchestration point for screen-level loading behaviour.&lt;/p&gt;
&lt;p&gt;The screen no longer knows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;which blocs exist&lt;/li&gt;
&lt;li&gt;which events they need&lt;/li&gt;
&lt;li&gt;which dependencies they have&lt;/li&gt;
&lt;li&gt;which refresh strategy they use&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;implementation&quot;&gt;Implementation&lt;/h2&gt;
&lt;h3 id=&quot;1-events&quot;&gt;1. Events&lt;/h3&gt;
&lt;p&gt;Keep events coarse-grained and intent-based.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;sealed class HomeCoordinatorEvent {}

class HomeDataFetch extends HomeCoordinatorEvent {}

class HomeDataRefresh extends HomeCoordinatorEvent {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;event-intent&quot;&gt;Event Intent&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Event &lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;HomeDataFetch&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Smart load with freshness checks&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;HomeDataRefresh&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Force reload everything&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-states&quot;&gt;2. States&lt;/h3&gt;
&lt;p&gt;The coordination bloc does &lt;strong&gt;not&lt;/strong&gt; own feature data.&lt;/p&gt;
&lt;p&gt;Its responsibility is orchestration, not aggregation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;sealed class HomeCoordinatorState {}

class HomeCoordinatorInitialState
    extends HomeCoordinatorState {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The UI still reads directly from feature blocs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;BlocBuilder&amp;lt;HomeMeditationGroupsBloc,
    HomeMeditationGroupsState&amp;gt;(
  builder: (context, state) {
    // render UI
  },
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This separation is important.&lt;/p&gt;
&lt;p&gt;The coordination bloc coordinates behaviour.&lt;br&gt;
The feature blocs remain the source of truth.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-the-coordination-bloc&quot;&gt;3. The Coordination BLoC&lt;/h3&gt;
&lt;h4 id=&quot;the-key-idea&quot;&gt;The key idea&lt;/h4&gt;
&lt;p&gt;The screen fires one event:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;HomeDataFetch()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The coordination bloc decides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;which blocs should refresh&lt;/li&gt;
&lt;li&gt;whether data is stale&lt;/li&gt;
&lt;li&gt;whether loading should be silent&lt;/li&gt;
&lt;li&gt;whether user context is required&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class HomeCoordinationBloc
    extends Bloc&amp;lt;HomeCoordinatorEvent,
        HomeCoordinatorState&amp;gt; {

  final HomeMeditationGroupsBloc
      homeMeditationGroupsBloc;

  final SaadhnaBloc saadhnaBloc;

  final UserFavouritesBloc
      userFavouritesBloc;

  final ProfileBloc profileBloc;

  late StreamSubscription profileSubscription;

  HomeCoordinationBloc({
    required this.homeMeditationGroupsBloc,
    required this.saadhnaBloc,
    required this.userFavouritesBloc,
    required this.profileBloc,
  }) : super(HomeCoordinatorInitialState()) {

    // Register handlers FIRST
    on&amp;lt;HomeDataFetch&amp;gt;(_fetchData);
    on&amp;lt;HomeDataRefresh&amp;gt;(_refetchData);

    // Trigger immediately if already loaded
    if (profileBloc.state is ProfileLoaded) {
      add(HomeDataFetch());
    }

    // Otherwise wait for profile load
    profileSubscription = profileBloc.stream
        .where((state) =&amp;gt; state is ProfileLoaded)
        .take(1)
        .listen((_) {
          add(HomeDataFetch());
        });
  }

  void _fetchData(
    HomeDataFetch event,
    Emitter emit,
  ) {
    final userId =
        (profileBloc.state as ProfileLoaded).id;

    final isMeditationFresh =
        homeMeditationGroupsBloc.state
            is HomeMeditationGroupsFetched;

    final isSaadhnaFresh =
        saadhnaBloc.state
            is SaadhnaGroupsFetched;

    final isFavFresh =
        userFavouritesBloc.state
            is UserFavouritesFetched;

    homeMeditationGroupsBloc.add(
      isMeditationFresh
          ? SilentRefreshHomeMeditationGroups()
          : FetchHomeMeditationGroups(),
    );

    saadhnaBloc.add(
      isSaadhnaFresh
          ? RefetchAllSaadhnaGroups()
          : FetchAllSaadhnaGroups(),
    );

    userFavouritesBloc.add(
      isFavFresh
          ? RefetchUserFavouriteMeditations(userId)
          : FetchUserFavouriteMeditations(userId),
    );
  }

  void _refetchData(
    HomeDataRefresh event,
    Emitter emit,
  ) {
    final userId =
        (profileBloc.state as ProfileLoaded).id;

    homeMeditationGroupsBloc.add(
      RefreshHomeMeditationGroups(),
    );

    saadhnaBloc.add(
      FetchAllSaadhnaGroups(),
    );

    userFavouritesBloc.add(
      RefetchUserFavouriteMeditations(userId),
    );
  }

  @override
  Future&amp;lt;void&amp;gt; close() {
    profileSubscription.cancel();
    return super.close();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;dependency-injection-getit-&quot;&gt;Dependency Injection (GetIt)&lt;/h2&gt;
&lt;p&gt;This is where most production bugs come from.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;sl.registerLazySingleton(
  () =&amp;gt; HomeMeditationGroupsBloc(
    getMeditationGroupsByType: sl(),
  ),
);

sl.registerLazySingleton(
  () =&amp;gt; SaadhnaBloc(
    getAllGroups: sl(),
  ),
);

sl.registerLazySingleton(
  () =&amp;gt; UserFavouritesBloc(),
);

sl.registerLazySingleton(
  () =&amp;gt; HomeCoordinationBloc(
    homeMeditationGroupsBloc: sl(),
    saadhnaBloc: sl(),
    userFavouritesBloc: sl(),
    profileBloc: sl(),
  ),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;bloc-providers&quot;&gt;BLoC Providers&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;BlocProvider&amp;lt;UserFavouritesBloc&amp;gt;(
  create: (context) =&amp;gt; sl&amp;lt;UserFavouritesBloc&amp;gt;(),
),

BlocProvider&amp;lt;HomeMeditationGroupsBloc&amp;gt;(
  create: (context)
      =&amp;gt; sl&amp;lt;HomeMeditationGroupsBloc&amp;gt;(),
),

BlocProvider&amp;lt;SaadhnaBloc&amp;gt;(
  create: (context) =&amp;gt; sl&amp;lt;SaadhnaBloc&amp;gt;(),
),

BlocProvider&amp;lt;HomeCoordinationBloc&amp;gt;(
  create: (context)
      =&amp;gt; sl&amp;lt;HomeCoordinationBloc&amp;gt;(),
),
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-screen&quot;&gt;The Screen&lt;/h2&gt;
&lt;p&gt;The screen now knows nothing about orchestration.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;@override
Widget build(BuildContext context) {
  return LiquidPullToRefresh(
    onRefresh: () async {
      context.read&amp;lt;HomeCoordinationBloc&amp;gt;()
          .add(HomeDataRefresh());
    },
    child: ListView(
      children: [
        BlocBuilder&amp;lt;
            HomeMeditationGroupsBloc,
            HomeMeditationGroupsState&amp;gt;(
          builder: (context, state) {
            // render section
          },
        ),
      ],
    ),
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;mistakes-to-not-make&quot;&gt;Mistakes to Not Make&lt;/h2&gt;
&lt;h3 id=&quot;mistake-1-registering-handlers-after-calling-add-&quot;&gt;Mistake 1: Registering Handlers After Calling &lt;code&gt;add()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This causes a runtime &lt;code&gt;Bad state&lt;/code&gt; error.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// WRONG
HomeCoordinationBloc(...)
    : super(HomeCoordinatorInitialState()) {

  if (profileBloc.state is ProfileLoaded) {
    add(HomeDataFetch());
  }

  on&amp;lt;HomeDataFetch&amp;gt;(_fetchData);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// CORRECT
HomeCoordinationBloc(...)
    : super(HomeCoordinatorInitialState()) {

  on&amp;lt;HomeDataFetch&amp;gt;(_fetchData);

  if (profileBloc.state is ProfileLoaded) {
    add(HomeDataFetch());
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The BLoC library checks for a handler immediately when &lt;code&gt;add()&lt;/code&gt; is called.&lt;/p&gt;
&lt;p&gt;Constructor order matters.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;mistake-2-using-registerfactory&quot;&gt;Mistake 2: Using &lt;code&gt;registerFactory&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This bug is especially nasty because everything appears to work — except the UI never updates.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// WRONG
sl.registerFactory(
  () =&amp;gt; HomeMeditationGroupsBloc(...),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What happens:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Coordination bloc gets Instance A&lt;/li&gt;
&lt;li&gt;UI listens to Instance B&lt;/li&gt;
&lt;li&gt;Events fire correctly&lt;/li&gt;
&lt;li&gt;Data loads correctly&lt;/li&gt;
&lt;li&gt;UI never rebuilds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two blocs.&lt;br&gt;
Two states.&lt;br&gt;
Zero visible errors.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// CORRECT
sl.registerLazySingleton(
  () =&amp;gt; HomeMeditationGroupsBloc(...),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;rule&quot;&gt;Rule&lt;/h4&gt;
&lt;p&gt;Any bloc referenced by a coordination bloc should usually be a singleton.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;mistake-3-forgetting-to-cancel-stream-subscriptions&quot;&gt;Mistake 3: Forgetting to Cancel Stream Subscriptions&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// WRONG
@override
Future&amp;lt;void&amp;gt; close() =&amp;gt; super.close();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// CORRECT
@override
Future&amp;lt;void&amp;gt; close() {
  profileSubscription.cancel();
  return super.close();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Otherwise the subscription can attempt to call &lt;code&gt;add()&lt;/code&gt; on a closed bloc.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;mistake-4-forgetting-take-1-&quot;&gt;Mistake 4: Forgetting &lt;code&gt;.take(1)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Without &lt;code&gt;.take(1)&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;profileBloc.stream
    .where((state) =&amp;gt; state is ProfileLoaded)
    .listen((_) =&amp;gt; add(HomeDataFetch()));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every future &lt;code&gt;ProfileLoaded&lt;/code&gt; state retriggers fetches.&lt;/p&gt;
&lt;p&gt;That can create phantom refreshes after:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;token refreshes&lt;/li&gt;
&lt;li&gt;profile edits&lt;/li&gt;
&lt;li&gt;re-authentication&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Usually you only want the first load:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;profileBloc.stream
    .where((state) =&amp;gt; state is ProfileLoaded)
    .take(1)
    .listen((_) =&amp;gt; add(HomeDataFetch()));
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;mistake-5-mixing-getit-and-context-read&quot;&gt;Mistake 5: Mixing &lt;code&gt;GetIt&lt;/code&gt; and &lt;code&gt;context.read&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// Confusing and unnecessary
_homeCoordinationBloc =
    GetIt.instance&amp;lt;HomeCoordinationBloc&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then later:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;context.read&amp;lt;HomeCoordinationBloc&amp;gt;()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pick one access pattern.&lt;/p&gt;
&lt;p&gt;If the bloc comes from &lt;code&gt;BlocProvider&lt;/code&gt;,&lt;br&gt;
prefer &lt;code&gt;context.read&amp;lt;X&amp;gt;()&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;mistake-6-leaving-half-the-logic-in-the-screen&quot;&gt;Mistake 6: Leaving Half the Logic in the Screen&lt;/h3&gt;
&lt;p&gt;Once you introduce a coordination bloc, commit to it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// Split ownership
context.read&amp;lt;HomeCoordinationBloc&amp;gt;()
    .add(HomeDataFetch());

context.read&amp;lt;SaadhnaBloc&amp;gt;()
    .add(FetchAllSaadhnaGroups());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now orchestration lives in two places.&lt;/p&gt;
&lt;p&gt;That is worse than either approach alone.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-hidden-cost-forced-singletons&quot;&gt;The Hidden Cost: Forced Singletons&lt;/h2&gt;
&lt;p&gt;This pattern introduces a real architectural tradeoff.&lt;/p&gt;
&lt;p&gt;Your feature blocs now live for the entire app lifecycle.&lt;/p&gt;
&lt;p&gt;That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cached state survives navigation&lt;/li&gt;
&lt;li&gt;previous data persists&lt;/li&gt;
&lt;li&gt;memory usage increases&lt;/li&gt;
&lt;li&gt;state does not automatically reset&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is usually correct for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;home feeds&lt;/li&gt;
&lt;li&gt;dashboards&lt;/li&gt;
&lt;li&gt;cached content&lt;/li&gt;
&lt;li&gt;long-lived tabs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is usually dangerous for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;forms&lt;/li&gt;
&lt;li&gt;temporary workflows&lt;/li&gt;
&lt;li&gt;checkout flows&lt;/li&gt;
&lt;li&gt;wizard-style screens&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Do not accidentally turn screen-scoped state into app-scoped state.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;when-to-use-this-pattern&quot;&gt;When To Use This Pattern&lt;/h2&gt;
&lt;p&gt;Use it when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3+ blocs must coordinate together&lt;/li&gt;
&lt;li&gt;another bloc's state gates loading&lt;/li&gt;
&lt;li&gt;loading logic contains real conditions&lt;/li&gt;
&lt;li&gt;orchestration is reused across screens&lt;/li&gt;
&lt;li&gt;refresh behaviour is non-trivial&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;when-not-to-use-it&quot;&gt;When NOT To Use It&lt;/h2&gt;
&lt;p&gt;Do not use it when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you're only triggering 1-2 blocs&lt;/li&gt;
&lt;li&gt;coordination is just sequential &lt;code&gt;add()&lt;/code&gt; calls&lt;/li&gt;
&lt;li&gt;the screen logic is still simple&lt;/li&gt;
&lt;li&gt;feature blocs should remain screen-scoped&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In those cases, a &lt;code&gt;BlocListener&lt;/code&gt; is simpler and cleaner.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-simpler-alternative&quot;&gt;The Simpler Alternative&lt;/h2&gt;
&lt;p&gt;For smaller flows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;BlocListener&amp;lt;ProfileBloc, ProfileState&amp;gt;(
  listenWhen: (prev, curr) =&amp;gt;
      curr is ProfileLoaded &amp;amp;&amp;amp;
      prev is! ProfileLoaded,

  listener: (context, state) {
    final userId =
        (state as ProfileLoaded).id;

    context.read&amp;lt;HomeMeditationGroupsBloc&amp;gt;()
        .add(FetchHomeMeditationGroups());

    context.read&amp;lt;SaadhnaBloc&amp;gt;()
        .add(FetchAllSaadhnaGroups());

    context.read&amp;lt;UserFavouritesBloc&amp;gt;()
        .add(
          FetchUserFavouriteMeditations(
            userId,
          ),
        );
  },

  child: YourScreen(),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This solution:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;is smaller&lt;/li&gt;
&lt;li&gt;avoids singletons&lt;/li&gt;
&lt;li&gt;avoids stream subscriptions&lt;/li&gt;
&lt;li&gt;avoids an extra bloc entirely&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Start here first.&lt;/p&gt;
&lt;p&gt;Graduate to coordination blocs only when the orchestration becomes genuinely complex.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Coordination BLoC &lt;/th&gt;&lt;th&gt;BlocListener&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Complexity &lt;/td&gt;&lt;td&gt;High &lt;/td&gt;&lt;td&gt;Low&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Testability &lt;/td&gt;&lt;td&gt;High &lt;/td&gt;&lt;td&gt;Medium&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Reusability &lt;/td&gt;&lt;td&gt;High &lt;/td&gt;&lt;td&gt;Low&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Singleton Requirement &lt;/td&gt;&lt;td&gt;Usually Yes &lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Best For &lt;/td&gt;&lt;td&gt;Complex orchestration &lt;/td&gt;&lt;td&gt;Simple fan-out logic&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The Coordination BLoC pattern is a real solution for real complexity.&lt;/p&gt;
&lt;p&gt;The mistake is introducing it too early.&lt;/p&gt;
&lt;p&gt;Start simple.&lt;br&gt;
Escalate only when the orchestration actually demands it.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/17135/the-coordinated-bloc-pattern-in-flutter-a-practical-guide</guid>
<pubDate>Sat, 09 May 2026 08:12:03 +0000</pubDate>
</item>
<item>
<title>Optimizing Flutter Apps: A Deep Dive into Dart DevTools</title>
<link>https://coderlegion.com/17045/optimizing-flutter-apps-a-deep-dive-into-dart-devtools</link>
<description>&lt;p&gt;Most Flutter developers know how to build apps. Far fewer know how to build &lt;em&gt;fast&lt;/em&gt; ones. The gap between a good app and a great one often isn't in the features. It's in the frames dropped, the memory leaked, and the milliseconds lost.&lt;/p&gt;
&lt;p&gt;Dart DevTools is your toolkit for closing that gap. This guide walks through each of its major panels, what they're good for, and how to actually use them on a real app.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Before you start:&lt;/strong&gt; Always profile in &lt;strong&gt;profile mode&lt;/strong&gt;, not debug mode. Debug mode runs with extra assertions and overhead that will skew every reading you take.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;flutter run --profile
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-flutter-inspector-the-widget-tree-x-ray&quot;&gt;1. Flutter Inspector — The Widget Tree X-Ray&lt;/h3&gt;
&lt;p&gt;The Flutter Inspector gives you a live, interactive view of your widget tree. Think of it as browser DevTools, but for your UI hierarchy. Here are the four toggles you'll use most.&lt;/p&gt;
&lt;h4 id=&quot;slow-animations&quot;&gt;Slow Animations&lt;/h4&gt;
&lt;p&gt;Complex animations can be impossible to debug at full speed. Toggling &lt;strong&gt;Slow Animations&lt;/strong&gt; in the Inspector, or setting this in code, drops them to 20% speed so you can catch glitches frame by frame.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import 'package:flutter/scheduler.dart';

void main() {
  timeDilation = 5.0; // 5× slower
  runApp(const MyApp());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;guidelines-amp-baselines&quot;&gt;Guidelines &amp;amp; Baselines&lt;/h4&gt;
&lt;p&gt;Enables visual overlays that draw:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scroll direction indicators on scrollable widgets&lt;/li&gt;
&lt;li&gt;Widget boundaries (render object outlines)&lt;/li&gt;
&lt;li&gt;Text baselines to catch misaligned typography&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is particularly useful when you suspect a layout issue but cannot pinpoint which widget is the culprit.&lt;/p&gt;
&lt;h4 id=&quot;highlight-repaints&quot;&gt;Highlight Repaints&lt;/h4&gt;
&lt;div align=&quot;center&quot;&gt;
  &lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12462702801371394349&quot; width=&quot;45%&quot; style=&quot;border-radius:50px&quot; alt=&quot;image&quot;&gt;
&lt;p&gt;  &lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1234491430867038178&quot; width=&quot;44%&quot; style=&quot;border-radius:50px&quot; alt=&quot;image&quot;&gt;&lt;br&gt;
&lt;/p&gt;&lt;/div&gt;&lt;br&gt;
Every time a widget repaints, it flashes a continuous stream of color. If your entire screen is flashing because a small loading spinner is animating, you have a problem.
&lt;p&gt;I once traced a scrolling jank issue to a blinking typing indicator that was forcing an entire chat screen to repaint every frame.&lt;/p&gt;
&lt;p&gt;The fix is a &lt;code&gt;RepaintBoundary&lt;/code&gt;. It tells Flutter to isolate that subtree into its own render layer, so its repaints do not cascade upward.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;RepaintBoundary(
  child: MyAnimatedWidget(),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use this around widgets that animate independently, such as loaders, progress bars, and live counters.&lt;/p&gt;
&lt;h4 id=&quot;highlight-oversized-images&quot;&gt;Highlight Oversized Images&lt;/h4&gt;
&lt;p&gt;If an image is decoded at 2000×2000px but displayed at 100×100px, you're wasting memory on 39,900 pixels that will never be seen. This toggle shows them as negative and rotates them as shown in the image&lt;br&gt;
&lt;/p&gt;&lt;div align=&quot;center&quot;&gt;&lt;br&gt;
  &lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=13324385877305745567&quot; width=&quot;45%&quot; style=&quot;border-radius:50px&quot; alt=&quot;image&quot;&gt;&lt;br&gt;
&lt;/div&gt;
&lt;p&gt;The fix is straightforward. Constrain decoding at the source:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;Image.network(
  'https://example.com/large-image.jpg',
  cacheWidth: 200,
  cacheHeight: 200,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Flutter will downsample the image before caching it, significantly reducing memory pressure on image-heavy screens.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-app-size-analysis-know-what-you-re-shipping&quot;&gt;2. App Size Analysis — Know What You're Shipping&lt;/h3&gt;
&lt;p&gt;A bloated app takes longer to download, longer to install, and gives a worse first impression before the user has even launched it. The &lt;code&gt;--analyze-size&lt;/code&gt; flag gives you a breakdown of exactly what's eating into your bundle.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For Android&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;flutter build apk --analyze-size
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;For iOS&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;flutter build ipa --analyze-size
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This outputs a JSON file that you import directly into DevTools. Once loaded, you get two views:&lt;/p&gt;
&lt;h4 id=&quot;dominator-tree&quot;&gt;Dominator Tree&lt;/h4&gt;
&lt;p&gt;Shows which packages are responsible for the largest chunks of your app size. If a utility package you barely use is claiming 3MB, it is worth reconsidering.&lt;/p&gt;
&lt;h4 id=&quot;call-graph&quot;&gt;Call Graph&lt;/h4&gt;
&lt;p&gt;Maps how your dependencies import each other. This is how you find transitive dependencies you did not know you had. A package pulls another package, which pulls another, each silently adding weight.&lt;/p&gt;
&lt;p&gt;Size is a feature.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8905883950125565687&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-performance-amp-jank-tracking-chasing-smooth-frames&quot;&gt;3. Performance &amp;amp; Jank Tracking — Chasing Smooth Frames&lt;/h3&gt;
&lt;p&gt;The target is 60 frames per second. That gives each frame exactly &lt;strong&gt;16.6 milliseconds&lt;/strong&gt; to build and render. Any frame that exceeds this budget is called a &lt;strong&gt;jank&lt;/strong&gt;, and users feel every one of them, even if they cannot name what's wrong.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Performance&lt;/strong&gt; tab lets you:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Record a session of real app interactions&lt;/li&gt;
&lt;li&gt;View a flame chart of frame build times&lt;/li&gt;
&lt;li&gt;Click into expensive frames to see exactly which build, layout, or paint call consumed the most time&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The key is to record &lt;em&gt;specific interactions&lt;/em&gt; rather than letting it run indefinitely. Navigate to the screen you suspect is slow, hit record, reproduce the issue, then stop. A focused recording is far easier to read than five minutes of noise.&lt;/p&gt;
&lt;p&gt;Look for the red frames in the timeline. Those are your jank frames. Drill into them, and you'll usually find one of three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An expensive &lt;code&gt;build()&lt;/code&gt; method&lt;/li&gt;
&lt;li&gt;An unnecessary full-tree rebuild&lt;/li&gt;
&lt;li&gt;A synchronous operation blocking the main thread&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Heavy JSON parsing, large computations, or image processing should not run on the UI isolate. Move them to another isolate using &lt;code&gt;compute()&lt;/code&gt; whenever possible.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;final result = await compute(parseLargeJson, jsonString);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;section&quot;&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=4199897461815345682&quot; alt=&quot;&quot;&gt;&lt;/h3&gt;
&lt;h3 id=&quot;4-network-amp-cpu-profiling-under-the-hood&quot;&gt;4. Network &amp;amp; CPU Profiling — Under the Hood&lt;/h3&gt;
&lt;h4 id=&quot;network-tab&quot;&gt;Network Tab&lt;/h4&gt;
&lt;p&gt;If you've ever tried to debug an API issue by reading through a wall of &lt;code&gt;flutter: {...}&lt;/code&gt; in the debug console, the Network tab will feel like a revelation.&lt;/p&gt;
&lt;p&gt;It captures every HTTP request your app makes and displays:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full request and response headers&lt;/li&gt;
&lt;li&gt;Response body and status codes&lt;/li&gt;
&lt;li&gt;Request timing and duration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No more print-statement archaeology. You can see at a glance if a request is failing, slow, or returning unexpected data.&lt;/p&gt;
&lt;h4 id=&quot;cpu-profiler&quot;&gt;CPU Profiler&lt;/h4&gt;
&lt;p&gt;The CPU Profiler records a flame chart of method calls while your app runs. When something feels sluggish but the Performance tab does not show obvious jank, this is where you look.&lt;/p&gt;
&lt;p&gt;It is especially good for catching:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Expensive operations accidentally running on the main isolate&lt;/li&gt;
&lt;li&gt;Framework callbacks firing more often than expected&lt;/li&gt;
&lt;li&gt;Third-party packages with surprisingly heavy footprints&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-memory-amp-debugger-stability-under-pressure&quot;&gt;5. Memory &amp;amp; Debugger — Stability Under Pressure&lt;/h3&gt;
&lt;h4 id=&quot;memory-tab&quot;&gt;Memory Tab&lt;/h4&gt;
&lt;p&gt;Memory leaks are silent killers. Your app might run perfectly in testing and degrade over time in production as memory accumulates and is never released.&lt;/p&gt;
&lt;p&gt;The most common culprit in Flutter is forgetting to &lt;code&gt;dispose()&lt;/code&gt; objects that hold onto resources.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class MyWidget extends StatefulWidget { ... }

class _MyWidgetState extends State&amp;lt;MyWidget&amp;gt;
    with SingleTickerProviderStateMixin {

  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Memory tab shows a real-time chart of heap allocation. If memory climbs steadily as you navigate through your app and never comes back down, you likely have a leak.&lt;/p&gt;
&lt;p&gt;Reproduce the navigation repeatedly and watch the graph. A healthy app's memory should stabilize over time.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=7694226450075472459&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;debugger&quot;&gt;Debugger&lt;/h4&gt;
&lt;p&gt;The built-in debugger supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Breakpoints&lt;/li&gt;
&lt;li&gt;Step-through execution&lt;/li&gt;
&lt;li&gt;Variable inspection&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All without leaving DevTools.&lt;/p&gt;
&lt;p&gt;This is useful for tracking down logic bugs that only appear in certain states, or for understanding a complex call chain without littering your codebase with &lt;code&gt;print()&lt;/code&gt; calls.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-principles-to-take-away&quot;&gt;Key Principles to Take Away&lt;/h3&gt;
&lt;h4 id=&quot;profile-mode-is-non-negotiable&quot;&gt;Profile Mode Is Non-Negotiable&lt;/h4&gt;
&lt;p&gt;Debug mode adds overhead that makes everything look slower than it is. Measure performance in profile mode, always.&lt;/p&gt;
&lt;h4 id=&quot;do-not-optimize-blindly&quot;&gt;Do Not Optimize Blindly&lt;/h4&gt;
&lt;p&gt;Reach for DevTools when you observe a real problem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A user complaint&lt;/li&gt;
&lt;li&gt;A visible stutter&lt;/li&gt;
&lt;li&gt;A reported crash&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Premature optimization creates complexity without measurable benefit.&lt;/p&gt;
&lt;h4 id=&quot;older-devices-are-your-benchmark&quot;&gt;Older Devices Are Your Benchmark&lt;/h4&gt;
&lt;p&gt;An app that runs smoothly on a flagship device might choke on a three-year-old mid-range phone. Test on the weakest hardware your users are likely to have.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Dart DevTools removes the guesswork from Flutter performance.&lt;/p&gt;
&lt;p&gt;Each panel answers a specific category of question:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What's rendering&lt;/li&gt;
&lt;li&gt;What's too big&lt;/li&gt;
&lt;li&gt;What's too slow&lt;/li&gt;
&lt;li&gt;What's leaking&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use them together, and you'll spend less time wondering why your app feels sluggish and more time fixing it.&lt;/p&gt;
&lt;p&gt;Thanks to &lt;strong&gt;Aswin Gopinathan&lt;/strong&gt; for this wonderful workshop, from which I got the knowledge for this blog. Do check it out on &lt;a rel=&quot;nofollow&quot; href=&quot;https://youtu.be/dTNMxeIfS-o?si=GttEhpSuIeRD8s8i&quot;&gt;Youtube&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17045/optimizing-flutter-apps-a-deep-dive-into-dart-devtools</guid>
<pubDate>Fri, 08 May 2026 09:59:05 +0000</pubDate>
</item>
<item>
<title>Why Your Flutter UI Freezes: Understanding Async, Await, Compute &amp; Isolates</title>
<link>https://coderlegion.com/16956/why-your-flutter-ui-freezes-understanding-async-await-compute-isolates</link>
<description>&lt;p&gt;You're building a Flutter app. Everything feels smooth — taps register instantly, animations glide, scrolling is butter.&lt;/p&gt;
&lt;p&gt;Then one day you load a large dataset. Maybe 15,000 transactions from a crypto API. Maybe a chunky image processing job. Maybe just a fat JSON response.&lt;/p&gt;
&lt;p&gt;And suddenly: &lt;strong&gt;the UI freezes&lt;/strong&gt;. Buttons stop responding. Animations stutter. Scrolling locks up. Users notice.&lt;/p&gt;
&lt;p&gt;Your first instinct? Blame &lt;code&gt;async/await&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Bad news: &lt;code&gt;async/await&lt;/code&gt; is innocent. The real culprit is something more fundamental — and once you understand it, you'll never guess at Flutter performance problems again. You'll &lt;em&gt;engineer&lt;/em&gt; around them.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-uncomfortable-truth-dart-is-single-threaded&quot;&gt;The Uncomfortable Truth: Dart Is Single-Threaded&lt;/h3&gt;
&lt;p&gt;Every Flutter app runs inside a &lt;strong&gt;Dart isolate&lt;/strong&gt;. That isolate has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Its own memory&lt;/li&gt;
&lt;li&gt;Its own event loop&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One execution thread&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One. Single. Thread.&lt;/p&gt;
&lt;p&gt;Picture it as a conveyor belt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;→ RENDER FRAME → HANDLE TAP → RENDER FRAME → YOUR HEAVY WORK → RENDER FRAME →
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The belt moves forward only when each item finishes. If &quot;YOUR HEAVY WORK&quot; takes 3 seconds, the belt stops for 3 seconds. No rendering. No touch events. Nothing.&lt;/p&gt;
&lt;p&gt;That's your UI freeze. That's jank.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-biggest-misconception-in-flutter-development&quot;&gt;The Biggest Misconception in Flutter Development&lt;/h3&gt;
&lt;p&gt;Here's what most developers assume:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;await heavyCalculation(); // &quot;this runs in the background, right?&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Wrong.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;async/await&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; move work to another thread. It does not create parallelism. It does not make your code run somewhere else.&lt;/p&gt;
&lt;p&gt;What it &lt;em&gt;actually&lt;/em&gt; means is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;&quot;Pause this function here. Resume it later when the Future completes.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's it. That's the whole trick.&lt;/p&gt;
&lt;p&gt;To prove it — this code freezes your UI completely, even with &lt;code&gt;async/await&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;Future&amp;lt;void&amp;gt; freezeUI() async {
  for (var i = 0; i &amp;lt; 1000000000; i++) {} // CPU grinding on the UI thread
}

// Calling this:
await freezeUI(); // Still freezes. async didn't save you.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The work is still running on the &lt;strong&gt;same isolate&lt;/strong&gt; as your UI. &lt;code&gt;await&lt;/code&gt; just made it look like it wouldn't.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;so-what-does-async-await-actually-do-well-&quot;&gt;So What Does Async-Await Actually Do Well?&lt;/h3&gt;
&lt;p&gt;Async shines when your code is &lt;strong&gt;waiting&lt;/strong&gt;, not &lt;strong&gt;working&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When you make a network request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;final response = await http.get(Uri.parse('https://api.example.com/data'));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's what actually happens under the hood:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Dart initiates the request&lt;/li&gt;
&lt;li&gt;Dart tells the OS: &lt;em&gt;&quot;ping me when this finishes&quot;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The isolate is now free&lt;/strong&gt; — it goes back to rendering, handling taps, running animations&lt;/li&gt;
&lt;li&gt;When the response arrives, Dart resumes your function from where it left off&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No blocking. No freezing. The isolate was idle &lt;em&gt;anyway&lt;/em&gt; — it was waiting on the network, not computing.&lt;/p&gt;
&lt;p&gt;This is non-blocking I/O. And &lt;code&gt;async/await&lt;/code&gt; handles it beautifully.&lt;/p&gt;
&lt;p&gt;The problem only starts when you stop &lt;em&gt;waiting&lt;/em&gt; and start &lt;em&gt;working&lt;/em&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-dart-event-loop-and-why-it-matters-&quot;&gt;The Dart Event Loop (And Why It Matters)&lt;/h3&gt;
&lt;p&gt;To really understand async behavior, you need to know how Dart schedules work.&lt;/p&gt;
&lt;p&gt;Dart's event loop has two queues:&lt;/p&gt;
&lt;h4 id=&quot;microtask-queue-high-priority-&quot;&gt;Microtask Queue (High Priority)&lt;/h4&gt;
&lt;p&gt;Used for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Future&lt;/code&gt; continuations (what runs after an &lt;code&gt;await&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scheduleMicrotask()&lt;/code&gt; calls&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;event-queue-normal-priority-&quot;&gt;Event Queue (Normal Priority)&lt;/h4&gt;
&lt;p&gt;Used for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Timer callbacks&lt;/li&gt;
&lt;li&gt;Network/IO completions&lt;/li&gt;
&lt;li&gt;Gesture events&lt;/li&gt;
&lt;li&gt;Platform messages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The loop runs like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. Run all synchronous code
2. Drain every pending microtask
3. Execute one event from the event queue
4. Repeat forever
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ordering has a real-world consequence. Here's a classic puzzle:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;void main() {
  print(&quot;A&quot;);                                    // sync
  Future(() =&amp;gt; print(&quot;B&quot;));                      // event queue
  scheduleMicrotask(() =&amp;gt; print(&quot;C&quot;));           // microtask queue
  print(&quot;D&quot;);                                    // sync
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A
D
C
B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why? Because:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;A&lt;/code&gt; and &lt;code&gt;D&lt;/code&gt; are synchronous — they run first, in order&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C&lt;/code&gt; is a microtask — it runs before any events&lt;/li&gt;
&lt;li&gt;&lt;code&gt;B&lt;/code&gt; is in the event queue — it runs last&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Knowing this helps you reason about execution order, debug subtle timing bugs, and understand &lt;em&gt;why&lt;/em&gt; &lt;code&gt;async/await&lt;/code&gt; resumptions feel immediate (they're microtasks).&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-json-parsing-trap&quot;&gt;The JSON Parsing Trap&lt;/h3&gt;
&lt;p&gt;Here's where developers get burned all the time.&lt;/p&gt;
&lt;p&gt;Your app fetches data. The network call is fine — async, non-blocking, smooth. But then:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;final response = await http.get(url);               // non-blocking, isolate is free
final data = jsonDecode(response.body);             // CPU-heavy, blocks the isolate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;jsonDecode&lt;/code&gt; on a small payload? No problem. On 15,000 transaction records? That single line can block your UI for &lt;strong&gt;2–4 seconds&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The network call wasn't your enemy. The parsing was.&lt;/p&gt;
&lt;p&gt;This is the trap: &lt;strong&gt;the operation after &lt;code&gt;await&lt;/code&gt; can still freeze your UI&lt;/strong&gt; if it's CPU-intensive.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-fix-isolates&quot;&gt;The Fix: Isolates&lt;/h3&gt;
&lt;p&gt;An &lt;strong&gt;isolate&lt;/strong&gt; is an independent Dart execution environment. It has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Its own memory heap (completely separate from your UI isolate)&lt;/li&gt;
&lt;li&gt;Its own event loop&lt;/li&gt;
&lt;li&gt;Its own thread&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unlike OS threads, isolates &lt;strong&gt;cannot share memory&lt;/strong&gt;. This eliminates entire classes of bugs — no race conditions, no mutex deadlocks, no shared-state corruption. Instead, isolates communicate by passing messages.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Main Isolate                    Worker Isolate
─────────────                   ──────────────
UI rendering                    JSON parsing
Touch handling       ←──────→   Image processing
Animations          (messages)  Heavy computation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While the worker isolate grinds through CPU work, your UI isolate keeps rendering at 60fps.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;compute-isolates-without-the-ceremony&quot;&gt;&lt;code&gt;compute()&lt;/code&gt;: Isolates Without the Ceremony&lt;/h3&gt;
&lt;p&gt;Spawning a raw isolate involves some boilerplate. For one-off tasks, Flutter gives you &lt;code&gt;compute()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// Before: freezes UI
final transactions = jsonDecode(response.body);

// After: runs in a separate isolate, UI stays smooth
final transactions = await compute(parseTransactions, response.body);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;code&gt;parseTransactions&lt;/code&gt; is a top-level or static function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;List&amp;lt;Transaction&amp;gt; parseTransactions(String rawJson) {
  final decoded = jsonDecode(rawJson) as List;
  return decoded.map((e) =&amp;gt; Transaction.fromJson(e)).toList();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;compute()&lt;/code&gt; under the hood:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Spawns a new isolate&lt;/li&gt;
&lt;li&gt;Sends your data to it (via message passing — it gets copied)&lt;/li&gt;
&lt;li&gt;Runs your function&lt;/li&gt;
&lt;li&gt;Returns the result to the main isolate&lt;/li&gt;
&lt;li&gt;Destroys the worker isolate&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;One function call. UI stays responsive. Task done in parallel.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-compute-isn-t-enough-persistent-isolates&quot;&gt;When &lt;code&gt;compute()&lt;/code&gt; Isn't Enough: Persistent Isolates&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;compute()&lt;/code&gt; is great for one-shot jobs. But spawning and destroying isolates has overhead. If you're doing &lt;strong&gt;continuous background work&lt;/strong&gt; — like a live transaction feed, streaming parser, or on-device ML inference — you want a persistent isolate.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// Pseudo-code for a long-running isolate
final receivePort = ReceivePort();
final isolate = await Isolate.spawn(backgroundWorker, receivePort.sendPort);

receivePort.listen((message) {
  // Handle result from background isolate
  setState(() =&amp;gt; _data = message);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use persistent isolates when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You're processing a continuous stream of data&lt;/li&gt;
&lt;li&gt;You're running local AI/ML models&lt;/li&gt;
&lt;li&gt;You have a background sync engine&lt;/li&gt;
&lt;li&gt;The spawning overhead of &lt;code&gt;compute()&lt;/code&gt; becomes measurable&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-decision-framework&quot;&gt;The Decision Framework&lt;/h3&gt;
&lt;p&gt;Every time you write async code, ask: &lt;strong&gt;am I waiting or working?&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Scenario &lt;/th&gt;&lt;th&gt;Use&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;API call / network request &lt;/td&gt;&lt;td&gt;&lt;code&gt;async/await&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Database read/write &lt;/td&gt;&lt;td&gt;&lt;code&gt;async/await&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;File I/O &lt;/td&gt;&lt;td&gt;&lt;code&gt;async/await&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Timer / delay &lt;/td&gt;&lt;td&gt;&lt;code&gt;async/await&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;JSON parsing (large) &lt;/td&gt;&lt;td&gt;&lt;code&gt;compute()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Image processing &lt;/td&gt;&lt;td&gt;&lt;code&gt;compute()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Encryption / hashing &lt;/td&gt;&lt;td&gt;&lt;code&gt;compute()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;One-off heavy computation &lt;/td&gt;&lt;td&gt;&lt;code&gt;compute()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Streaming data parser &lt;/td&gt;&lt;td&gt;Persistent Isolate&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;On-device ML inference &lt;/td&gt;&lt;td&gt;Persistent Isolate&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Background sync engine &lt;/td&gt;&lt;td&gt;Persistent Isolate&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;real-world-example-crypto-wallet-transaction-feed&quot;&gt;Real-World Example: Crypto Wallet Transaction Feed&lt;/h3&gt;
&lt;p&gt;Let's put it all together with a realistic scenario.&lt;/p&gt;
&lt;p&gt;Your wallet app fetches 15,000 transactions. Naïve implementation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// Freezes UI for multiple seconds
Future&amp;lt;void&amp;gt; loadTransactions() async {
  final response = await http.get(transactionsUrl);   // fine
  final txs = jsonDecode(response.body);              //  blocks here
  setState(() =&amp;gt; _transactions = txs);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Better:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// UI stays responsive throughout
Future&amp;lt;void&amp;gt; loadTransactions() async {
  final response = await http.get(transactionsUrl);                    // non-blocking
  final txs = await compute(parseTransactions, response.body);         // off-thread
  setState(() =&amp;gt; _transactions = txs);                                 // back on UI thread
}

List&amp;lt;Transaction&amp;gt; parseTransactions(String json) {
  return (jsonDecode(json) as List)
      .map((e) =&amp;gt; Transaction.fromJson(e))
      .toList();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The user never sees a freeze. Scrolling stays smooth at 60fps while thousands of transactions parse in the background.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;common-myths-killed&quot;&gt;Common Myths, Killed&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&quot;Async/await makes my code multithreaded&quot;&lt;/strong&gt;&lt;br&gt;
No. It handles asynchronous &lt;em&gt;waiting&lt;/em&gt;. Multithreading requires isolates.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;If I put await before it, it won't block the UI&quot;&lt;/strong&gt;&lt;br&gt;
Only if the work is I/O-bound. CPU-heavy code blocks regardless of &lt;code&gt;await&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;compute() is some kind of magic optimization&quot;&lt;/strong&gt;&lt;br&gt;
It's just a thin wrapper around isolate spawning. The magic is the isolate, not the function.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;Dart can only use one CPU core&quot;&lt;/strong&gt;&lt;br&gt;
False. Each isolate can run on its own OS thread. Dart can use all your cores through isolates.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-mental-model-that-fixes-everything&quot;&gt;The Mental Model That Fixes Everything&lt;/h3&gt;
&lt;p&gt;Burn this into your brain:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async/await  →  for WAITING
isolates     →  for WORKING
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's the whole framework. Every Flutter performance bug related to async ultimately comes down to confusing these two. Once you stop conflating them, you stop guessing — and start building apps that are genuinely fast.&lt;/p&gt;
&lt;p&gt;Your UI thread should &lt;em&gt;only&lt;/em&gt; wait. Never grind.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16956/why-your-flutter-ui-freezes-understanding-async-await-compute-isolates</guid>
<pubDate>Thu, 07 May 2026 11:28:09 +0000</pubDate>
</item>
<item>
<title>Everything about Stateful &amp; Stateless widgets in Flutter</title>
<link>https://coderlegion.com/16550/everything-about-stateful-stateless-widgets-in-flutter</link>
<description>&lt;p&gt;Flutter's widget system looks deceptively simple on the surface — everything is a widget, you compose them, and the UI appears. But once you go deeper, you realize Flutter made a very deliberate architectural decision by splitting widgets into two kinds: &lt;strong&gt;Stateless&lt;/strong&gt; and &lt;strong&gt;Stateful&lt;/strong&gt;. Understanding &lt;em&gt;why&lt;/em&gt; that split exists, and &lt;em&gt;how&lt;/em&gt; the machinery behind it works, is what separates developers who use Flutter from developers who truly understand it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-a-widget-really-&quot;&gt;What Is a Widget, Really?&lt;/h3&gt;
&lt;p&gt;Before distinguishing the two, it's worth being precise about what a widget actually is in Flutter's model.&lt;/p&gt;
&lt;p&gt;A widget is &lt;strong&gt;an immutable description of a piece of UI&lt;/strong&gt;. That's it. It's a configuration object — a blueprint. It doesn't draw anything, it doesn't hold layout state, it doesn't live on screen. It just describes what should exist.&lt;/p&gt;
&lt;p&gt;Flutter maintains three separate trees to actually make that description come alive:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Widget tree&lt;/strong&gt; — The lightweight, immutable descriptions you write in code. Rebuilt frequently.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Element tree&lt;/strong&gt; — The long-lived, mutable counterpart to each widget. Elements persist across rebuilds and act as the glue between widgets and render objects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RenderObject tree&lt;/strong&gt; — The heavy, layout-and-paint layer. This is what actually measures, positions, and renders pixels.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This separation is critical to Flutter's performance. Rebuilding the widget tree is cheap because widgets are just Dart objects with a few fields. The element and render object trees, which are expensive to create, are preserved and updated incrementally.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;stateless-widgets&quot;&gt;Stateless Widgets&lt;/h3&gt;
&lt;p&gt;A &lt;code&gt;StatelessWidget&lt;/code&gt; is a widget whose entire output is determined solely by its constructor arguments. Given the same inputs, it will always produce the same widget tree — there is no internal state that can change between builds.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class GreetingCard extends StatelessWidget {
  final String name;
  final String message;

  const GreetingCard({super.key, required this.name, required this.message});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Hello, $name'),
        Text(message),
      ],
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Flutter calls &lt;code&gt;build()&lt;/code&gt; on a stateless widget when its parent rebuilds and passes it new configuration. The widget itself has no lifecycle beyond that single &lt;code&gt;build()&lt;/code&gt; call — no initialization, no teardown, no reactive hooks.&lt;/p&gt;
&lt;h4 id=&quot;the-const-optimization&quot;&gt;The &lt;code&gt;const&lt;/code&gt; Optimization&lt;/h4&gt;
&lt;p&gt;Stateless widgets shine when paired with &lt;code&gt;const&lt;/code&gt; constructors. When Flutter encounters a &lt;code&gt;const&lt;/code&gt; widget in a rebuild, it can short-circuit the entire subtree — it knows the widget is identical to the previous one (same type, same arguments, canonicalized at compile time), so there's nothing to diff.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// Flutter will never rebuild this subtree unless the parent explicitly changes it
const Icon(Icons.star, color: Colors.amber, size: 24);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For UI elements that are truly static — icons, labels, decorative containers — &lt;code&gt;const&lt;/code&gt; is a free performance win. Make it a habit to use &lt;code&gt;const&lt;/code&gt; wherever possible and let the Dart analyzer enforce it with the &lt;code&gt;prefer_const_constructors&lt;/code&gt; lint.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;stateful-widgets&quot;&gt;Stateful Widgets&lt;/h3&gt;
&lt;p&gt;A &lt;code&gt;StatefulWidget&lt;/code&gt; is for UI that needs to change over time based on user interaction, async results, animations, or any other runtime event. The key design decision Flutter made here is that &lt;strong&gt;the widget and its state are two separate objects&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State&amp;lt;Counter&amp;gt; createState() =&amp;gt; _CounterState();
}

class _CounterState extends State&amp;lt;Counter&amp;gt; {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: () =&amp;gt; setState(() =&amp;gt; _count++),
      child: Text('Count: $_count'),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why separate them? Because &lt;strong&gt;widgets are immutable and short-lived&lt;/strong&gt;, but &lt;strong&gt;state needs to persist&lt;/strong&gt;. When Flutter rebuilds the &lt;code&gt;Counter&lt;/code&gt; widget, it creates a brand-new &lt;code&gt;Counter&lt;/code&gt; object — but the &lt;code&gt;_CounterState&lt;/code&gt; instance that holds &lt;code&gt;_count&lt;/code&gt; is kept alive by the element tree. The element simply calls &lt;code&gt;widget.build()&lt;/code&gt; on the new widget description while retaining the existing &lt;code&gt;State&lt;/code&gt; object beneath it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-stateful-lifecycle&quot;&gt;The Stateful Lifecycle&lt;/h3&gt;
&lt;p&gt;State objects have a full lifecycle managed by the Flutter framework. Understanding each phase is essential for writing correct, leak-free Flutter code.&lt;/p&gt;
&lt;h4 id=&quot;createstate-&quot;&gt;&lt;code&gt;createState()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Called on the widget (not the state) to create the associated &lt;code&gt;State&lt;/code&gt; object. This happens exactly once — when the widget is first inserted into the tree.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;@override
State&amp;lt;MyWidget&amp;gt; createState() =&amp;gt; _MyWidgetState();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;initstate-&quot;&gt;&lt;code&gt;initState()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;The first method called on the &lt;code&gt;State&lt;/code&gt; object after it's created. This is where you perform one-time initialization — subscribing to streams, setting up animation controllers, making initial API calls.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;@override
void initState() {
  super.initState(); // Always call super first
  _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
  _fetchInitialData();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;initState()&lt;/code&gt; runs before the first build, and &lt;code&gt;context&lt;/code&gt; is available but restricted here — you cannot call &lt;code&gt;context&lt;/code&gt;-dependent operations like &lt;code&gt;Navigator.of(context)&lt;/code&gt; from &lt;code&gt;initState()&lt;/code&gt;. Use &lt;code&gt;didChangeDependencies()&lt;/code&gt; or a post-frame callback for that.&lt;/p&gt;
&lt;h4 id=&quot;didchangedependencies-&quot;&gt;&lt;code&gt;didChangeDependencies()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Called immediately after &lt;code&gt;initState()&lt;/code&gt;, and again whenever an &lt;code&gt;InheritedWidget&lt;/code&gt; that this state object depends on changes. For example, if your widget reads from &lt;code&gt;Theme.of(context)&lt;/code&gt; or a custom &lt;code&gt;InheritedWidget&lt;/code&gt;, and that data changes, &lt;code&gt;didChangeDependencies()&lt;/code&gt; fires before the next build.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;@override
void didChangeDependencies() {
  super.didChangeDependencies();
  // Safe to use context-dependent APIs here
  final locale = Localizations.localeOf(context);
  _updateForLocale(locale);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;build-&quot;&gt;&lt;code&gt;build()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;The only required override. Called every time the state is marked dirty — after &lt;code&gt;setState()&lt;/code&gt;, after &lt;code&gt;didChangeDependencies()&lt;/code&gt;, after &lt;code&gt;didUpdateWidget()&lt;/code&gt;, and during the initial render. Must be pure and free of side effects.&lt;/p&gt;
&lt;h4 id=&quot;setstate-&quot;&gt;&lt;code&gt;setState()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;The mechanism for signaling that internal state has changed and the widget needs to rebuild.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;setState(() {
  _count++;
  _isLoading = false;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you call &lt;code&gt;setState()&lt;/code&gt;, Flutter marks the element as &lt;strong&gt;dirty&lt;/strong&gt;. It doesn't trigger an immediate rebuild — instead, it schedules a frame. At the start of the next frame, Flutter walks the element tree, finds all dirty elements, and calls &lt;code&gt;build()&lt;/code&gt; on each of them. The widget returned by &lt;code&gt;build()&lt;/code&gt; is then reconciled against the previous widget using the element tree (more on this below).&lt;/p&gt;
&lt;h4 id=&quot;didupdatewidget-&quot;&gt;&lt;code&gt;didUpdateWidget()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Called when the &lt;em&gt;parent&lt;/em&gt; rebuilds and passes a new widget instance to this state's element. The old widget is passed as an argument, giving you a chance to react to configuration changes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;@override
void didUpdateWidget(covariant MyWidget oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (widget.userId != oldWidget.userId) {
    _fetchDataForUser(widget.userId);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the correct place to respond to external prop changes — not &lt;code&gt;initState()&lt;/code&gt;, which only runs once.&lt;/p&gt;
&lt;h4 id=&quot;deactivate-&quot;&gt;&lt;code&gt;deactivate()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Called when the &lt;code&gt;State&lt;/code&gt; object is temporarily removed from the tree — for example, when a widget is moved to a different subtree location using a &lt;code&gt;GlobalKey&lt;/code&gt;. This is uncommon in everyday Flutter code, but important to know.&lt;/p&gt;
&lt;h4 id=&quot;dispose-&quot;&gt;&lt;code&gt;dispose()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Called when the &lt;code&gt;State&lt;/code&gt; object is permanently removed from the tree. This is where you clean up resources — cancel timers, close streams, dispose animation controllers, remove listeners.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;@override
void dispose() {
  _controller.dispose();
  _scrollController.dispose();
  _subscription.cancel();
  super.dispose(); // Always call super last
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Forgetting to &lt;code&gt;dispose()&lt;/code&gt; resources is one of the most common sources of memory leaks in Flutter apps.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-reconciliation-actually-works&quot;&gt;How Reconciliation Actually Works&lt;/h3&gt;
&lt;p&gt;When a dirty element rebuilds, it gets a new widget from &lt;code&gt;build()&lt;/code&gt;. Flutter then needs to decide: does the existing child element still apply, or does it need to be replaced?&lt;/p&gt;
&lt;p&gt;Flutter checks two things in order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Type&lt;/strong&gt; — Is the new widget the same runtime type as the widget the element currently holds?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key&lt;/strong&gt; — If a key is present, does it match?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If both match, Flutter &lt;strong&gt;reuses the element&lt;/strong&gt; and simply updates its widget reference. The underlying &lt;code&gt;RenderObject&lt;/code&gt; is updated in place — expensive layout state is preserved.&lt;/p&gt;
&lt;p&gt;If either differs, Flutter &lt;strong&gt;deactivates the old element&lt;/strong&gt;, creates a new one, and mounts it. For stateful widgets, this means the &lt;code&gt;State&lt;/code&gt; is disposed and a new one is created from scratch.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// Scenario: parent conditionally renders one of two widgets
if (_showA)
  const WidgetA()   // Type: WidgetA
else
  const WidgetB()   // Type: WidgetB — new element, old state discarded
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;keys-making-reconciliation-precise&quot;&gt;Keys: Making Reconciliation Precise&lt;/h3&gt;
&lt;p&gt;By default, Flutter reconciles children by &lt;strong&gt;type and position&lt;/strong&gt;. For static, fixed-size lists this works perfectly. But for &lt;strong&gt;dynamic, reorderable, or filtered lists&lt;/strong&gt;, positional matching breaks down.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// Imagine these items get reordered from [A, B] to [B, A]
// Without keys, Flutter sees: position 0 = same type, position 1 = same type
// It reuses the same elements — the state of A bleeds into B's position
Column(
  children: [
    TodoItem(label: 'B'),
    TodoItem(label: 'A'),
  ],
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keys tell Flutter explicitly which element corresponds to which widget, regardless of position.&lt;/p&gt;
&lt;h4 id=&quot;localkey&quot;&gt;&lt;code&gt;LocalKey&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Scoped to siblings within the same parent. The common variants are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ValueKey&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/strong&gt; — keyed by a value (most common, use with IDs or strings)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ObjectKey&lt;/code&gt;&lt;/strong&gt; — keyed by object identity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;UniqueKey&lt;/code&gt;&lt;/strong&gt; — generates a new key every time (forces recreation — use deliberately)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;ListView(
  children: _todos.map((todo) =&amp;gt;
    TodoItem(key: ValueKey(todo.id), label: todo.label)
  ).toList(),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when items reorder, Flutter correctly matches each &lt;code&gt;TodoItem&lt;/code&gt; to its element by ID, preserving the right state in the right place.&lt;/p&gt;
&lt;h4 id=&quot;globalkey&quot;&gt;&lt;code&gt;GlobalKey&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Unique across the entire widget tree. A &lt;code&gt;GlobalKey&lt;/code&gt; lets you access a widget's &lt;code&gt;State&lt;/code&gt;, &lt;code&gt;BuildContext&lt;/code&gt;, or &lt;code&gt;RenderObject&lt;/code&gt; from anywhere in the app.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;final _formKey = GlobalKey&amp;lt;FormState&amp;gt;();

// Later:
if (_formKey.currentState!.validate()) {
  _formKey.currentState!.save();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;GlobalKey&lt;/code&gt; is powerful but expensive — Flutter maintains a global registry for them. Avoid using them as a shortcut to reach into widget state; prefer proper state management patterns for that. Use &lt;code&gt;GlobalKey&lt;/code&gt; when you genuinely need framework-level access — form validation, imperative scroll control, or moving a subtree across the tree while preserving its state.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-mounted-property-and-async-safety&quot;&gt;The &lt;code&gt;mounted&lt;/code&gt; Property and Async Safety&lt;/h3&gt;
&lt;p&gt;Every &lt;code&gt;State&lt;/code&gt; object has a &lt;code&gt;mounted&lt;/code&gt; boolean. It becomes &lt;code&gt;true&lt;/code&gt; after &lt;code&gt;initState()&lt;/code&gt; runs and transitions to &lt;code&gt;false&lt;/code&gt; after &lt;code&gt;dispose()&lt;/code&gt;. It exists to solve a specific and common problem: &lt;strong&gt;calling &lt;code&gt;setState()&lt;/code&gt; after the widget has been removed from the tree&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;Future&amp;lt;void&amp;gt; _loadData() async {
  final result = await repository.fetchItems(); // async gap — widget may be disposed here

  setState(() =&amp;gt; _items = result); //  throws if widget was already disposed
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The safe pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;Future&amp;lt;void&amp;gt; _loadData() async {
  final result = await repository.fetchItems();

  if (mounted) {
    setState(() =&amp;gt; _items = result);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is especially important in scenarios where a user navigates away while a network call is still in flight, or when a parent removes the widget before the async operation completes. The &lt;code&gt;mounted&lt;/code&gt; check is a lightweight guard that prevents both the exception and the logical error of mutating dead state.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-to-use-which&quot;&gt;When to Use Which&lt;/h3&gt;
&lt;p&gt;The choice is usually straightforward once you internalize the rule:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;If the widget can fully describe its UI from its constructor arguments alone, use &lt;code&gt;StatelessWidget&lt;/code&gt;. If it needs to track any internal state that changes over time, use &lt;code&gt;StatefulWidget&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Some practical heuristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pure display widgets (cards, labels, icons, decorative layouts) → &lt;code&gt;StatelessWidget&lt;/code&gt; with &lt;code&gt;const&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Anything with user interaction that changes appearance (toggles, counters, form fields) → &lt;code&gt;StatefulWidget&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Animations → &lt;code&gt;StatefulWidget&lt;/code&gt; with &lt;code&gt;SingleTickerProviderStateMixin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Widgets that only receive state from above (via constructor or &lt;code&gt;InheritedWidget&lt;/code&gt;) and just render it → &lt;code&gt;StatelessWidget&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's also worth noting that the separation isn't just about correctness — it's about communicating intent. A &lt;code&gt;StatelessWidget&lt;/code&gt; signals to every reader of your code: &lt;em&gt;this widget has no internal moving parts&lt;/em&gt;. That's a strong, useful guarantee.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;putting-it-all-together&quot;&gt;Putting It All Together&lt;/h3&gt;
&lt;p&gt;Flutter's architecture makes more sense when you see the whole picture at once:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Widgets&lt;/strong&gt; are cheap, immutable blueprints. Rebuilt frequently, thrown away without ceremony.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Elements&lt;/strong&gt; are the persistent, mutable objects that hold the tree together. They match incoming widgets to existing state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RenderObjects&lt;/strong&gt; are expensive and preserved. Updated in place whenever possible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stateful widgets&lt;/strong&gt; get a separate &lt;code&gt;State&lt;/code&gt; object, anchored to an element, that survives widget rebuilds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;setState()&lt;/code&gt;&lt;/strong&gt; marks elements dirty and schedules a frame — not an immediate rebuild.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reconciliation&lt;/strong&gt; by type and key determines whether to reuse or replace elements.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keys&lt;/strong&gt; make that reconciliation correct when position alone isn't enough.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;mounted&lt;/code&gt;&lt;/strong&gt; is your safety net for async operations that outlive the widget.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every Flutter performance tip — minimizing rebuilds, using &lt;code&gt;const&lt;/code&gt;, lifting state appropriately, keying lists — traces back to understanding these fundamentals. Once the model is clear, the best practices follow naturally.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16550/everything-about-stateful-stateless-widgets-in-flutter</guid>
<pubDate>Mon, 04 May 2026 03:34:50 +0000</pubDate>
</item>
<item>
<title>Top Package: GoRouter Advanced Navigation Tutorial 2026</title>
<link>https://coderlegion.com/11395/top-package-gorouter-advanced-navigation-tutorial-2026</link>
<description>&lt;p&gt;Advanced GoRouter tutorial—bottom nav with preserved state, nested routes, typed navigation, and auth redirects. GoRouter is Flutter's official best navigation system, stable in maintenance mode.&lt;br&gt;
Features: Deep linking, ShellRoute for tabs, typed routes via builder.&lt;/p&gt;
</description>
<category>Videos</category>
<guid isPermaLink="true">https://coderlegion.com/11395/top-package-gorouter-advanced-navigation-tutorial-2026</guid>
<pubDate>Tue, 10 Feb 2026 22:40:29 +0000</pubDate>
</item>
<item>
<title>The Hidden Gem of Flutter Lists: Unlocking scrollable_positioned_list's Superpowers</title>
<link>https://coderlegion.com/11390/the-hidden-gem-of-flutter-lists-unlocking-scrollablepositionedlists-superpowers</link>
<description>&lt;p&gt;As Flutter developers, we've all faced the common challenge: building lists that need more control than the standard &lt;code&gt;ListView&lt;/code&gt; provides. Whether it's scrolling to a specific index, tracking which items are visible, or implementing complex navigation patterns, Flutter's built-in widgets sometimes leave us wanting more.&lt;/p&gt;
&lt;p&gt;Enter &lt;strong&gt;scrollable_positioned_list&lt;/strong&gt; – a powerful yet often-overlooked package from Google's flutter.widgets repository that solves these problems elegantly.&lt;/p&gt;
&lt;p&gt;In this comprehensive guide, we'll explore the hidden gems of this package and demonstrate how it can transform your Flutter applications.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-scrollable-positioned-list-matters&quot;&gt;Why scrollable_positioned_list Matters&lt;/h3&gt;
&lt;h4 id=&quot;the-problem-with-standard-listview&quot;&gt;The Problem with Standard ListView&lt;/h4&gt;
&lt;p&gt;Flutter's &lt;code&gt;ListView&lt;/code&gt; is excellent for basic scrolling, but it has significant limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No built-in &lt;code&gt;scrollToIndex()&lt;/code&gt;&lt;/strong&gt; – You must develop custom solutions to measure element offsets&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No visibility detection&lt;/strong&gt; – Determining which items are currently visible requires complex workarounds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited programmatic control&lt;/strong&gt; – Jumping to specific items is non-trivial&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-scrollable-positioned-list-solution&quot;&gt;The scrollable_positioned_list Solution&lt;/h4&gt;
&lt;p&gt;This package addresses all these limitations with a clean, well-documented API that maintains the familiar &lt;code&gt;ListView.builder&lt;/code&gt; pattern while adding powerful capabilities.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;According to the &lt;a rel=&quot;nofollow&quot; href=&quot;https://pub.dev/documentation/scrollable_positioned_list/latest/&quot;&gt;official documentation&lt;/a&gt;&lt;/strong&gt;, this widget allows scrolling to a specific item and determining what items are currently visible – exactly what advanced Flutter applications need.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;installation-and-setup&quot;&gt;Installation and Setup&lt;/h3&gt;
&lt;p&gt;Add the package to your &lt;code&gt;pubspec.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;dependencies:
  scrollable_positioned_list: ^0.3.8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then import it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;hidden-gem-1-programmatic-scrolling-with-scrolltoindex&quot;&gt;Hidden Gem #1: Programmatic Scrolling with scrollToIndex&lt;/h3&gt;
&lt;p&gt;The most powerful feature is the ability to scroll to any item programmatically – functionality that standard &lt;code&gt;ListView&lt;/code&gt; simply doesn't provide.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class ScrollableListExample extends StatefulWidget {
  @override
  _ScrollableListExampleState createState() =&amp;gt; _ScrollableListExampleState();
}

class _ScrollableListExampleState extends State&amp;lt;ScrollableListExample&amp;gt; {
  final ItemScrollController _itemScrollController = ItemScrollController();
  final ItemPositionsNotifier _itemPositionsNotifier = ItemPositionsNotifier();
  final List&amp;lt;String&amp;gt; _items = List.generate(100, (index) =&amp;gt; 'Item ${index + 1}');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ScrollablePositionedList Demo')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(onPressed: () =&amp;gt; _scrollToIndex(0), child: const Text('To Start')),
                ElevatedButton(onPressed: () =&amp;gt; _scrollToIndex(_items.length - 1), child: const Text('To End')),
                ElevatedButton(onPressed: () =&amp;gt; _scrollToIndex(49), child: const Text('To Middle')),
              ],
            ),
          ),
          Expanded(
            child: ScrollablePositionedList.builder(
              itemCount: _items.length,
              itemScrollController: _itemScrollController,
              itemPositionsNotifier: _itemPositionsNotifier,
              itemBuilder: (context, index) =&amp;gt; ListTile(
                title: Text(_items[index]),
                subtitle: Text('Position: ${index + 1}'),
              ),
            ),
          ),
        ],
      ),
    );
  }

  void _scrollToIndex(int index) {
    _itemScrollController.scrollTo(index: index, duration: const Duration(seconds: 2), curve: Curves.easeInOutCubic);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Advanced Options:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;// Jump immediately (no animation)
void _jumpToIndex(int index) =&amp;gt; _itemScrollController.jumpTo(index: index);

// Scroll with custom alignment (0.0 = top, 0.5 = center, 1.0 = bottom)
void _scrollToCenter(int index) {
  _itemScrollController.scrollTo(index: index, alignment: 0.5, duration: const Duration(milliseconds: 500));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;hidden-gem-2-itempositionlistener-tracking-what-s-visible&quot;&gt;Hidden Gem #2: ItemPositionListener – Tracking What's Visible&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;ItemPositionListener&lt;/code&gt; monitors which items are visible – perfect for reading progress, lazy loading, or scroll-synced animations.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class ReadingProgressExample extends StatefulWidget {
  @override
  _ReadingProgressExampleState createState() =&amp;gt; _ReadingProgressExampleState();
}

class _ReadingProgressExampleState extends State&amp;lt;ReadingProgressExample&amp;gt; {
  final ItemPositionsNotifier _itemPositionsNotifier = ItemPositionsNotifier();
  double _readingProgress = 0.0;

  @override
  void initState() {
    super.initState();
    _itemPositionsNotifier.itemPositions.addListener(_updateProgress);
  }

  @override
  void dispose() {
    _itemPositionsNotifier.itemPositions.removeListener(_updateProgress);
    super.dispose();
  }

  void _updateProgress() {
    final positions = _itemPositionsNotifier.itemPositions.value;
    if (positions.isNotEmpty) {
      final minIndex = positions.where((p) =&amp;gt; p.itemLeadingEdge &amp;lt; 1).reduce((min, p) =&amp;gt; p.itemLeadingEdge &amp;lt; min.itemLeadingEdge ? p : min).index;
      final maxIndex = positions.where((p) =&amp;gt; p.itemTrailingEdge &amp;gt; 0).reduce((max, p) =&amp;gt; p.itemTrailingEdge &amp;gt; max.itemTrailingEdge ? p : max).index;
      setState(() =&amp;gt; _readingProgress = ((minIndex + maxIndex) / 200).clamp(0.0, 1.0));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Reading Progress'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: LinearProgressIndicator(value: _readingProgress))),
      body: ScrollablePositionedList.builder(itemCount: 100, itemPositionsNotifier: _itemPositionsNotifier, itemBuilder: (context, index) =&amp;gt; Padding(padding: const EdgeInsets.all(16.0), child: Text('Chapter ${index + 1}', style: const TextStyle(fontSize: 18)))),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Detecting Fully Visible Items:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;bool isItemFullyVisible(ItemPosition position) =&amp;gt; position.itemLeadingEdge &amp;gt;= 0 &amp;amp;&amp;amp; position.itemTrailingEdge &amp;lt;= 1;

List&amp;lt;int&amp;gt; getFullyVisibleIndices() =&amp;gt; _itemPositionsNotifier.itemPositions.value.where(isItemFullyVisible).map((p) =&amp;gt; p.index).toList();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;hidden-gem-3-variable-height-items-support&quot;&gt;Hidden Gem #3: Variable Height Items Support&lt;/h3&gt;
&lt;p&gt;This package handles items of varying heights gracefully – crucial for dynamic content apps.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class VariableHeightListExample extends StatelessWidget {
  final ItemScrollController _scrollController = ItemScrollController();
  final List&amp;lt;_ContentItem&amp;gt; _items = List.generate(50, (index) {
    final lines = (index % 5) + 1;
    return _ContentItem(title: 'Article ${index + 1}', content: 'This article has $lines lines.\n' * lines);
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Variable Height List'), actions: [
        IconButton(icon: const Icon(Icons.arrow_downward), onPressed: () =&amp;gt; _scrollController.scrollTo(index: _items.length - 1, duration: const Duration(seconds: 2)))
      ]),
      body: ScrollablePositionedList.builder(
        itemCount: _items.length,
        itemScrollController: _scrollController,
        itemBuilder: (context, index) =&amp;gt; Card(
          margin: const EdgeInsets.all(8),
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
              Text(_items[index].title, style: Theme.of(context).textTheme.titleLarge),
              const SizedBox(height: 8), Text(_items[index].content)
            ]),
          ),
        ),
      ),
    );
  }
}

class _ContentItem {
  final String title, content;
  _ContentItem({required this.title, required this.content});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;hidden-gem-4-scroll-offset-tracking&quot;&gt;Hidden Gem #4: Scroll Offset Tracking&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class ScrollOffsetExample extends StatefulWidget {
  @override
  _ScrollOffsetExampleState createState() =&amp;gt; _ScrollOffsetExampleState();
}

class _ScrollOffsetExampleState extends State&amp;lt;ScrollOffsetExample&amp;gt; {
  final ItemPositionsNotifier _positionsNotifier = ItemPositionsNotifier();
  final ItemScrollController _scrollController = ItemScrollController();
  double _currentScrollOffset = 0.0;

  @override
  void initState() {
    super.initState();
    _positionsNotifier.itemPositions.addListener(() {
      final positions = _positionsNotifier.itemPositions.value;
      if (positions.isNotEmpty) setState(() =&amp;gt; _currentScrollOffset = positions.first.itemLeadingEdge);
    });
  }

  @override
  void dispose() {
    _positionsNotifier.itemPositions.removeListener(() {});
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Offset: ${_currentScrollOffset.toStringAsFixed(2)}')),
      body: Stack(
        children: [
          Positioned(top: -_currentScrollOffset * 0.5, left: 0, right: 0, child: Container(height: 200, decoration: BoxDecoration(gradient: LinearGradient(colors: [Colors.blue.shade200, Colors.purple.shade200])))),
          ScrollablePositionedList.builder(
            itemCount: 50,
            itemScrollController: _scrollController,
            itemPositionsNotifier: _positionsNotifier,
            padding: const EdgeInsets.only(top: 150),
            itemBuilder: (context, index) =&amp;gt; ListTile(title: Text('Item ${index + 1}'), subtitle: Text('Offset: ${_currentScrollOffset.toStringAsFixed(2)}')),
          ),
        ],
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;hidden-gem-5-scroll-spy-navigation&quot;&gt;Hidden Gem #5: Scroll Spy Navigation&lt;/h3&gt;
&lt;p&gt;Implement a table of contents that highlights the current section – perfect for documentation sites.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class ScrollSpyExample extends StatefulWidget {
  @override
  _ScrollSpyExampleState createState() =&amp;gt; _ScrollSpyExampleState();
}

class _ScrollSpyExampleState extends State&amp;lt;ScrollSpyExample&amp;gt; {
  final ItemScrollController _scrollController = ItemScrollController();
  final ItemPositionsNotifier _positionsNotifier = ItemPositionsNotifier();
  final List&amp;lt;String&amp;gt; _sections = List.generate(20, (index) =&amp;gt; 'Section ${index + 1}');
  int _activeSection = 0;

  @override
  void initState() {
    super.initState();
    _positionsNotifier.itemPositions.addListener(() {
      final positions = _positionsNotifier.itemPositions.value;
      if (positions.isNotEmpty) {
        final mostVisible = positions.reduce((a, b) =&amp;gt; (a.itemLeadingEdge.abs() + a.itemTrailingEdge.abs()) / 2 &amp;lt; (b.itemLeadingEdge.abs() + b.itemTrailingEdge.abs()) / 2 ? a : b);
        if (_activeSection != mostVisible.index) setState(() =&amp;gt; _activeSection = mostVisible.index);
      }
    });
  }

  @override
  void dispose() {
    _positionsNotifier.itemPositions.removeListener(() {});
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Scroll Spy Navigation')),
      body: Row(
        children: [
          Container(
            width: 200,
            color: Colors.grey[100],
            child: ListView.builder(
              itemCount: _sections.length,
              itemBuilder: (context, index) {
                final isActive = index == _activeSection;
                return ListTile(
                  title: Text(_sections[index], style: TextStyle(fontWeight: isActive ? FontWeight.bold : FontWeight.normal, color: isActive ? Colors.blue : Colors.black87)),
                  selected: isActive,
                  onTap: () =&amp;gt; _scrollController.scrollTo(index: index, duration: const Duration(milliseconds: 300), alignment: 0.0),
                );
              },
            ),
          ),
          Expanded(
            child: ScrollablePositionedList.builder(
              itemCount: _sections.length,
              itemScrollController: _scrollController,
              itemPositionsNotifier: _positionsNotifier,
              itemBuilder: (context, index) =&amp;gt; Container(height: 300, padding: const EdgeInsets.all(24), child: Text(_sections[index], style: Theme.of(context).textTheme.headlineMedium)),
            ),
          ),
        ],
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;common-pitfalls-and-solutions&quot;&gt;Common Pitfalls and Solutions&lt;/h3&gt;
&lt;h4 id=&quot;pitfall-1-controller-not-attached&quot;&gt;Pitfall 1: Controller Not Attached&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Calling &lt;code&gt;scrollTo&lt;/code&gt; before the controller is attached.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;void _safeScrollTo(int index) {
  if (_itemScrollController.isAttached) {
    _itemScrollController.scrollTo(index: index);
  } else {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_itemScrollController.isAttached) _itemScrollController.scrollTo(index: index);
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;pitfall-2-customscrollview-incompatibility&quot;&gt;Pitfall 2: CustomScrollView Incompatibility&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Doesn't work well inside &lt;code&gt;CustomScrollView&lt;/code&gt; with other slivers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Use as standalone or consider &lt;code&gt;super_sliver_list&lt;/code&gt; for complex sliver scenarios. See &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/google/flutter.widgets/issues/32&quot;&gt;GitHub Issue #32&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;pitfall-3-alignment-confusion&quot;&gt;Pitfall 3: Alignment Confusion&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: The &lt;code&gt;alignment&lt;/code&gt; parameter specifies where the target item positions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0.0&lt;/code&gt; = Top of viewport&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0.5&lt;/code&gt; = Center of viewport&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1.0&lt;/code&gt; = Bottom of viewport&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;comparison-when-to-use-which-widget&quot;&gt;Comparison: When to Use Which Widget&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Feature &lt;/th&gt;&lt;th&gt;ListView &lt;/th&gt;&lt;th&gt;scrollable_positioned_list &lt;/th&gt;&lt;th&gt;CustomScrollView&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Basic scrolling &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;scrollToIndex() &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;❌&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Visibility detection &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;❌&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Variable height items &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Custom sliver integration &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;scrollable_positioned_list&lt;/code&gt; for:&lt;/strong&gt; scroll-to-index, visibility tracking, scroll-offset animations, reading progress, scroll spy navigation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use standard &lt;code&gt;ListView&lt;/code&gt; for:&lt;/strong&gt; basic scrolling, maximum performance, CustomScrollView integration.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;real-world-example-music-player-with-queue&quot;&gt;Real-World Example: Music Player with Queue&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dart&quot;&gt;class MusicPlayerQueue extends StatefulWidget {
  const MusicPlayerQueue({Key? key}) : super(key: key);
  @override
  _MusicPlayerQueueState createState() =&amp;gt; _MusicPlayerQueueState();
}

class _MusicPlayerQueueState extends State&amp;lt;MusicPlayerQueue&amp;gt; {
  final ItemScrollController _scrollController = ItemScrollController();
  final ItemPositionsNotifier _positionsNotifier = ItemPositionsNotifier();
  final List&amp;lt;Song&amp;gt; _playlist = List.generate(50, (index) =&amp;gt; Song(title: 'Track ${index + 1}', artist: 'Artist ${(index % 10) + 1}', duration: Duration(minutes: 3, seconds: (index * 7) % 60)));
  int _currentTrackIndex = 0;
  bool _isPlaying = false;

  void _playTrack(int index) {
    setState(() =&amp;gt; _currentTrackIndex = _currentTrackIndex = index);
    _scrollController.scrollTo(index: index, alignment: 0.5, duration: const Duration(milliseconds: 400), curve: Curves.easeInOut);
  }

  void _nextTrack() =&amp;gt; _playTrack((_currentTrackIndex + 1) % _playlist.length);
  void _previousTrack() =&amp;gt; _playTrack((_currentTrackIndex - 1 + _playlist.length) % _playlist.length);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Music Player'), actions: [
        IconButton(icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow), onPressed: () =&amp;gt; setState(() =&amp;gt; _isPlaying = !_isPlaying))
      ]),
      body: Column(
        children: [
          Container(
            padding: const EdgeInsets.all(16),
            color: Colors.blue.shade50,
            child: Row(children: [
              const Icon(Icons.music_note, size: 48),
              const SizedBox(width: 16),
              Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
                Text(_playlist[_currentTrackIndex].title, style: const TextStyle(fontWeight: FontWeight.bold)),
                Text(_playlist[_currentTrackIndex].artist),
                Text(_formatDuration(_playlist[_currentTrackIndex].duration))
              ])),
              IconButton(icon: const Icon(Icons.skip_previous), onPressed: _previousTrack),
              IconButton(icon: const Icon(Icons.skip_next), onPressed: _nextTrack)
            ]),
          ),
          Expanded(
            child: ScrollablePositionedList.builder(
              itemCount: _playlist.length,
              itemScrollController: _scrollController,
              itemPositionsNotifier: _positionsNotifier,
              itemBuilder: (context, index) {
                final song = _playlist[index];
                final isPlaying = index == _currentTrackIndex;
                return ListTile(
                  leading: CircleAvatar(backgroundColor: isPlaying ? Colors.blue : Colors.grey[300], child: Icon(isPlaying ? (_isPlaying ? Icons.pause : Icons.play_arrow) : Icons.music_note, color: isPlaying ? Colors.white : Colors.black54)),
                  title: Text(song.title, style: TextStyle(fontWeight: isPlaying ? FontWeight.bold : FontWeight.normal, color: isPlaying ? Colors.blue : null)),
                  subtitle: Text(song.artist),
                  trailing: Text(_formatDuration(song.duration)),
                  onTap: () =&amp;gt; _playTrack(index),
                );
              },
            ),
          ),
        ],
      ),
    );
  }

  String _formatDuration(Duration d) =&amp;gt; '${d.inMinutes}:${d.inSeconds.toString().padLeft(2, '0')}';
}

class Song {
  final String title, artist;
  final Duration duration;
  Song({required this.title, required this.artist, required this.duration});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;scrollable_positioned_list&lt;/code&gt; package is a hidden gem every Flutter developer should know:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Solves Real Problems&lt;/strong&gt; – Addresses &lt;code&gt;ListView&lt;/code&gt; limitations without complexity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean API&lt;/strong&gt; – Familiar &lt;code&gt;ListView.builder&lt;/code&gt; pattern with powerful features&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google-Maintained&lt;/strong&gt; – Part of &lt;code&gt;google/flutter.widgets&lt;/code&gt; repository&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Production-Ready&lt;/strong&gt; – Used in apps like API Dash&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feature-Rich&lt;/strong&gt; – &lt;code&gt;scrollToIndex()&lt;/code&gt;, visibility tracking, offset monitoring&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Perfect for:&lt;/strong&gt; playlist apps, documentation readers, chat apps, table of contents, any precise scroll control scenario.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;scrollable_positioned_list&lt;/code&gt; package fills a crucial gap in Flutter's ecosystem by providing &lt;code&gt;scrollToIndex()&lt;/code&gt;, visibility tracking, and scroll offset monitoring – all while maintaining the familiar &lt;code&gt;ListView.builder&lt;/code&gt; API. Try it in your next project!&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/11390/the-hidden-gem-of-flutter-lists-unlocking-scrollablepositionedlists-superpowers</guid>
<pubDate>Tue, 10 Feb 2026 19:56:35 +0000</pubDate>
</item>
<item>
<title> Why Theme Setup Matters (More Than You Think)</title>
<link>https://coderlegion.com/7468/why-theme-setup-matters-more-than-you-think</link>
<description>&lt;p&gt;Dark mode isn’t just aesthetics anymore — it’s part of the vibe. Users expect apps to feel native, polished, and flexible. Good theming isn’t just flipping background colors; it’s about creating a consistent design language that can scale across dozens of screens. Flutter makes this super smooth with its built-in theming system, but you’ve gotta wire it right from day one.&lt;/p&gt;
&lt;p&gt;So let's set up a proper Light + Dark theme structure that’s clean, maintainable, and future-proof.&lt;/p&gt;
&lt;p&gt;️ Step 1: Create Your Theme Files&lt;/p&gt;
&lt;p&gt;Don’t dump theme code directly into main.dart. That’s chaos energy. Instead, create a dedicated folder:&lt;/p&gt;
&lt;p&gt;lib/&lt;br&gt;
  theme/&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;light_theme.dart
dark_theme.dart
theme_colors.dart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example: light_theme.dart&lt;br&gt;
import 'package:flutter/material.dart';&lt;br&gt;
import 'theme_colors.dart';&lt;/p&gt;
&lt;p&gt;ThemeData lightTheme = ThemeData(&lt;br&gt;
  brightness: Brightness.light,&lt;br&gt;
  primaryColor: AppColors.primary,&lt;br&gt;
  scaffoldBackgroundColor: Colors.white,&lt;br&gt;
  appBarTheme: const AppBarTheme(&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;backgroundColor: Colors.white,
foregroundColor: Colors.black87,
elevation: 0,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ),&lt;br&gt;
  colorScheme: const ColorScheme.light(),&lt;br&gt;
);&lt;/p&gt;
&lt;p&gt;Example: dark_theme.dart&lt;br&gt;
import 'package:flutter/material.dart';&lt;br&gt;
import 'theme_colors.dart';&lt;/p&gt;
&lt;p&gt;ThemeData darkTheme = ThemeData(&lt;br&gt;
  brightness: Brightness.dark,&lt;br&gt;
  primaryColor: AppColors.primary,&lt;br&gt;
  scaffoldBackgroundColor: Colors.black,&lt;br&gt;
  appBarTheme: const AppBarTheme(&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;backgroundColor: Colors.black,
foregroundColor: Colors.white,
elevation: 0,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ),&lt;br&gt;
  colorScheme: const ColorScheme.dark(),&lt;br&gt;
);&lt;/p&gt;
&lt;p&gt;Example: theme_colors.dart&lt;br&gt;
import 'package:flutter/material.dart';&lt;/p&gt;
&lt;p&gt;class AppColors {&lt;br&gt;
  static const primary = Color(0xFF6750A4);&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;Separating colors like this makes it easier to manage branding changes without nuking your whole codebase.&lt;/p&gt;
&lt;p&gt; Step 2: Set Up Theme Switching&lt;/p&gt;
&lt;p&gt;Most apps either follow system theme automatically or give users a toggle. Flutter makes both easy.&lt;/p&gt;
&lt;p&gt;Inside main.dart:&lt;br&gt;
import 'package:flutter/material.dart';&lt;br&gt;
import 'theme/light_theme.dart';&lt;br&gt;
import 'theme/dark_theme.dart';&lt;/p&gt;
&lt;p&gt;void main() {&lt;br&gt;
  runApp(const MyApp());&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;class MyApp extends StatefulWidget {&lt;br&gt;
  const MyApp({super.key});&lt;/p&gt;
&lt;p&gt;  @override&lt;br&gt;
  State createState() =&amp;gt; _MyAppState();&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;class _MyAppState extends State {&lt;br&gt;
  ThemeMode themeMode = ThemeMode.system;&lt;/p&gt;
&lt;p&gt;  @override&lt;br&gt;
  Widget build(BuildContext context) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return MaterialApp(
  title: 'Themed App',
  theme: lightTheme,
  darkTheme: darkTheme,
  themeMode: themeMode,
  home: HomeScreen(
    onThemeChange: (mode) {
      setState(() =&amp;gt; themeMode = mode);
    },
  ),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;This gives your app three modes:&lt;br&gt;
ThemeMode.light&lt;br&gt;
ThemeMode.dark&lt;br&gt;
ThemeMode.system (the smart default)&lt;/p&gt;
&lt;p&gt;️ Step 3: Add a Toggle Switch in Your UI&lt;br&gt;
Give users control. At minimum, drop a Switch somewhere in your settings screen.&lt;br&gt;
Example settings widget:&lt;br&gt;
class HomeScreen extends StatelessWidget {&lt;br&gt;
  final Function(ThemeMode) onThemeChange;&lt;br&gt;
  const HomeScreen({super.key, required this.onThemeChange});&lt;/p&gt;
&lt;p&gt;  @override&lt;br&gt;
  Widget build(BuildContext context) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final currentBrightness = MediaQuery.of(context).platformBrightness;

return Scaffold(
  appBar: AppBar(title: const Text('Theme Demo')),
  body: Column(
    children: [
      ListTile(
        title: const Text(&quot;Light Mode&quot;),
        leading: const Icon(Icons.light_mode),
        onTap: () =&amp;gt; onThemeChange(ThemeMode.light),
      ),
      ListTile(
        title: const Text(&quot;Dark Mode&quot;),
        leading: const Icon(Icons.dark_mode),
        onTap: () =&amp;gt; onThemeChange(ThemeMode.dark),
      ),
      ListTile(
        title: const Text(&quot;System Default&quot;),
        leading: const Icon(Icons.phone_android),
        subtitle: Text(&quot;Current: $currentBrightness&quot;),
        onTap: () =&amp;gt; onThemeChange(ThemeMode.system),
      ),
    ],
  ),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;This setup is simple but super effective — and instantly feels like a real, production app.&lt;/p&gt;
&lt;p&gt; Step 4: Customize Widgets with Theme Extensions (Pro Level)&lt;/p&gt;
&lt;p&gt;Once your app grows, you’ll hit the limits of stock ThemeData. That’s where Theme Extensions come in — letting you build your own reusable style blocks.&lt;/p&gt;
&lt;p&gt;Example:&lt;br&gt;
class ExtraColors extends ThemeExtension {&lt;br&gt;
  final Color cardBackground;&lt;br&gt;
  const ExtraColors({required this.cardBackground});&lt;/p&gt;
&lt;p&gt;  @override&lt;br&gt;
  ExtraColors copyWith({Color? cardBackground}) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return ExtraColors(cardBackground: cardBackground ?? this.cardBackground);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;/p&gt;
&lt;p&gt;  @override&lt;br&gt;
  ExtraColors lerp(ThemeExtension? other, double t) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (other is! ExtraColors) return this;
return ExtraColors(
  cardBackground: Color.lerp(cardBackground, other.cardBackground, t)!,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;Add it inside your theme:&lt;/p&gt;
&lt;p&gt;extensions: const [
  ExtraColors(cardBackground: Colors.grey),
],&lt;/p&gt;
&lt;p&gt;This is the kinda structure big apps use — scalable, organized, clean.&lt;/p&gt;
&lt;p&gt; Step 5: Test Like a Pro&lt;br&gt;
Don’t trust your eyes; test with Flutter’s built-in theme toggling.&lt;/p&gt;
&lt;p&gt;Run:&lt;br&gt;
flutter run --dart-define=theme=light&lt;br&gt;
flutter run --dart-define=theme=dark&lt;/p&gt;
&lt;p&gt;Or in DevTools:&lt;br&gt;
Click the &quot;Device Settings&quot; tab&lt;br&gt;
Flip system theme live&lt;br&gt;
Watch your app glow up&lt;/p&gt;
&lt;p&gt; Final Thoughts&lt;br&gt;
Dark mode isn’t &quot;optional polish&quot; anymore. It’s part of the standard UX checklist. Flutter gives you a ridiculously good theming system out of the box — but only if you structure it right. With separate theme files, ThemeMode switching, extensions, and organized color management, your app instantly jumps into that premium tier where everything feels intentional.&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12306083947827536952&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
</description>
<category>Mobile Dev</category>
<guid isPermaLink="true">https://coderlegion.com/7468/why-theme-setup-matters-more-than-you-think</guid>
<pubDate>Tue, 18 Nov 2025 10:30:26 +0000</pubDate>
</item>
<item>
<title>Building “R U Ready” – My Flutter Dating App Development Journey</title>
<link>https://coderlegion.com/7030/building-r-u-ready-my-flutter-dating-app-development-journey</link>
<description>&lt;p&gt;I’ve been working on a new project called R U Ready, a full-featured Flutter dating app, and it’s been one of the most technically enriching experiences in my Flutter journey.&lt;/p&gt;
&lt;p&gt;The goal is to build a real-time, scalable, and intuitive dating platform using Flutter + Firebase as the core stack. I started with a modular architecture to ensure clean separation between UI, logic, and data layers — applying principles of Clean Architecture and MVVM for maintainability.&lt;/p&gt;
&lt;p&gt;Key modules include:&lt;/p&gt;
&lt;p&gt;Authentication Layer using Firebase Auth (Email/Mobile)&lt;/p&gt;
&lt;p&gt;Profile Management with dynamic Firestore data binding&lt;/p&gt;
&lt;p&gt;Swipe Engine built with custom animation controllers for smooth profile transitions&lt;/p&gt;
&lt;p&gt;Chat System using Firestore real-time streams and optimized query listeners&lt;/p&gt;
&lt;p&gt;App Theming &amp;amp; State Management implemented via Provider and ChangeNotifier for reactive UI updates&lt;/p&gt;
&lt;p&gt;Throughout the process, I’ve learned how to fine-tune Firestore query performance, implement pagination, handle context-safe navigation, and optimize rebuilds using widget lifecycle awareness.&lt;/p&gt;
&lt;p&gt;Currently, I’m focusing on extending the architecture for Reels and Group Chat features while integrating AdMob monetization and in-app upgrades.&lt;/p&gt;
&lt;p&gt;Every commit teaches me something new about building production-grade apps with Flutter. &lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=17006208594055898090&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
</description>
<category>Personal Branding</category>
<guid isPermaLink="true">https://coderlegion.com/7030/building-r-u-ready-my-flutter-dating-app-development-journey</guid>
<pubDate>Tue, 04 Nov 2025 11:59:38 +0000</pubDate>
</item>
<item>
<title>Why I Started Creating Models in My Flutter Project</title>
<link>https://coderlegion.com/4879/why-i-started-creating-models-in-my-flutter-project</link>
<description>&lt;p&gt;When I was building my music app in Flutter, I began with the “quick and dirty” way of handling data. You know that vibe coding flow — fetch some JSON, dump it in a &lt;code&gt;Map&amp;lt;String, dynamic&amp;gt;&lt;/code&gt;, and just wire it up to the UI. It felt fast at first, but it quickly turned into a mess.&lt;/p&gt;
&lt;p&gt;Take this example: I was fetching playback data from my service and passing it around like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class PlaybackWidget extends StatelessWidget {
  final Map&amp;lt;String, dynamic&amp;gt; playbackData;

  PlaybackWidget({required this.playbackData});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(&quot;Now playing: ${playbackData['title']}&quot;),
        Text(&quot;Artist: ${playbackData['artist']}&quot;),
        Text(&quot;Mode: ${playbackData['playbackMode']}&quot;),
      ],
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It worked. But every time I typed &lt;code&gt;playbackData['title']&lt;/code&gt;, I felt uneasy. One typo like &lt;code&gt;playbackData['titel']&lt;/code&gt; and boom — runtime error. On top of that, the widget didn’t tell me what kind of data it really expected. Was playbackMode a string? An enum? A number? I had to keep it all in my head.&lt;/p&gt;
&lt;h3 id=&quot;switching-to-models&quot;&gt;Switching to Models&lt;/h3&gt;
&lt;p&gt;After wrestling with this a few times, I finally decided to create a proper model for my playback data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum PlaybackMode { repeat, shuffle, normal }

class Playback {
  final String title;
  final String artist;
  final PlaybackMode playbackMode;

  Playback({
    required this.title,
    required this.artist,
    required this.playbackMode,
  });

  factory Playback.fromJson(Map&amp;lt;String, dynamic&amp;gt; json) {
    return Playback(
      title: json['title'],
      artist: json['artist'],
      playbackMode: PlaybackMode.values.firstWhere(
        (e) =&amp;gt; e.toString() == 'PlaybackMode.${json['playbackMode']}',
      ),
    );
  }

  Map&amp;lt;String, dynamic&amp;gt; toJson() {
    return {
      'title': title,
      'artist': artist,
      'playbackMode': playbackMode.name,
    };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now my widget looked much cleaner:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class PlaybackWidget extends StatelessWidget {
  final Playback playback;

  PlaybackWidget({required this.playback});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(&quot;Now playing: ${playback.title}&quot;),
        Text(&quot;Artist: ${playback.artist}&quot;),
        Text(&quot;Mode: ${playback.playbackMode.name}&quot;),
      ],
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Suddenly everything felt safer. Autocomplete worked, I didn’t worry about typos, and my code told me exactly what kind of data I was dealing with.&lt;/p&gt;
&lt;h3 id=&quot;what-i-learned&quot;&gt;What I Learned&lt;/h3&gt;
&lt;p&gt;Models might look like “extra code” at first, but in reality, they save time and mental energy. Instead of juggling raw maps everywhere, you give your app a solid data structure to lean on.&lt;/p&gt;
&lt;p&gt;In my case, once I switched to models in my music app, my code got way easier to maintain. Adding new fields, refactoring, or passing data between widgets stopped being a headache.&lt;/p&gt;
&lt;p&gt;So if you’re just hacking away in Flutter and think models are overkill — trust me, they’re not. Future-you will thank present-you for adding them.&lt;/p&gt;
</description>
<category>Database</category>
<guid isPermaLink="true">https://coderlegion.com/4879/why-i-started-creating-models-in-my-flutter-project</guid>
<pubDate>Wed, 20 Aug 2025 18:01:17 +0000</pubDate>
</item>
<item>
<title>How I Paid Off a Little Technical Debt in My Flutter Music App</title>
<link>https://coderlegion.com/4746/how-i-paid-off-a-little-technical-debt-in-my-flutter-music-app</link>
<description>&lt;p&gt;So here’s the story.&lt;br&gt;
I was facing a &lt;strong&gt;technical debt kind of issue&lt;/strong&gt; in my Flutter music app — my &lt;code&gt;MusicPlaybackService&lt;/code&gt; was doing &lt;em&gt;two&lt;/em&gt; jobs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Talking to the audio engine (&lt;code&gt;audio_service&lt;/code&gt; + &lt;code&gt;just_audio&lt;/code&gt;) to play/pause/skip.&lt;/li&gt;
&lt;li&gt;Acting like a &lt;code&gt;ChangeNotifier&lt;/code&gt; to update the UI about song lists, current song, etc.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;why-this-was-bad&quot;&gt;&lt;strong&gt;Why this was bad&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;At first, it felt convenient.&lt;br&gt;
&quot;Why not just let one class handle everything, right?&quot; &lt;/p&gt;
&lt;p&gt;But here’s the problem with &lt;strong&gt;one giant class doing multiple jobs&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unnecessary widget rebuilds&lt;/strong&gt;: Even small playback state changes could cause the entire UI to refresh.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Harder debugging&lt;/strong&gt;: Was a bug coming from the UI or the audio engine? You never know without digging deep.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tight coupling&lt;/strong&gt;: Any change in playback logic risked breaking the UI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Poor testability&lt;/strong&gt;: Couldn’t mock or test the UI without pulling in the full audio player.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-refactor&quot;&gt;&lt;strong&gt;The Refactor&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;I split the responsibilities like this:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MusicPlaybackService
   ↳ Handles playback (play/pause/skip)
   ↳ Stores allSongs, currentSong
   ↳ Notifies UI of changes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MusicPlaybackService   → Only handles audio playback logic
PlaybackProvider       → Holds UI-facing state &amp;amp; notifies widgets
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;how-the-new-structure-works&quot;&gt;&lt;strong&gt;How the new structure works&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MusicPlaybackService&lt;/code&gt; → Talks to the background audio handler, keeps pure playback logic. No &lt;code&gt;notifyListeners()&lt;/code&gt; here.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PlaybackProvider&lt;/code&gt; → Watches the service, stores the current song list, play state, etc., and &lt;em&gt;only&lt;/em&gt; notifies the UI when something UI-relevant changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;benefits-i-noticed-immediately&quot;&gt;&lt;strong&gt;Benefits I noticed immediately&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Fewer unnecessary rebuilds&lt;/strong&gt; → smoother scrolling, faster tab switches.&lt;br&gt;
&lt;strong&gt;Easier debugging&lt;/strong&gt; → I instantly know whether an issue is in the audio engine or UI.&lt;br&gt;
&lt;strong&gt;More modular code&lt;/strong&gt; → I can swap playback logic without rewriting UI state.&lt;br&gt;
&lt;strong&gt;Better testing&lt;/strong&gt; → I can mock the provider without worrying about actual audio playback.&lt;/p&gt;
&lt;h4 id=&quot;lesson-learned&quot;&gt;&lt;strong&gt;Lesson learned&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;If your service class is doing &lt;strong&gt;too much&lt;/strong&gt; — step back and split the responsibilities.&lt;br&gt;
It’s like having a drummer who’s also trying to do the vocals and mix the sound live. Eventually… something’s going to be off-beat. &lt;/p&gt;
</description>
<category>State Management</category>
<guid isPermaLink="true">https://coderlegion.com/4746/how-i-paid-off-a-little-technical-debt-in-my-flutter-music-app</guid>
<pubDate>Fri, 15 Aug 2025 12:08:11 +0000</pubDate>
</item>
<item>
<title>When to Use setState, Provider, and Consumer in Flutter</title>
<link>https://coderlegion.com/4569/when-to-use-setstate-provider-and-consumer-in-flutter</link>
<description>&lt;p&gt;Imagine you’re building a music player app &lt;br&gt;
You have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A Play button on the player screen&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Mini Player at the bottom of the home screen&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Playlist screen showing all songs&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now… Flutter gives you multiple ways to manage data and update the UI.&lt;br&gt;
Three of the most common are:&lt;/p&gt;
&lt;h3 id=&quot;setstate-the-i-just-need-to-refresh-this-widget-approach&quot;&gt;setState — The “I just need to refresh this widget” approach&lt;/h3&gt;
&lt;p&gt;Think of setState as saying:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;“Hey Flutter, just repaint this small part of the UI because something here changed.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When to use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Simple, temporary state&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Widget-specific changes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Doesn’t need to be shared across the app&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Music App Example:&lt;/strong&gt;&lt;br&gt;
Updating the Play/Pause button icon when the user taps it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class PlayButton extends StatefulWidget {
  @override
  _PlayButtonState createState() =&amp;gt; _PlayButtonState();
}

class _PlayButtonState extends State&amp;lt;PlayButton&amp;gt; {
  bool isPlaying = false;

  void togglePlay() {
    setState(() {
      isPlaying = !isPlaying; // just flip the icon state
    });
  }

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow),
      onPressed: togglePlay,
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, only the Play button changes — no need to bother the rest of the app.&lt;/p&gt;
&lt;h3 id=&quot;provider-the-shared-state-manager-&quot;&gt;Provider — The “shared state manager”&lt;/h3&gt;
&lt;p&gt;Think of Provider as:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;“I have data that multiple widgets need to know about, even if they’re far apart.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When to use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;App-wide or screen-wide shared data&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;State needs to survive across multiple widgets/screens&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Perfect for your music queue, playback status, user preferences&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Music App Example:&lt;/strong&gt;&lt;br&gt;
Keeping track of the currently playing song so both Mini Player and Full Player Screen update together.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MusicProvider extends ChangeNotifier {
  String? currentSong;

  void playSong(String song) {
    currentSong = song;
    notifyListeners(); // tell all listening widgets to rebuild
  }
}

// Provide at top level
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) =&amp;gt; MusicProvider(),
      child: MyApp(),
    ),
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can read this currentSong anywhere in your app — that’s the magic.&lt;/p&gt;
&lt;h3 id=&quot;consumer-the-i-only-want-to-rebuild-this-tiny-part-trick&quot;&gt;Consumer — The “I only want to rebuild this tiny part” trick&lt;/h3&gt;
&lt;p&gt;Think of Consumer as:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;“I’ll listen to the Provider’s changes, but only rebuild this small widget, not the whole screen.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Optimize performance (avoid rebuilding the entire widget tree)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Listen to changes in one spot only&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Music App Example:&lt;/strong&gt;&lt;br&gt;
Updating just the song title in the mini player without reloading the play button or progress bar unnecessarily.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MiniPlayer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.grey[900],
      child: Row(
        children: [
          Consumer&amp;lt;MusicProvider&amp;gt;(
            builder: (context, music, _) {
              return Text(
                music.currentSong ?? &quot;No song playing&quot;,
                style: TextStyle(color: Colors.white),
              );
            },
          ),
          Icon(Icons.play_arrow, color: Colors.white),
        ],
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only the Text widget rebuilds when the song changes — everything else stays put.&lt;/p&gt;
</description>
<category>State Management</category>
<guid isPermaLink="true">https://coderlegion.com/4569/when-to-use-setstate-provider-and-consumer-in-flutter</guid>
<pubDate>Mon, 11 Aug 2025 10:32:32 +0000</pubDate>
</item>
<item>
<title>Why does my Flutter music player jump from 0:00 to actual time? (One-liner fix inside!)</title>
<link>https://coderlegion.com/4488/why-does-my-flutter-music-player-jump-from-0-00-to-actual-time-one-liner-fix-inside</link>
<description>&lt;p&gt;I hit a weird (but common) UI issue in my Flutter music player app:&lt;/p&gt;
&lt;p&gt;Whenever I tapped the &lt;strong&gt;mini player&lt;/strong&gt; to open the full screen player, the &lt;strong&gt;seek bar&lt;/strong&gt; would flash 0:00 for a split second — and then jump to the actual current timestamp.&lt;/p&gt;
&lt;p&gt;It looked like &lt;strong&gt;playback was restarting&lt;/strong&gt; every time. Spoiler: it wasn’t. &lt;/p&gt;
&lt;h3 id=&quot;what-was-happening-&quot;&gt;What was happening?&lt;/h3&gt;
&lt;p&gt;The full screen player uses a &lt;strong&gt;StreamBuilde&lt;/strong&gt;r to listen to the current position (&lt;strong&gt;Stream&lt;/strong&gt;). But when the widget builds, that stream hasn’t emitted anything yet — so it shows 0:00 by default.&lt;/p&gt;
&lt;p&gt;Only after the stream sends the actual position does the UI update. That tiny delay causes the annoying visual &quot;jump&quot;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;initialData: musicService.currentPosition,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding this to the StreamBuilder gives it a starting value immediately — the latest position we already have from our service.&lt;/p&gt;
&lt;p&gt;So now the UI starts at the right place without waiting for the stream to emit.&lt;/p&gt;
&lt;p&gt;No more flicker. No more weird jumps. Just smooth playback &lt;/p&gt;
&lt;p&gt;Hope this helps someone else!&lt;/p&gt;
</description>
<category>Stateful</category>
<guid isPermaLink="true">https://coderlegion.com/4488/why-does-my-flutter-music-player-jump-from-0-00-to-actual-time-one-liner-fix-inside</guid>
<pubDate>Fri, 08 Aug 2025 05:35:20 +0000</pubDate>
</item>
<item>
<title>Music App Bug: Songs Broke on Switching — Here’s Why</title>
<link>https://coderlegion.com/4404/music-app-bug-songs-broke-on-switching-heres-why</link>
<description>&lt;p&gt;I was facing this annoying issue in Flutter music player:&lt;/p&gt;
&lt;p&gt;When you tap songs quickly (like switching from one to another), playback breaks, mini player disappears, and you have to tap again to play. It was 2-3 times out of every 10 switch. &lt;/p&gt;
&lt;p&gt;It took me a while to trace it. I tried:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Making our playback function async&lt;/li&gt;
&lt;li&gt;Adding delays&lt;/li&gt;
&lt;li&gt;Debugging tap triggers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But nothing worked.&lt;/p&gt;
&lt;p&gt;Then I found the real culprit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await _player.stop(); //  this innocent-looking line was breaking everything
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-wrong-code&quot;&gt;The Wrong Code&lt;/h3&gt;
&lt;p&gt;In the background audio handler's updateQueue() method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Future&amp;lt;void&amp;gt; updateQueue(List&amp;lt;MediaItem&amp;gt; newQueue) async {
  final newAudioSources = newQueue.map((item) {
    final uri = item.extras?['uri'] as String;
    return AudioSource.uri(Uri.parse(uri));
  }).toList();

  await _player.setAudioSource(_playlist, initialIndex: 0);
  await _player.stop(); // ❌ this line breaks playback!

  queue.add(newQueue);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-s-the-problem-&quot;&gt;What’s the Problem?&lt;/h3&gt;
&lt;p&gt;This creates a race condition:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;setAudioSource() loads and prepares the new song(s).&lt;/li&gt;
&lt;li&gt;Immediately after, stop() wipes the loaded source.&lt;/li&gt;
&lt;li&gt;So by the time the player is supposed to play, there's nothing left&lt;br&gt;
to play.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This bug especially shows up during rapid song switching, because set -&amp;gt; stop -&amp;gt; play happens too fast and gets out of sync.&lt;/p&gt;
&lt;h3 id=&quot;the-right-code&quot;&gt;The Right Code&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Future&amp;lt;void&amp;gt; updateQueue(List&amp;lt;MediaItem&amp;gt; newQueue) async {
  final newAudioSources = newQueue.map((item) {
    final uri = item.extras?['uri'] as String;
    return AudioSource.uri(Uri.parse(uri));
  }).toList();

  _playlist.clear();
  _playlist.addAll(newAudioSources);

  await _player.setAudioSource(_playlist, initialIndex: 0);

  if (_player.playing) {
    await _player.play();
  }

  queue.add(newQueue);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you're using just_audio and audio_service, never call stop() right after setAudioSource().&lt;br&gt;
You're basically loading a song and immediately throwing it away.&lt;/p&gt;
&lt;p&gt;Let the player handle resets internally unless you truly need to stop everything.&lt;/p&gt;
&lt;p&gt;Happy building! &lt;/p&gt;
</description>
<category>Mobile Dev</category>
<guid isPermaLink="true">https://coderlegion.com/4404/music-app-bug-songs-broke-on-switching-heres-why</guid>
<pubDate>Sun, 03 Aug 2025 22:32:31 +0000</pubDate>
</item>
<item>
<title>Query Smarter, Not Harder</title>
<link>https://coderlegion.com/4282/query-smarter-not-harder</link>
<description>&lt;p&gt;If you're building a music app with on_audio_query, it’s tempting to call querySongs() everywhere—home screen, playlists, search, etc. But this means repeated storage scans → slower UI, battery drain, and unnecessary complexity.&lt;/p&gt;
&lt;h3 id=&quot;the-wrong-way-querying-every-time&quot;&gt;The Wrong Way – Querying Every Time&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;final OnAudioQuery audioQuery = OnAudioQuery();

Future&amp;lt;List&amp;lt;SongModel&amp;gt;&amp;gt; getSongs() async {
  return await audioQuery.querySongs(); // Called in every screen
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In each screen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final songs = await getSongs(); // Repeats scan again
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-better-way-query-once-share-via-provider&quot;&gt;The Better Way – Query Once, Share via Provider&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Fetch once in a service (e.g., MusicPlaybackService):&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class MusicPlaybackService extends ChangeNotifier {
  List&amp;lt;SongModel&amp;gt; _allSongs = [];

  Future&amp;lt;void&amp;gt; loadSongs() async {
    final OnAudioQuery query = OnAudioQuery();
    _allSongs = await query.querySongs();
    notifyListeners();
  }

  List&amp;lt;SongModel&amp;gt; get allSongs =&amp;gt; _allSongs;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Provide it globally:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;runApp(
  ChangeNotifierProvider(
    create: (_) {
      final service = MusicPlaybackService();
      service.loadSongs(); // Fetch once
      return service;
    },
    child: MyApp(),
  ),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Reuse anywhere without re-querying:&lt;/p&gt;
&lt;p&gt;  final allSongs = context.watch().allSongs;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Benefits&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No repeated disk access.&lt;/li&gt;
&lt;li&gt;Faster UI transitions (playlist, search, player use the same list).&lt;/li&gt;
&lt;li&gt;Centralized state → easier maintenance.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/4282/query-smarter-not-harder</guid>
<pubDate>Sat, 26 Jul 2025 19:58:21 +0000</pubDate>
</item>
<item>
<title>UI issues are beautiful.....</title>
<link>https://coderlegion.com/4267/ui-issues-are-beautiful</link>
<description>&lt;p&gt;Ever had your Flutter screen layout shift or break when your text changes length?&lt;/p&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;In my music player screen, long song titles caused the UI to push down — shifting the seek bar &amp;amp; controls, or worse, throwing an overflow error.&lt;/p&gt;
&lt;p&gt;Here’s how the screen was built without LayoutBuilder:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Problematic layout
return Padding(
  padding: const EdgeInsets.all(20),
  child: Column(
    children: [
      const SizedBox(height: 80),
      AnimatedBeatCircle(size: 300, isPlaying: true),

      // Title (may be 1 or 2 lines)
      Text(
        mediaItem.title,
        maxLines: 2,
        overflow: TextOverflow.ellipsis,
      ),

      const SizedBox(height: 10),
      // Seek bar + playback controls follow
      ...
    ],
  ),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Issue:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If the title is short → empty space.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If it’s long → seek bar/control buttons shift or overflow.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;UI feels unstable between different songs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-fix-use-layoutbuilder-expanded&quot;&gt;The Fix: Use LayoutBuilder + Expanded&lt;/h3&gt;
&lt;p&gt;By wrapping the layout in a LayoutBuilder and explicitly splitting the screen into:&lt;/p&gt;
&lt;p&gt;Top (flexible): Beat circle, title, artist&lt;/p&gt;
&lt;p&gt;Bottom (fixed): Seek bar, playback controls&lt;/p&gt;
&lt;p&gt;We pin the bottom UI and allow the top to adapt safely.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Fixed layout
return LayoutBuilder(
  builder: (context, constraints) {
    return Column(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(20),
            child: Column(
              children: [
                AnimatedBeatCircle(size: 300, isPlaying: true),
                const SizedBox(height: 20),
                Text(
                  mediaItem.title,
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
                Text(mediaItem.artist ?? ''),
              ],
            ),
          ),
        ),

        // Bottom stays pinned no matter what
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20.0),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Slider(...),
              Row(...), // duration labels
              Row(...), // controls
            ],
          ),
        ),
      ],
    );
  },
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Layout is predictable&lt;/li&gt;
&lt;li&gt;Playback controls stay pinned&lt;/li&gt;
&lt;li&gt;UI looks great no matter how long the song title is!&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/4267/ui-issues-are-beautiful</guid>
<pubDate>Fri, 25 Jul 2025 10:29:26 +0000</pubDate>
</item>
<item>
<title>Fixing a Tricky Playback Issue in My Flutter Music App</title>
<link>https://coderlegion.com/4256/fixing-a-tricky-playback-issue-in-my-flutter-music-app</link>
<description>&lt;p&gt;I recently ran into a subtle but super frustrating issue in my Flutter music player app using just_audio and audio_service. It had to do with updating the playback queue when selecting a new song — and I want to share how I fixed it.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-1&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Let’s say I have 5 songs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a  b  c  d  e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's what was happening:&lt;/p&gt;
&lt;p&gt;I open the app and click on song b.&lt;/p&gt;
&lt;p&gt;Music starts playing . I can see the notification with controls.&lt;/p&gt;
&lt;p&gt;I click next in the notification → it moves to c. Perfect!&lt;/p&gt;
&lt;p&gt;I go back to the app and tap on song a.&lt;/p&gt;
&lt;p&gt;Now... song plays but playback control like next and previous don't work. &lt;/p&gt;
&lt;p&gt;So clearly, something went wrong when I tried to load a new playlist and song from the UI.&lt;/p&gt;
&lt;h3 id=&quot;my-buggy-code&quot;&gt;My (Buggy) Code&lt;/h3&gt;
&lt;p&gt;This is how I was updating the queue in my AudioHandler:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Future&amp;lt;void&amp;gt; updateQueue(List&amp;lt;MediaItem&amp;gt; newQueue) async {
  await _player.stop();              // Stop current audio
  _playlist.clear();                 //  Clear existing playlist

  final audioSources = newQueue.map((item) {
    final uri = item.extras?['uri'] as String;
    return AudioSource.uri(Uri.parse(uri));
  }).toList();

  _playlist.addAll(audioSources);   //  Add new items to old list
  await _player.setAudioSource(_playlist); //  Reuse the same playlist

  queue.add(newQueue);              //  Update the queue
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-hidden-issue&quot;&gt;The Hidden Issue&lt;/h3&gt;
&lt;p&gt;I was clearing and modifying the same _playlist instance that was already connected to the player.&lt;/p&gt;
&lt;p&gt;just_audio doesn't always like it when you mutate a ConcatenatingAudioSource after it's been set — especially if it's already active in playback.&lt;/p&gt;
&lt;p&gt;That caused inconsistent behavior.&lt;/p&gt;
&lt;h3 id=&quot;the-fix-use-a-fresh-playlist-instance&quot;&gt;The Fix: Use a Fresh Playlist Instance&lt;/h3&gt;
&lt;p&gt;Instead of modifying the existing _playlist, I now create a brand new ConcatenatingAudioSource every time I update the queue:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@override
Future&amp;lt;void&amp;gt; updateQueue(List&amp;lt;MediaItem&amp;gt; newQueue) async {
  await _player.stop(); // Stop safely

  final newAudioSources = newQueue.map((item) {
    final uri = item.extras?['uri'] as String;
    return AudioSource.uri(Uri.parse(uri));
  }).toList();

  final newPlaylist = ConcatenatingAudioSource(children: newAudioSources); // Fresh playlist

  await _player.setAudioSource(newPlaylist); // Clean set

  _playlist.clear();         // Optional: if you track items
  _playlist.addAll(newAudioSources);

  queue.add(newQueue);       // Update queue after setting source
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When working with just_audio and audio_service, always pass a new AudioSource instance when switching playlists or queues.&lt;br&gt;
Avoid mutating an existing one after it’s been assigned to the player. Otherwise, you'll run into strange bugs like:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Next/Previous buttons breaking&lt;br&gt;
Playback getting stuck &lt;br&gt;
Notifications going out of sync&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/4256/fixing-a-tricky-playback-issue-in-my-flutter-music-app</guid>
<pubDate>Thu, 24 Jul 2025 10:37:42 +0000</pubDate>
</item>
<item>
<title>One wrong activity caused me 10+ hours of debugging</title>
<link>https://coderlegion.com/4220/one-wrong-activity-caused-me-10-hours-of-debugging</link>
<description>&lt;p&gt;The music app I vibe coded is working fine but I wanted to improve it. So I decided to add some extra features like playing in the background so that I can control playback from notification bar. Cool right ?&lt;/p&gt;
&lt;p&gt;I also wanted to refactor my code because It would give me huge pain if I scale it just from here. I started working on it, I fixed all errors of refactoring but when I say&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flutter run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It gives me an error which forced me scratch my head for days. &lt;/p&gt;
&lt;p&gt;&lt;em&gt;The Activity class declared in your AndroidManifest.xml is wrong...&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now I'm trying to fix things in xml file which I needed to actually make change in MainActivity.kt . The only thing I needed to replace below code &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MainActivity: FlutterFragmentActivity() {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;with this &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MainActivity: AudioServiceFragmentActivity() {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally I'm a bit relieved. I did learn a lot as I actually reverse engineered code, refactored so much just to find answers. Now I really need a break lol.....................&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/4220/one-wrong-activity-caused-me-10-hours-of-debugging</guid>
<pubDate>Tue, 22 Jul 2025 10:48:31 +0000</pubDate>
</item>
<item>
<title>After facing weird permission issues, I built my own music app in 6 hours.</title>
<link>https://coderlegion.com/3956/after-facing-weird-permission-issues-i-built-my-own-music-app-in-6-hours</link>
<description>&lt;p&gt;Ever noticed how even the simplest apps want access to everything?&lt;br&gt;
I just wanted to play music — not hand over my call logs.&lt;/p&gt;
&lt;p&gt;Even lightweight music players come loaded with creepy permission requests. Contacts? Call data? Overlay access? Just to hit play on a song?&lt;/p&gt;
&lt;p&gt;It didn’t sit right with me.&lt;br&gt;
So I decided to build my own.&lt;/p&gt;
&lt;h3 id=&quot;1-why-i-chose-to-build-my-own-music-app&quot;&gt;1: Why I Chose to Build My Own Music App&lt;/h3&gt;
&lt;p&gt;I’ve always had a very specific music taste — mostly soulful tracks, spiritual sounds, and some of 90s nostalgia. &lt;/p&gt;
&lt;p&gt;Sure, I could stream them on Spotify or YouTube like everyone else. But I’ve always preferred keeping my own MP3 collection — offline, in my control, with zero interruptions.&lt;/p&gt;
&lt;p&gt;The real issue began when I started noticing how these music apps behave.&lt;/p&gt;
&lt;p&gt;They demand weird permissions.&lt;br&gt;
They push random ads. That I understand, I'm fine with it.&lt;/p&gt;
&lt;p&gt;Last week, I finally snapped. I figured — why not just make my own app?&lt;/p&gt;
&lt;p&gt;I’d been meaning to learn mobile development anyway, so this felt like the perfect excuse.&lt;/p&gt;
&lt;p&gt;After checking out a few frameworks, I settled on Flutter — lightweight and flexible. My laptop isn’t built for Android Studio (tried that once, it basically gave up), so I kept things minimal.&lt;/p&gt;
&lt;p&gt;Set up took an hour or two. I plugged in my phone, enabled developer mode, and got my first “Hello World” up and running.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9230962751924756071&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;From there, it just flowed — I picked up Dart, started testing small things with help from AI, and slowly began to see my idea take shape.&lt;/p&gt;
&lt;h3 id=&quot;2-only-building-what-i-actually-needed&quot;&gt;2: Only Building What I Actually Needed&lt;/h3&gt;
&lt;p&gt;Once I had the basics working, I tested it out with a single MP3 file. Just dropped it into the assets folder and tried playing it through the app. It worked.&lt;/p&gt;
&lt;p&gt;That small win gave me the push to go further.&lt;/p&gt;
&lt;p&gt;Next up — scanning local storage for all my saved music files. I added logic to pull out MP3s from the phone’s directories, and once that was done, I layered in the essentials:&lt;/p&gt;
&lt;p&gt;Play / Pause&lt;/p&gt;
&lt;p&gt;Skip&lt;/p&gt;
&lt;p&gt;Auto-play&lt;/p&gt;
&lt;p&gt;Total duration&lt;/p&gt;
&lt;p&gt;Current time tracking&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=3152915918707568126&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;No recommendations. No suggestions. No sneaky tracking.&lt;br&gt;
Just clean, simple playback — the way I wanted it.&lt;/p&gt;
&lt;p&gt;The UI is still very barebones, and honestly, that’s fine for now. I’m slowly working on polishing it up. But I’m clear on one thing:&lt;br&gt;
It’ll stay lightweight. No junk. No shady permissions.&lt;/p&gt;
&lt;p&gt;Just a music player that respects your privacy — and plays your songs.&lt;/p&gt;
&lt;h3 id=&quot;3-what-s-next&quot;&gt;3 : What’s Next&lt;/h3&gt;
&lt;p&gt;For now, I’m just refining the app to suit my personal use. Clean UI, maybe dark mode, and a couple of nice-to-have touches that I actually care about. I might upload it to the Play Store at some point — not because I want to compete with big apps, but just to share something that feels simple and respectful of users.&lt;/p&gt;
&lt;p&gt;Honestly, building this was fun. More than that, it felt right. I got to learn, create, and finally use an app that doesn’t ask me for things it shouldn’t.&lt;/p&gt;
&lt;p&gt;But now I’m curious…&lt;br&gt;
Have you ever felt weird giving an app permission to read your call logs just to play music?&lt;br&gt;
What problems have you run into?&lt;br&gt;
And hey — do you still listen to local MP3s, or are you all-in on streaming?&lt;/p&gt;
</description>
<category>Mobile Dev</category>
<guid isPermaLink="true">https://coderlegion.com/3956/after-facing-weird-permission-issues-i-built-my-own-music-app-in-6-hours</guid>
<pubDate>Mon, 14 Jul 2025 06:13:19 +0000</pubDate>
</item>
<item>
<title>Reelora</title>
<link>https://coderlegion.com/3707/reelora</link>
<description>&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=6972448456452199574&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Body Highlights:&lt;/p&gt;
&lt;p&gt;Journey of building Reelora from scratch&lt;/p&gt;
&lt;p&gt;Key features (Smart Search, Pagination, Firebase Login, TMDB API integration)&lt;/p&gt;
&lt;p&gt;UI decisions (Dark Mode, shimmer loaders, scroll-based triggers)&lt;/p&gt;
&lt;p&gt;Handling offline states and in-app updates&lt;/p&gt;
&lt;p&gt;Open testing feedback loop &amp;amp; future plan&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=2432645424360975977&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;CTA:&lt;br&gt;
 Try the app on Play Store (Open Testing):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://play.google.com/store/apps/details?id=com.dk.reelora&quot;&gt;https://play.google.com/store/apps/details?id=com.dk.reelora&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Live on Play Store this Sunday&lt;br&gt;
 iOS coming next&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5368165094459256128&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
</description>
<category>IoT &amp; Hardware</category>
<guid isPermaLink="true">https://coderlegion.com/3707/reelora</guid>
<pubDate>Thu, 03 Jul 2025 08:09:19 +0000</pubDate>
</item>
</channel>
</rss>