<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Jesutoni Aderibigbe - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Jesutoni Aderibigbe - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 13 Aug 2026 22:19:36 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/ToniAderibigbe/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Flutter Frontend Systems Design: How to Think Like a Senior Engineer in the AI Age ]]>
                </title>
                <description>
                    <![CDATA[ Systems design has always been treated as a backend problem. Ask a group of Flutter engineers what systems design means, and most will describe server architecture: load balancers, databases, and micr ]]>
                </description>
                <link>https://www.freecodecamp.org/news/flutter-frontend-systems-design-how-to-think-like-a-senior-engineer-in-the-ai-age/</link>
                <guid isPermaLink="false">6a79dcd1e93f9db759fd99d6</guid>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ System Design ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mobile app development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Riverpod ]]>
                    </category>
                
                    <category>
                        <![CDATA[ interview-prep ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jesutoni Aderibigbe ]]>
                </dc:creator>
                <pubDate>Mon, 10 Aug 2026 14:14:41 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/682cb489-c8fd-4530-9226-357edb4e8c19.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Systems design has always been treated as a backend problem.</p>
<p>Ask a group of Flutter engineers what systems design means, and most will describe server architecture: load balancers, databases, and microservices.</p>
<p>Ask them to design a distributed cache or sketch out a message queue, and they'll hesitate. Ask them to design the Flutter client for a social feed, and they'll open a new file and start writing widgets.</p>
<p>That's the gap. And it's closing fast.</p>
<p>As Flutter applications grow more complex with real-time features, offline support, multiple platform targets, and AI-generated code that still needs to be maintainable, the architectural decisions you make before writing a single widget become just as important as your backend architecture.</p>
<p>Senior Flutter interviews at product companies increasingly test this skill. The engineers who can clearly explain <em>why</em> they chose a particular architecture, the trade-offs they considered, and the problems they were optimizing for are the ones who get hired and promoted.</p>
<p>This article is structured in two halves. The first half explains what frontend systems design actually is and why it matters for Flutter engineers specifically in 2026. The second half works through a full mock interview answer for one of the most common scenario questions: designing the Flutter architecture for a social feed with infinite scroll, likes, comments, and real-time updates. We'll walk through the kind of answer that separates mid-level from senior in an interview room.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This article assumes you're a working Flutter developer comfortable with state management (Riverpod, Bloc, or similar), REST APIs, and basic Dart. You don't need backend experience, but familiarity with concepts like caching, pagination, and WebSockets will help you follow the deeper sections.</p>
<p>No code setup is required. This is a thinking and architecture article, not a tutorial. Dart/Flutter snippets are used to ground abstract ideas in concrete implementation.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-1-what-is-frontend-systems-design">1. What is Frontend Systems Design?</a></p>
</li>
<li><p><a href="#heading-2-why-flutter-engineers-cant-ignore-it-anymore">2. Why Flutter Engineers Can't Ignore It Anymore</a></p>
</li>
<li><p><a href="#heading-3-the-interview-format-what-to-expect">3. The Interview Format: What to Expect</a></p>
</li>
<li><p><a href="#heading-4-how-to-structure-your-answer">4. How to Structure Your Answer</a></p>
</li>
<li><p><a href="#heading-5-mock-interview-design-a-social-feed">5. Mock Interview: Design a Social Feed</a></p>
</li>
<li><p><a href="#heading-6-other-questions-to-prepare-for">6. Other Questions to Prepare For</a></p>
</li>
<li><p><a href="#heading-7-key-takeaways">7. Key Takeaways</a></p>
</li>
</ul>
<h2 id="heading-1-what-is-frontend-systems-design">1. What is Frontend Systems Design?</h2>
<p>Systems design is the practice of making high-level decisions about how a software system is structured before implementation begins: how its components are divided, how they communicate, how it handles scale, failure, and change over time.</p>
<p>On the backend, this means deciding between microservices and a monolith, choosing a database, designing an API contract, and planning for horizontal scaling. The feedback loop is fast: a bad database schema causes slow queries within days, and a poorly designed API breaks clients immediately.</p>
<p>On the frontend, the consequences of bad design are slower and quieter. A 600-line screen widget still ships. A god-class repository with 40 methods still works. State leaks between sessions only surface after a frustrated user reports it.</p>
<p>Frontend systems design asks the same category of questions, applied to the client layer:</p>
<ul>
<li><p>How do you divide a large app into independently-buildable features?</p>
</li>
<li><p>Where does business logic live, and what enforces that boundary?</p>
</li>
<li><p>How does data flow from the network to the screen and back?</p>
</li>
<li><p>What happens when the network fails, the API changes shape, or the user logs out mid-session?</p>
</li>
<li><p>How do you design components that can be tested in isolation?</p>
</li>
<li><p>How do you structure the app so a team of engineers can work on it without stepping on each other?</p>
</li>
</ul>
<p>These aren't widget questions. They're architecture questions. And they have answers: principled ones, with real tradeoffs.</p>
<h2 id="heading-2-why-flutter-engineers-cant-ignore-it-anymore">2. Why Flutter Engineers Can't Ignore It Anymore</h2>
<p>Three forces are pushing systems design into the Flutter conversation in a way that simply didn't exist three years ago.</p>
<h3 id="heading-flutter-apps-are-no-longer-just-uis">Flutter Apps Are No Longer Just UIs</h3>
<p>With Serverpod and Dart Frog on the server, Jaspr on the web, and Flutter on mobile and desktop, Dart is now a genuinely full-stack language. Engineers making architecture decisions that span mobile, web, and server in the same codebase need systems thinking, not just widget composition skills.</p>
<p>When your Freezed model is shared between the Flutter client and the Dart backend, the boundary between "frontend" and "backend" design dissolves. You're designing a system.</p>
<h3 id="heading-ai-agents-expose-bad-architecture-immediately">AI Agents Expose Bad Architecture Immediately</h3>
<p>This is the new pressure point. When Claude Code or any AI coding agent reads your project cold, it has no accumulated mental model to compensate for messiness. It reads files sequentially. It works within a limited context window. It makes decisions based on the patterns it sees.</p>
<p>A codebase with tangled dependencies, inconsistent naming, and business logic scattered across the widget tree produces unreliable AI output. This doesn't happen because the AI is wrong, but because the code doesn't communicate its own structure clearly enough to be navigated by something without human intuition.</p>
<p>Good systems design and AI-navigable architecture are almost identical. Feature-first structure, clear layer boundaries, consistent naming, small, focused files. These aren't just team hygiene practices anymore. They're what make AI-assisted development actually work at scale.</p>
<h3 id="heading-senior-flutter-interviews-now-test-it-explicitly">Senior Flutter Interviews Now Test it Explicitly</h3>
<p>As Flutter matures and product companies build larger apps with larger teams, the interview bar has risen. A mid-level Flutter interview might test widget lifecycle and state management fundamentals. A senior interview tests your ability to design a system you've never seen before, live, under pressure, while explaining your thinking out loud.</p>
<p>If you haven't thought about this before walking into that room, you'll be caught off guard.</p>
<h2 id="heading-3-the-interview-format-what-to-expect">3. The Interview Format: What to Expect</h2>
<p>Frontend systems design interviews at senior level typically run 45–60 minutes. You're given a vague scenario, like "design the <strong>Flutter client for a social feed"</strong>, and you're expected to drive the conversation.</p>
<p>The interviewer isn't looking for a single correct answer. They're watching how you think:</p>
<ul>
<li><p>Do you clarify requirements before jumping to solutions?</p>
</li>
<li><p>Do you identify the hard problems (real-time sync, optimistic UI, offline states) rather than the easy ones?</p>
</li>
<li><p>Do you make tradeoffs explicitly rather than just picking the thing you know best?</p>
</li>
<li><p>Can you go deep on any layer when pushed?</p>
</li>
</ul>
<p>The biggest mistake candidates make is opening Xcode or a code file immediately and starting to build. Systems design interviews are whiteboard conversations, not implementation sessions. Draw boxes. Name the layers. Talk through the data flow before writing a single method signature.</p>
<h2 id="heading-4-how-to-structure-your-answer">4. How to Structure Your Answer</h2>
<p>Use this framework for any frontend systems design question:</p>
<ol>
<li><p><strong>Clarify requirements (5 minutes)</strong> What platforms? How many users? Offline support? Real-time? Authentication? What's in scope for this conversation? Never assume.</p>
</li>
<li><p><strong>Define the data model (5–10 minutes)</strong> What are the core entities? What are their relationships? This anchors every architectural decision that follows.</p>
</li>
<li><p><strong>Design the layer architecture (10 minutes)</strong> How is the app divided? What are the layers? What enforces the boundaries between them?</p>
</li>
<li><p><strong>Solve the hard problems one by one (20–25 minutes)</strong> Pagination. Optimistic UI. Real-time sync. Offline. Performance. Go deep on each one, and name the tradeoffs.</p>
</li>
<li><p><strong>Address failure states (5 minutes)</strong> What breaks? What's the user experience when it does? Senior answers always include error handling.</p>
</li>
<li><p><strong>Summarise and invite questions (5 minutes)</strong> Recap the key decisions and the tradeoffs you made. Show you can hold the whole picture.</p>
</li>
</ol>
<h2 id="heading-5-mock-interview-design-a-social-feed">5. Mock Interview: Design a Social Feed</h2>
<blockquote>
<p><strong>Interviewer:</strong> Design the Flutter client architecture for a social feed. Users can scroll through posts, like and comment on them, and receive real-time updates when new posts arrive.</p>
</blockquote>
<p>This is the answer.</p>
<h3 id="heading-step-1-clarify-requirements">Step 1: Clarify Requirements</h3>
<p>Before touching architecture, ask the questions that constrain your decisions.</p>
<blockquote>
<p><em>"A few questions before I start. What platforms are we targeting? Mobile only, or web and desktop too? How many users are we designing for? Is this a startup MVP or an app at scale? Do we need offline support? How real-time does real-time need to be? Are we talking push notifications, or should the feed update while the user is looking at it? And what's the authentication model? Are users logged in, or is there a guest mode?"</em></p>
</blockquote>
<p>For this walkthrough, assume:</p>
<ul>
<li><p>Mobile (iOS + Android), with web on the roadmap</p>
</li>
<li><p>Tens of thousands of MAU. Not Twitter scale, but meaningful.</p>
</li>
<li><p>Offline: show cached content, queue interactions</p>
</li>
<li><p>Real-time: live feed updates while the screen is open (WebSocket)</p>
</li>
<li><p>Auth: logged-in users only</p>
</li>
</ul>
<p>These answers change every architectural decision that follows. Offline support means a local cache layer. Live updates while the screen is open means WebSockets, not polling. Web on the roadmap means avoiding anything mobile-only in the business logic layer.</p>
<h3 id="heading-step-2-define-the-data-model">Step 2: Define the Data Model</h3>
<p>Start with the entities and their relationships. Draw these before writing any code.</p>
<pre><code class="language-dart">// Core entities

@freezed
class Post with _$Post {
  const factory Post({
    required String id,
    required String authorId,
    required String authorName,
    required String authorAvatarUrl,
    required String content,
    String? imageUrl,
    required int likeCount,
    required int commentCount,
    required bool isLikedByMe,      // derived from current user context
    required DateTime createdAt,
  }) = _Post;
}

@freezed
class Comment with _$Comment {
  const factory Comment({
    required String id,
    required String postId,
    required String authorId,
    required String authorName,
    required String content,
    required DateTime createdAt,
  }) = _Comment;
}

@freezed
class FeedPage with _$FeedPage {
  const factory FeedPage({
    required List&lt;Post&gt; posts,
    required String? nextCursor,    // null = end of feed
  }) = _FeedPage;
}
</code></pre>
<p>A few design decisions embedded in this model are worth calling out explicitly in an interview:</p>
<p><code>isLikedByMe</code> <strong>lives on the Post.</strong> You could derive this from a separate user-likes table, but embedding it in the post response is simpler and makes the UI stateless. The screen doesn't need to join two data sources to render a like button.</p>
<p><strong>Cursor-based pagination, not offset.</strong> <code>nextCursor</code> rather than <code>page: 2</code>. Offset pagination breaks when new posts are inserted at the top. Item 20 on page 2 becomes item 21, and you either show a duplicate or skip an item. Cursors are stable.</p>
<p><code>likeCount</code> <strong>and</strong> <code>commentCount</code> <strong>are integers, not arrays.</strong> You don't fetch all likers to render a post. You fetch the count and a flag. This is a deliberate API contract decision that prevents unbounded payload size.</p>
<h3 id="heading-step-3-design-the-layer-architecture">Step 3: Design the Layer Architecture</h3>
<p>A feed is a good test of layer discipline because data flows in multiple directions: down from the API, up from user interactions, and sideways from real-time events. A flat architecture collapses quickly.</p>
<p>Here's the structure:</p>
<pre><code class="language-plaintext">lib/
├── core/
│   ├── network/          # Dio client, interceptors, token refresh
│   ├── cache/            # Local storage abstraction (Hive or Isar)
│   ├── realtime/         # WebSocket connection manager
│   └── errors/           # Typed error classes
└── features/
    └── feed/
        ├── data/
        │   ├── models/   # Post, Comment, FeedPage (Freezed)
        │   ├── sources/
        │   │   ├── feed_remote_source.dart   # API calls
        │   │   └── feed_local_source.dart    # Cache reads/writes
        │   └── repositories/
        │       └── feed_repository.dart      # Coordinates remote + local
        └── presentation/
            ├── screens/
            │   └── feed_screen.dart
            ├── widgets/
            │   ├── post_card.dart
            │   ├── like_button.dart
            │   └── comment_sheet.dart
            └── providers/
                ├── feed_provider.dart        # Paginated post list
                ├── like_provider.dart        # Like/unlike actions
                └── realtime_provider.dart    # WebSocket events → state
</code></pre>
<p>A couple things worth noting here:</p>
<p>First, the repository is the only component that talks to both the remote source and the local source. Providers call the repository. The repository decides whether to hit the network or return cached data. Screens never know the data came from cache.</p>
<p>Second, the real-time layer is separate from the data fetching layer. It's a common mistake to wire WebSocket events directly into the same provider that manages pagination, and it becomes impossible to test or reason about. The <code>realtime_provider</code> receives events and patches the feed state and the <code>feed_provider</code> manages the paginated list. They coordinate through Riverpod's <code>ref</code>, not through direct dependency.</p>
<h3 id="heading-step-4-pagination-and-infinite-scroll">Step 4: Pagination and Infinite Scroll</h3>
<p>Infinite scroll is the first hard problem. The naïve implementation: a <code>ListView</code> that loads everything falls apart at a few hundred posts.</p>
<p>Here's a Riverpod <code>AsyncNotifier</code> that handles cursor-based pagination:</p>
<pre><code class="language-dart">@riverpod
class FeedNotifier extends _$FeedNotifier {
  static const _pageSize = 20;
  String? _nextCursor;
  bool _isFetchingMore = false;

  @override
  Future&lt;List&lt;Post&gt;&gt; build() async {
    // Load first page + seed from cache if available
    final cached = await ref.read(feedLocalSourceProvider).getCachedPosts();
    if (cached.isNotEmpty) {
      // Show cache immediately, refresh in background
      _refreshInBackground();
      return cached;
    }
    return _fetchPage(cursor: null);
  }

  Future&lt;void&gt; loadMore() async {
    if (_isFetchingMore || _nextCursor == null) return;
    _isFetchingMore = true;

    final currentPosts = state.valueOrNull ?? [];
    final page = await ref
        .read(feedRepositoryProvider)
        .getFeedPage(cursor: _nextCursor, limit: _pageSize);

    _nextCursor = page.nextCursor;
    state = AsyncData([...currentPosts, ...page.posts]);
    _isFetchingMore = false;
  }

  Future&lt;List&lt;Post&gt;&gt; _fetchPage({required String? cursor}) async {
    final page = await ref
        .read(feedRepositoryProvider)
        .getFeedPage(cursor: cursor, limit: _pageSize);
    _nextCursor = page.nextCursor;
    await ref.read(feedLocalSourceProvider).cachePosts(page.posts);
    return page.posts;
  }

  void _refreshInBackground() {
    Future.microtask(() async {
      final freshPosts = await _fetchPage(cursor: null);
      state = AsyncData(freshPosts);
    });
  }

  bool get hasMore =&gt; _nextCursor != null;
}
</code></pre>
<p>In the screen, trigger <code>loadMore()</code> before the user reaches the bottom, not at the last item, but a few items before it:</p>
<pre><code class="language-dart">NotificationListener&lt;ScrollNotification&gt;(
  onNotification: (notification) {
    if (notification.metrics.pixels &gt;
        notification.metrics.maxScrollExtent - 400) {
      ref.read(feedNotifierProvider.notifier).loadMore();
    }
    return false;
  },
  child: ListView.builder(
    itemCount: posts.length + (hasMore ? 1 : 0),
    itemBuilder: (context, index) {
      if (index == posts.length) return const FeedLoadingIndicator();
      return PostCard(post: posts[index]);
    },
  ),
)
</code></pre>
<p>The 400-pixel threshold means the next page starts loading before the user sees the end of the list. The experience feels seamless.</p>
<h3 id="heading-step-5-optimistic-ui-for-likes-and-comments">Step 5: Optimistic UI for Likes and Comments</h3>
<p>Optimistic UI is the practice of updating the local state immediately when a user takes an action, before the server confirms it, then rolling back if the server rejects it. It's what makes a like button feel instant rather than laggy.</p>
<p>The pattern has three steps: apply the optimistic update, fire the network request, and roll back on failure.</p>
<pre><code class="language-dart">@riverpod
class LikeNotifier extends _$LikeNotifier {
  @override
  void build() {}

  Future&lt;void&gt; toggleLike(String postId) async {
    final feedNotifier = ref.read(feedNotifierProvider.notifier);
    final currentPosts = ref.read(feedNotifierProvider).valueOrNull ?? [];

    // Find the post
    final postIndex = currentPosts.indexWhere((p) =&gt; p.id == postId);
    if (postIndex == -1) return;
    final post = currentPosts[postIndex];

    // Step 1: Apply optimistic update immediately
    final optimisticPost = post.copyWith(
      isLikedByMe: !post.isLikedByMe,
      likeCount: post.isLikedByMe ? post.likeCount - 1 : post.likeCount + 1,
    );
    feedNotifier.patchPost(postIndex, optimisticPost);

    // Step 2: Fire the network request
    try {
      await ref.read(feedRepositoryProvider).toggleLike(postId);
    } catch (e) {
      // Step 3: Roll back on failure
      feedNotifier.patchPost(postIndex, post);
      // Show a snackbar or error indicator
    }
  }
}
</code></pre>
<p>The <code>patchPost</code> method on <code>FeedNotifier</code> replaces a single post in the list without rebuilding the whole feed. This is an important performance detail when the list has hundreds of items.</p>
<p><strong>The tradeoff to name explicitly in an interview:</strong> optimistic UI can produce an inconsistent state if the server is the source of truth for like counts. Two users liking simultaneously might both see their local count increment from 41 to 42, but the real count is 43. For a social app, this is usually acceptable. You show the user their action was registered, and the next feed refresh corrects the count. For financial transactions, an optimistic UI is inappropriate. Know where to draw the line.</p>
<h3 id="heading-step-6-real-time-updates">Step 6: Real-Time Updates</h3>
<p>Real-time feed updates and new posts appearing while the user is looking at the screen require a persistent connection. WebSocket is the right tool here. Server-Sent Events work too, but WebSocket is bidirectional, which matters if you later want to push events (typing indicators, presence).</p>
<p>Design the WebSocket layer as a singleton service, not inside the feed feature:</p>
<pre><code class="language-dart">// core/realtime/realtime_service.dart

class RealtimeService {
  WebSocketChannel? _channel;
  final _controller = StreamController&lt;RealtimeEvent&gt;.broadcast();

  Stream&lt;RealtimeEvent&gt; get events =&gt; _controller.stream;

  Future&lt;void&gt; connect(String token) async {
    _channel = WebSocketChannel.connect(
      Uri.parse('wss://api.yourapp.com/ws?token=$token'),
    );

    _channel!.stream.listen(
      (data) {
        final event = RealtimeEvent.fromJson(jsonDecode(data as String));
        _controller.add(event);
      },
      onError: (_) =&gt; _scheduleReconnect(),
      onDone: () =&gt; _scheduleReconnect(),
    );
  }

  void _scheduleReconnect() {
    Future.delayed(const Duration(seconds: 3), connect);
  }

  void dispose() {
    _channel?.sink.close();
    _controller.close();
  }
}
</code></pre>
<p>Then in the feed layer, listen to the stream and patch state when new posts arrive:</p>
<pre><code class="language-dart">@riverpod
class RealtimeFeedNotifier extends _$RealtimeFeedNotifier {
  StreamSubscription? _subscription;

  @override
  void build() {
    _subscription = ref
        .read(realtimeServiceProvider)
        .events
        .where((e) =&gt; e.type == RealtimeEventType.newPost)
        .listen((event) {
      final newPost = Post.fromJson(event.payload);
      ref.read(feedNotifierProvider.notifier).prependPost(newPost);
    });

    ref.onDispose(() =&gt; _subscription?.cancel());
  }
}
</code></pre>
<p><strong>The UX decision worth raising in an interview:</strong> do you silently prepend new posts to the top of the feed, or do you show a "3 new posts, tap to refresh" banner?</p>
<p>Silent prepend is jarring: the user is reading post 5, and suddenly they're reading post 8. The banner pattern (used by Twitter/X and LinkedIn) is almost always the better choice. It signals freshness without disrupting reading position.</p>
<h3 id="heading-step-7-offline-and-error-states">Step 7: Offline and Error States</h3>
<p>An offline-capable feed has two distinct requirements: show something useful when there's no connection, and queue interactions (likes, comments) so they fire when connectivity returns.</p>
<p>For showing cached content, the repository pattern handles this cleanly:</p>
<pre><code class="language-dart">// feed_repository.dart

Future&lt;List&lt;Post&gt;&gt; getFeed({String? cursor}) async {
  try {
    final page = await _remoteSource.getFeedPage(cursor: cursor);
    await _localSource.cachePosts(page.posts);
    return page.posts;
  } on DioException catch (e) {
    if (e.type == DioExceptionType.connectionError) {
      // Network unavailable — return cache
      final cached = await _localSource.getCachedPosts();
      if (cached.isNotEmpty) return cached;
    }
    rethrow;
  }
}
</code></pre>
<p>For queuing interactions offline, keep a simple pending actions queue in local storage:</p>
<pre><code class="language-dart">@freezed
class PendingAction with _$PendingAction {
  const factory PendingAction.like({
    required String postId,
    required bool isLike,
    required DateTime queuedAt,
  }) = PendingLike;

  const factory PendingAction.comment({
    required String postId,
    required String content,
    required DateTime queuedAt,
  }) = PendingComment;
}
</code></pre>
<p>When connectivity returns (detected via <code>connectivity_plus</code>), drain the queue and fire each action in order. If an action fails after retry, surface it to the user. Don't silently drop it.</p>
<h3 id="heading-step-8-performance-considerations">Step 8: Performance Considerations</h3>
<p>A feed is one of the most performance-sensitive screens in any app. There are a few non-negotiable practices:</p>
<p>First, use <code>ListView.builder</code>, never <code>ListView</code> with a <code>children</code> list. Builder renders only the items currently on screen. A <code>children</code> list renders all of them at once (which would be catastrophic for a feed of 200+ posts).</p>
<p>Second, keep <code>PostCard</code> build methods cheap. Every rebuild of a postcard is expensive at scale. Use <code>const</code> constructors everywhere possible. Avoid rebuilding the whole card when only the like count changes. Isolate the like button into its own Riverpod consumer.</p>
<pre><code class="language-dart">// Bad — whole PostCard rebuilds when like changes
class PostCard extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final post = ref.watch(feedNotifierProvider)
        .valueOrNull
        ?.firstWhere((p) =&gt; p.id == postId);
    // ...
  }
}

// Good — only LikeButton rebuilds
class LikeButton extends ConsumerWidget {
  final String postId;
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final post = ref.watch(
      feedNotifierProvider.select(
        (state) =&gt; state.valueOrNull?.firstWhere((p) =&gt; p.id == postId),
      ),
    );
    // Only rebuilds when this specific post's like state changes
  }
}
</code></pre>
<p>Third, cache network images aggressively. Use <code>cached_network_image</code> with a memory cache limit. On a feed with avatars and post images, uncached network images are the single biggest source of jank.</p>
<p>And lastly, dispose WebSocket connections on screen exit. Don't keep a real-time connection alive when the user navigates away. Riverpod's <code>ref.onDispose</code> makes this straightforward, but it's easy to miss.</p>
<h2 id="heading-6-other-questions-to-prepare-for">6. Other Questions to Prepare For</h2>
<p>The social feed covers most of the hard architectural territory. These additional questions round out your preparation:</p>
<p><strong>Architecture &amp; structure:</strong></p>
<ul>
<li><p>How would you structure a large Flutter app for a team of 10 engineers?</p>
</li>
<li><p>How do you handle shared state between two features that shouldn't know about each other?</p>
</li>
<li><p>Walk me through how you'd design the data layer for an offline-first app.</p>
</li>
</ul>
<p><strong>State management:</strong></p>
<ul>
<li><p>Compare Riverpod, Bloc, and Redux from an architecture standpoint (not just API differences).</p>
</li>
<li><p>How do you prevent the state from leaking between sessions after a user logs out?</p>
</li>
</ul>
<p><strong>Networking &amp; data:</strong></p>
<ul>
<li><p>How would you handle token refresh across concurrent requests?</p>
</li>
<li><p>Walk me through optimistic UI for a financial transaction. How is it different from liking a post?</p>
</li>
</ul>
<p><strong>Performance:</strong></p>
<ul>
<li><p>A screen has 10,000 items. How do you render it without jank?</p>
</li>
<li><p>How do you design an image-loading system for a feed with mixed media types?</p>
</li>
</ul>
<p><strong>Multi-platform:</strong></p>
<ul>
<li><p>How would you share models and business logic between a Flutter mobile app and a Dart backend?</p>
</li>
<li><p>What changes about your architecture when you add a web as a target?</p>
</li>
</ul>
<p>For each of these, use the same framework: clarify the constraints, define the data model, name the layers, solve the hard problems explicitly, and address failure states.</p>
<h2 id="heading-7-key-takeaways">7. Key Takeaways</h2>
<p>Systems design is not a backend discipline that Flutter engineers are exempt from. It's a way of thinking about software that becomes unavoidable as apps grow in complexity, teams grow in size, and AI agents become part of the development workflow.</p>
<p>The social feed scenario illustrates five principles that apply across every frontend systems design problem:</p>
<h3 id="heading-1-layer-boundaries-are-load-bearing">1. Layer Boundaries Are Load-bearing</h3>
<p>The repository pattern, the separation of real-time from data fetching, and the isolation of pending actions aren't academic choices. They're what makes the system testable, navigable, and maintainable when requirements change.</p>
<h3 id="heading-2-the-data-model-anchors-everything">2. The Data Model Anchors Everything</h3>
<p>Decisions you make in the model (like cursor-based pagination, <code>isLikedByMe</code> on the post, and integer counts instead of arrays) ripple through every layer. Get the model right before designing anything else.</p>
<h3 id="heading-3-optimistic-ui-is-a-ux-contract-not-just-a-pattern">3. Optimistic UI is a UX Contract, Not Just a Pattern</h3>
<p>When you apply an optimistic update, you're making a promise to the user. Know when that promise is appropriate (social interactions) and when it isn't (financial transactions).</p>
<h3 id="heading-4-real-time-is-an-architecture-concern-not-a-feature">4. Real-time is an Architecture Concern, Not a Feature</h3>
<p>A WebSocket connection is a persistent resource that needs to be managed, connected when needed, disconnected when not, and reconnected on failure. Design it as infrastructure, not as part of a single screen.</p>
<h3 id="heading-5-offline-is-a-first-class-state">5. Offline is a First-class State</h3>
<p>Not an edge case, not a "nice to have." In markets with unreliable connectivity, which includes most of the world's fastest-growing mobile markets, an app that shows nothing when the network drops is a broken app.</p>
<p>The engineers who understand these principles and can articulate them out loud under interview pressure are the ones who get hired to build the systems that millions of people use.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Claude Code to Build Flutter Apps Faster — Best Practices for 2026 ]]>
                </title>
                <description>
                    <![CDATA[ In early 2023, I was interning at a US-based company, long before agentic AI became part of everyday development. We had tools like ChatGPT, Gemini, and Copilot, but they were mostly chat interfaces:  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-claude-code-to-build-flutter-apps-faster-best-practices/</link>
                <guid isPermaLink="false">6a427b9a9857c50fd3971c7a</guid>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter App Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ claude-code ]]>
                    </category>
                
                    <category>
                        <![CDATA[ claude ]]>
                    </category>
                
                    <category>
                        <![CDATA[ claude.ai ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jesutoni Aderibigbe ]]>
                </dc:creator>
                <pubDate>Mon, 29 Jun 2026 14:05:14 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/90650cde-75af-4ba0-af15-5d7c567d1583.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In early 2023, I was interning at a US-based company, long before agentic AI became part of everyday development.</p>
<p>We had tools like ChatGPT, Gemini, and Copilot, but they were mostly chat interfaces: you pasted code, got a response, and moved on.</p>
<p>During that time, my manager, who worked in AI/ML, told me that a day would come when developers would collaborate with AI agents and that learning how to write effective prompts would become a valuable skill.</p>
<p>I took that advice seriously. I spent countless nights experimenting with prompts, refining instructions, and learning how to communicate with AI systems effectively.</p>
<p>Today, while I still write code by hand and believe strongly in fundamentals, those early lessons have paid off. In an era where AI is embedded into the development workflow, I've been able to leverage it to significantly amplify my productivity as a software engineer.</p>
<p>You've probably seen all the excitement around AI coding assistants. But if you've tried using one on a real Flutter project, whether it's a fintech app, an e-commerce platform, or any application with a well-structured architecture, you've likely experienced the frustration, too.</p>
<p>The assistant generates a widget. You paste it in. It doesn't fit your architecture. It ignores your naming conventions. It recreates functionality that already exists somewhere else in your codebase. Before long, you've spent twenty minutes fixing code that was supposed to save you time.</p>
<p>The problem isn't the AI. The problem is that most developers still use AI as an advanced autocomplete tool when it can function as something much more powerful: a second engineer that understands your codebase, follows your conventions, and tackles parallel tasks while you focus on solving the hard problems.</p>
<p>In this article, I'll show you what has actually worked for me. We'll cover how to structure your Flutter projects so Claude Code can navigate them effectively and how to use skills, loops, and subagents to automate repetitive development tasks and dramatically increase your productivity.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before following along, you should be comfortable with the basics of Flutter development; building widgets, managing state, and running the app from the terminal. You don't need to be an expert.</p>
<p>On the tooling side, you'll need:</p>
<ul>
<li><p><strong>Flutter SDK</strong> (3.x or later): the framework we're building with. Install it from <a href="https://flutter.dev">flutter.dev</a>.</p>
</li>
<li><p><strong>Claude Code</strong>: Anthropic's agentic coding tool that runs in your terminal alongside your editor. Install it with <code>npm install -g @anthropic-ai/claude-code</code>, then run <code>claude</code> in your project directory to start a session. You'll need an Anthropic account and API key.</p>
</li>
<li><p><strong>A code editor</strong>: VS Code or Android Studio both work well. Claude Code operates in the terminal and reads/writes files directly, so it works alongside whatever editor you use.</p>
</li>
<li><p><strong>Git</strong>: version control is assumed throughout. Claude Code integrates with Git for commits, diffs, and branch awareness.</p>
</li>
</ul>
<p>Here's a quick overview of the Claude Code concepts we'll use throughout the article:</p>
<ul>
<li><p><strong>CLAUDE.md</strong>: a markdown file at your project root that Claude reads at the start of every session. Think of it as a briefing document: your architecture, your conventions, your commands.</p>
</li>
<li><p><strong>Skills</strong>: reusable instruction packs stored in <code>.claude/skills/</code>. You define them once, and Claude invokes them automatically when the task matches, or you call them manually with <code>/skillname</code>.</p>
</li>
<li><p><strong>Subagents</strong>: isolated Claude instances that handle a focused task in their own context window, then return only a summary. Great for parallel work without polluting your main session.</p>
</li>
<li><p><strong>Hooks</strong>: shell commands or scripts that fire on lifecycle events (before a tool runs, after a turn completes, and so on). They bypass Claude's judgment entirely — useful for enforcing rules deterministically.</p>
</li>
<li><p><strong>/loop</strong>: a built-in skill that reruns a task repeatedly until a condition you define is met.</p>
</li>
</ul>
<p>None of these require special configuration to unlock. They’re all available once you have Claude Code installed.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-1-why-architecture-comes-first">1. Why Architecture Comes First</a></p>
</li>
<li><p><a href="#heading-2-setting-up-your-claudemd">2. Setting Up Your CLAUDE.md</a></p>
</li>
<li><p><a href="#heading-3-feature-first-folder-structure-the-details">3. Feature-First Folder Structure — The Details</a></p>
</li>
<li><p><a href="#heading-4-writing-skills-for-your-most-repeated-tasks">4. Writing Skills for Your Most Repeated Tasks</a></p>
</li>
<li><p><a href="#heading-5-using-loop-for-self-correcting-workflows">5. Using /loop for Self-Correcting Workflows</a></p>
</li>
<li><p><a href="#heading-6-subagents-for-parallel-screen-development">6. Subagents for Parallel Screen Development</a></p>
</li>
<li><p><a href="#heading-7-hooks-enforcing-rules-deterministically">7. Hooks — Enforcing Rules Deterministically</a></p>
</li>
<li><p><a href="#heading-8-putting-it-all-together-a-real-sprint-workflow">8. Putting It All Together: A Real Sprint Workflow</a></p>
</li>
<li><p><a href="#heading-key-takeaways">Key Takeaways</a></p>
</li>
</ul>
<h2 id="heading-1-why-architecture-comes-first">1. Why Architecture Comes First</h2>
<p>Before you write a single skill or configure a single hook, your folder structure needs to make sense to an AI reading it cold.</p>
<p>Claude Code reads your files to understand your project. If your code is scattered across a layer-first structure (<code>lib/models/</code>, <code>lib/services/</code>, <code>lib/widgets/</code>), Claude has to piece together what each feature does by jumping between folders. It makes mistakes. It creates files in the wrong place. It generates code that doesn't conform to the pattern used in the rest of the app.</p>
<p>The fix is a feature-first structure. Each feature is a self-contained module. Everything Claude needs to understand the transfer flow, for example, lives inside <code>lib/features/transfer/</code>.</p>
<pre><code class="language-plaintext">lib/
├── core/
│   ├── constants/
│   ├── errors/
│   ├── router/
│   └── theme/
├── features/
│   ├── auth/
│   │   ├── data/
│   │   │   ├── models/         # Freezed models
│   │   │   └── repositories/
│   │   ├── presentation/
│   │   │   ├── screens/
│   │   │   ├── widgets/
│   │   │   └── providers/      # Riverpod providers
│   │   └── auth.dart           # barrel export
│   ├── transfer/
│   │   ├── data/
│   │   ├── presentation/
│   │   └── transfer.dart
│   └── wallet/
│       ├── data/
│       ├── presentation/
│       └── wallet.dart
└── main.dart
</code></pre>
<p>This structure tells Claude immediately: "Everything for the transfer feature is in <code>lib/features/transfer/</code>"When you ask it to '<em>add a beneficiary validation to the transfer flow,</em>' it knows exactly where to look and where to create new files.</p>
<p>It also maps cleanly to Riverpod with code generation. Each feature's providers live close to the screens that use them, which means <code>build_runner</code> output lands in the right place, too.</p>
<h2 id="heading-2-setting-up-your-claudemd">2. Setting Up Your CLAUDE.md</h2>
<p><code>CLAUDE.md</code> is arguably the most important file in your Claude Code setup. It's loaded at the beginning of every session. It remains in context throughout the conversation, helping Claude stay aligned with your project's architecture, conventions, and development practices no matter how long the session becomes.</p>
<p>Create it at the root of your project:</p>
<pre><code class="language-bash">touch CLAUDE.md
</code></pre>
<p>Here's a template shaped for a Flutter/Riverpod project:</p>
<pre><code class="language-markdown"># My Flutter App

## Commands
- `flutter pub get` — install dependencies
- `dart run build_runner build --delete-conflicting-outputs` — generate code
- `flutter analyze` — run linter
- `flutter test` — run tests
- `flutter run` — start dev build

## Architecture
Feature-first folder structure. Each feature lives in lib/features/&lt;name&gt;/.
State management: Riverpod with @riverpod code generation (AsyncNotifier pattern).
HTTP: Dio with interceptors in lib/core/network/.
Navigation: GoRouter with named routes defined in lib/core/router/.
Models: Freezed + JsonSerializable. Run build_runner after any model change.

## Conventions
- All monetary amounts in the smallest unit (e.g. kobo for NGN), stored as int — never use doubles for money
- Use ref.invalidate() not ref.refresh()
- No business logic in widgets — all logic goes in notifiers or repositories
- Widget files contain only one public widget per file
- Barrel exports via feature.dart in each feature root
- Prefix private widgets with an underscore

## What NOT to do
- Do not add new packages without asking first
- Do not modify *.g.dart or *.freezed.dart files directly — regenerate with build_runner
- Do not put API calls directly in notifiers — always go through the repository layer
</code></pre>
<p>A few things to note about this file:</p>
<p><strong>Keep it honest:</strong> If your conventions don't match what's actually in the codebase, Claude will get confused. The CLAUDE.md should reflect how the code actually works today, not aspirationally.</p>
<p><strong>The "What NOT to do" section matters:</strong> AI assistants are optimistic. They'll solve the problem in front of them without thinking about side effects. Explicitly telling Claude what to avoid saves a lot of cleanup.</p>
<p><strong>Don't make it too long:</strong> Every line in CLAUDE.md costs tokens on every single turn of every session. Put team-wide, always-relevant rules here. Everything else should be a skill (covered next).</p>
<h2 id="heading-3-feature-first-folder-structure-the-details">3. Feature-First Folder Structure — The Details</h2>
<p>Let's look inside a feature in more detail, using a wallet feature as an example:</p>
<pre><code class="language-plaintext">lib/features/wallet/
├── data/
│   ├── models/
│   │   ├── wallet.dart             # Freezed model
│   │   ├── wallet.freezed.dart     # Generated
│   │   ├── wallet.g.dart           # Generated
│   │   └── transaction.dart
│   └── repositories/
│       ├── wallet_repository.dart  # Abstract class
│       └── wallet_repository_impl.dart
├── presentation/
│   ├── screens/
│   │   ├── wallet_screen.dart
│   │   └── transaction_history_screen.dart
│   ├── widgets/
│   │   ├── balance_card.dart
│   │   └── transaction_tile.dart
│   └── providers/
│       ├── wallet_provider.dart
│       └── wallet_provider.g.dart  # Generated
└── wallet.dart                     # Barrel export
</code></pre>
<p>And here's what a clean Riverpod provider looks like in this structure:</p>
<pre><code class="language-dart">// lib/features/wallet/presentation/providers/wallet_provider.dart

import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../data/models/wallet.dart';
import '../../data/repositories/wallet_repository.dart';

part 'wallet_provider.g.dart';

@riverpod
class WalletNotifier extends _$WalletNotifier {
  @override
  Future&lt;Wallet&gt; build() async {
    return ref.watch(walletRepositoryProvider).getWallet();
  }

  Future&lt;void&gt; refreshBalance() async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(
      () =&gt; ref.read(walletRepositoryProvider).getWallet(),
    );
  }
}
</code></pre>
<p>When Claude Code sees this pattern repeated across multiple features, it learns to replicate it. The more consistent your structure, the better Claude's output matches what you'd write yourself.</p>
<h2 id="heading-4-writing-skills-for-your-most-repeated-tasks">4. Writing Skills for Your Most Repeated Tasks</h2>
<p>Skills are reusable instruction packs that Claude Code loads when they're relevant. They live in <code>.claude/skills/&lt;name&gt;/SKILL.md</code> and can be invoked manually with <code>/skillname</code> or triggered automatically when Claude recognises the right context.</p>
<p>A simple way to think about a Skill is as a specialist on your team. Imagine working with a designer, a QA engineer, and a security expert. You don't explain their entire job every time you need their help. Each person already knows their responsibilities and follows a defined process.</p>
<p>Skills work the same way. Instead of repeatedly telling Claude how to generate Riverpod providers, write tests, or review security concerns, you package those instructions into a Skill and let Claude load them whenever they're needed.</p>
<p>Think of a Skill as a saved recipe. Instead of writing out the ingredients and cooking steps every time you want to make a meal, you keep the recipe in one place and reuse it whenever needed.</p>
<p>Skills do the same thing for development workflows. They allow you to save a set of instructions once and have Claude follow them consistently every time a similar task comes up.</p>
<p>The key thing to understand is that the description field is what triggers a skill. Claude evaluates it on every turn and decides whether the current task matches. Because of this, you should describe it using the same verbs that developers actually type in real workflows, like <code>build</code>, <code>commit</code>, <code>release</code>, or <code>fix lint</code>, instead of documentation-style language.</p>
<h3 id="heading-creating-your-first-skill">Creating Your First Skill</h3>
<p>Before you write a skill, think about the tasks you perform over and over again. A good skill captures a workflow you already know by heart. If you find yourself giving Claude the same instructions every session, such as "run <code>flutter analyze</code>, then run <code>build_runner</code>, then execute the tests," that's a good candidate for a skill.</p>
<p>Start with one task. Keep the steps in the exact order you expect Claude to follow, and clearly define what a successful outcome looks like. Don't try to cover every possible edge case. The goal is to automate your normal workflow so Claude can handle the repetitive work consistently, while you step in only when something unexpected happens.</p>
<pre><code class="language-bash">mkdir -p .claude/skills/flutter-release
touch .claude/skills/flutter-release/SKILL.md
</code></pre>
<pre><code class="language-markdown">---
name: flutter-release
description: |
  Use this skill when building a release APK or preparing the app for deployment.
  Triggers on: "build release", "generate apk", "prepare release", "release build".
allowed-tools: Bash Read
---

# Flutter release checklist

Run these steps in order. Do not skip any step.

1. Run `flutter pub get`
2. Run `dart run build_runner build --delete-conflicting-outputs`
3. Run `flutter analyze` — fix every error before proceeding. Do not continue with warnings treated as errors.
4. Run `flutter test` — if any test fails, fix it before continuing
5. Run `flutter build apk --release`
6. Confirm build output at `build/app/outputs/flutter-apk/app-release.apk`
7. Create a git commit: `chore: release build vX.X.X`

If any step fails, stop and report the error clearly. Do not skip ahead.
</code></pre>
<p>Now, whenever you type <code>"prepare a release"</code> or <code>"build the apk"</code>Claude follows this checklist without you having to remind it of the steps.</p>
<h3 id="heading-a-skill-for-conventional-commits">A Skill For Conventional Commits</h3>
<pre><code class="language-bash">mkdir -p .claude/skills/commit
touch .claude/skills/commit/SKILL.md
</code></pre>
<pre><code class="language-markdown">---
name: commit
description: |
  Use when committing changes or writing a commit message.
  Triggers on: "commit", "git commit", "commit changes", "write a commit message".
---

Follow Conventional Commits format:

Types: feat | fix | chore | refactor | docs | test | perf

Format: `type(scope): short imperative summary`

Rules:
- Subject line max 72 characters
- Imperative mood — "add" not "added", "fix" not "fixed"
- Scope = the feature name (auth, transfer, wallet, cards)

Examples:
- `feat(transfer): add beneficiary validation on amount input`
- `fix(wallet): correct kobo-to-naira display conversion`
- `chore(deps): upgrade riverpod to 2.6.1`

Always run `flutter analyze` before committing. Never commit with lint errors.
</code></pre>
<h3 id="heading-dynamic-context-injection">Dynamic Context Injection</h3>
<p>Skills support a powerful trick: you can inject live shell output directly into the skill body using <code>!`command`</code> syntax. Claude receives the output as part of the skill, not as a separate step.</p>
<p>For example, you could embed something like !<code>git status</code> inside a skill, so Claude always sees the current state of your repository when applying that skill. In a Flutter workflow, you could also use something like !<code>flutter test</code> so the skill dynamically includes the latest test results before Claude suggests fixes or improvements.</p>
<pre><code class="language-markdown">---
name: sprint-status
description: |
  Use when asked about current status, what's left to do, or what changed.
---

## Current git status
!`git status --short`

## Uncommitted changes
!`git diff --stat HEAD`

## Recent commits
!`git log --oneline -10`

## Lint status
!`flutter analyze 2&gt;&amp;1 | tail -20`

Review the above and give a concise summary of: what's done, what's broken, and what needs attention before the next commit.
</code></pre>
<p>Type <code>/sprint-status</code> and Claude gets a live snapshot of your project state before responding.</p>
<h2 id="heading-5-using-loop-for-self-correcting-workflows">5. Using /loop for Self-Correcting Workflows</h2>
<p><code>/loop</code> is a built-in Claude Code skill that reruns a task repeatedly until a condition is met. It's the difference between "fix this lint error" (one shot) and "fix all lint errors" (autonomous loop).</p>
<p>For example, instead of running a one-time prompt like “fix this lint error,” you would use <code>/loop fix lint errors in this Flutter project until there are no warnings left</code>. Claude will then repeatedly check the output, apply fixes, and recheck until the condition is satisfied.</p>
<p>A more realistic Flutter workflow could look like <code>/loop run flutter analyze and fix all reported issues until analysis passes clean</code>. In this case, Claude keeps running analyses, fixing issues, and revalidating until the project reaches a clean state.</p>
<p>It's worthy of note here that a<code>/loop</code> and a <code>Skill</code> solve two different problems, and it helps to think of them like this:</p>
<ul>
<li><p>A Skill is <em>knowledge</em>.</p>
</li>
<li><p>A Loop is <em>behavior over time</em>.</p>
</li>
</ul>
<p>The pattern is always the same: tell Claude what to run, what to check, and when to stop.</p>
<h3 id="heading-fix-until-clean">Fix Until Clean</h3>
<pre><code class="language-plaintext">/loop
Run flutter analyze.
If there are any errors or warnings, read each one carefully and fix it.
Run flutter analyze again.
Continue until flutter analyze reports zero issues.
Do not move on while there are errors remaining.
</code></pre>
<h3 id="heading-tdd-loop">TDD Loop</h3>
<pre><code class="language-plaintext">/loop
Run: flutter test --name "WalletNotifier"
If the test fails, read the failure output carefully.
Make the minimal code change required to fix the failure.
Do not change the test itself.
Run the test again.
Stop when the test passes with no errors.
</code></pre>
<h3 id="heading-build-a-screen-check-it-iterate">Build a Screen, Check it, Iterate</h3>
<pre><code class="language-plaintext">/loop
Look at the Figma spec notes in CLAUDE.md under "Remaining screens".
Pick the next incomplete screen.
Build the screen following the architecture pattern in lib/features/wallet/presentation/.
After building, run flutter analyze and fix any issues.
Add a comment `// DONE` at the top of the completed screen file.
Move to the next screen.
Stop after completing 3 screens.
</code></pre>
<p>A word of caution: <code>/loop</code> is powerful, but give Claude a clear stop condition. "<em>Keep going until it's perfect</em>" is <strong>not</strong> <strong>a stop condition</strong>. "<em>Stop when flutter analyze and flutter test both pass with zero issues.</em>" is.</p>
<h2 id="heading-6-subagents-for-parallel-screen-development">6. Subagents for Parallel Screen Development</h2>
<p>Subagents are isolated Claude instances that run a task in their own context window and then return only a summary to the main session. This changes how you think about working with Claude Code on a multi-screen project.</p>
<p>A simple way to understand it is to imagine building a full Flutter app with multiple screens. Without subagents, you would design the home screen, then the profile screen, then settings, all in one long conversation. Over time, the context gets heavier, and Claude starts losing focus on earlier decisions.</p>
<p>With subagents, it's like giving each screen to a different engineer. One works on the home screen, another builds the profile screen, and another handles settings. Each one works independently, follows the same project rules, and reports back only when the screen is ready. You then combine their output into the main project without losing clarity or consistency.</p>
<h3 id="heading-setting-up-a-screen-builder-subagent">Setting Up a Screen-Builder Subagent</h3>
<p>Create a file at <code>.claude/agents/screen-builder.md</code>:</p>
<pre><code class="language-markdown">---
name: screen-builder
description: Builds a single Flutter screen following the app's feature-first Riverpod architecture
model: claude-sonnet-4-6
tools: [Read, Write, Bash, Glob]
---

You are a Flutter engineer building a screen for a fintech app.

Before building anything:
1. Read lib/features/wallet/presentation/screens/wallet_screen.dart to understand the existing screen pattern
2. Read CLAUDE.md for conventions and architecture rules
3. Read the feature's existing providers in the presentation/providers/ folder

When building the screen:
- Follow the exact same structure as the existing screens
- Use AsyncValue pattern for loading/error/data states
- No business logic in the widget — all state goes through the provider
- Every monetary amount displayed in naira but stored in kobo (divide by 100 for display)
- Use GoRouter for navigation, not Navigator.push

After building:
- Run flutter analyze on the file
- Fix any errors
- Return a summary: file path created, provider used, any decisions made
</code></pre>
<h3 id="heading-using-it">Using it</h3>
<p>In your main session, you can now say:</p>
<pre><code class="language-plaintext">Use the screen-builder subagent to build the Transaction History screen.
The screen should show a list of transactions from the WalletNotifier provider.
Each item should display: amount (formatted), description, date, and status badge.
</code></pre>
<p>Claude dispatches the subagent, which reads your existing code for context, builds the screen following your patterns, fixes any lint errors, and returns a clean summary, without cluttering your main thread with every intermediate step.</p>
<p>You can also run multiple subagents simultaneously for truly parallel work:</p>
<pre><code class="language-plaintext">Dispatch three screen-builder subagents in parallel:
1. Transaction History screen (list of transactions)
2. Send Money screen (amount input + recipient selection)
3. Wallet Top-Up screen (amount input + payment method)

Each should follow the existing wallet feature patterns.
Report back when all three are complete.
</code></pre>
<h2 id="heading-7-hooks-enforcing-rules-deterministically">7. Hooks — Enforcing Rules Deterministically</h2>
<p>Skills and subagents influence how Claude thinks and plans, but hooks are different. Hooks are deterministic. They run automatically at specific lifecycle events, no matter what Claude decides to do. This makes them useful for enforcing hard rules in your workflow.</p>
<p>A simple way to understand it is to think of hooks as guards in a real engineering pipeline. For example, before any code is committed, a <code>PreToolUse hook</code> can run to check formatting or block unsafe changes. After a tool runs, a <code>PostToolUse hook</code> can validate the output. When a session ends, a <code>Stop hook</code> can trigger cleanup tasks or logging. Other events, like <code>SessionStart</code>, <code>PreCompact</code> help you initialize context or manage memory before Claude continues working.</p>
<p>In practice, hooks are how you enforce consistency. While Skills and subagents guide Claude’s behavior, hooks ensure certain actions always happen at the right moment, without relying on Claude to “remember” or “decide.”</p>
<h3 id="heading-block-edits-to-generated-files">Block Edits to Generated Files</h3>
<p>Generated files like <code>*.g.dart</code> and <code>*.freezed.dart</code> should never be edited manually — they get overwritten by <code>build_runner</code>. This hook blocks Claude from writing to them:</p>
<p>Create <code>.claude/hooks.json</code>:</p>
<pre><code class="language-json">{
  "PreToolUse": [
    {
      "matcher": "Write|Edit",
      "command": "bash -c 'if [[ \"\(CLAUDE_TOOL_INPUT_PATH\" == *.g.dart ]] || [[ \"\)CLAUDE_TOOL_INPUT_PATH\" == *.freezed.dart ]]; then echo \"Blocked: Do not edit generated files. Run build_runner instead.\"; exit 1; fi'"
    }
  ]
}
</code></pre>
<h3 id="heading-run-analyze-before-every-stop">Run Analyze Before Every Stop</h3>
<p>This hook runs <code>flutter analyze</code> before Claude considers its turn complete, catching lint errors before they accumulate:</p>
<pre><code class="language-json">{
  "Stop": [
    {
      "command": "bash -c 'result=\((flutter analyze 2&gt;&amp;1); if echo \"\)result\" | grep -q \"error •\"; then echo \"Flutter analyze found errors. Fix before stopping:\"; echo \"$result\"; exit 1; fi'"
    }
  ]
}
</code></pre>
<p>Now Claude can't finish a turn if there are lint errors. It gets blocked and has to fix them first.</p>
<h2 id="heading-8-putting-it-all-together-a-real-sprint-workflow">8. Putting It All Together: A Real Sprint Workflow</h2>
<p>Here's what a typical feature development session looks like when all of this is configured:</p>
<h3 id="heading-morning-check-project-state">Morning: Check Project State</h3>
<pre><code class="language-plaintext">/sprint-status
</code></pre>
<p>Claude reads live Git status, recent commits, and current lint output, then summarises what needs attention.</p>
<h3 id="heading-start-a-new-feature">Start a New Feature</h3>
<pre><code class="language-plaintext">I need to build the beneficiary management feature. 
Users should be able to save, view, and delete beneficiaries for the transfer flow.
Start with the data layer — Freezed model and repository interface.
</code></pre>
<p>Claude reads your CLAUDE.md and existing feature patterns, then builds the model and repository in the right place, following your conventions.</p>
<h3 id="heading-generate-all-the-screens-in-parallel">Generate All the Screens in Parallel</h3>
<pre><code class="language-plaintext">Use the screen-builder subagent to build:
1. BeneficiaryListScreen — shows saved beneficiaries with search
2. AddBeneficiaryScreen — form with account number and bank selection
3. BeneficiaryDetailScreen — shows details with delete option
</code></pre>
<h3 id="heading-fix-everything-until-its-clean">Fix Everything Until it's Clean</h3>
<pre><code class="language-plaintext">/loop
Run flutter analyze.
Fix all errors.
Run flutter test.
Fix any test failures.
Stop when both pass with zero issues.
</code></pre>
<h3 id="heading-commit-cleanly">Commit Cleanly</h3>
<pre><code class="language-plaintext">Commit the beneficiary feature
</code></pre>
<p>The commit skill triggers, runs analyze one more time, and creates a correctly-formatted conventional commit message.</p>
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<p>If there's one key takeaway from all of this, it's that Claude Code isn't just about prompting. It's about setup. The quality of its output is shaped far more by what you define about your project upfront than by what you type in the moment.</p>
<p>This is also what separates vibe coding from real AI-assisted engineering. Without structure, you end up guessing and reacting, which feels fast but breaks down quickly.</p>
<p>With the right setup, Claude becomes a pair programming partner that follows your conventions and handles execution while you focus on decisions that actually require engineering judgment. <strong>That shift is what lets you spend less time fixing generated code and more time solving the problems that matter.</strong></p>
<p>The payoff compounds. A <code>CLAUDE.md</code> takes 20 minutes to write. A <code>skill</code> for your release flow takes 10 minutes. But both of those pay for themselves the first time Claude correctly follows your process without you having to walk it through every step.</p>
<p>Start small: write your <code>CLAUDE.md</code> this week. Add one skill for the task you repeat most — committing, releasing, or running lint. Then, when you're comfortable, try a <code>/loop</code> on your next test-fixing session. The rest follows naturally.</p>
<p>The goal isn't to let AI write all your code. It's to stop spending your limited engineering time on the parts that don't require your judgment, and to spend more of it on the parts that do.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
