Building Flutter Apps (1/5) — Shared Patterns Across Three Flutter Projects: UI and State

QuietLeaf, Minesweeper, GrowNote — the Flutter pattern architecture shared across three projects


Summary

  • Documented shared patterns and reuse strategies across three Flutter projects: QuietLeaf (Riverpod + Hive), Minesweeper (BLoC + Hive), and GrowNote (Riverpod + SQLite)
  • UI patterns including the theme system, settings panel, and custom widgets are shared across projects to eliminate duplicated development effort
  • Declaring [tech] relationships between projects in the ontology creates a structure where a bug fix in one project proactively prevents the same bug in another

Background

Running a single Flutter project gives no reason to think about pattern reuse. Three projects change that entirely. The same UI gets built three times, the same bug gets fixed three times, the same settings screen gets designed three times.

QuietLeaf (minimal text reader), Minesweeper (hexagonal grid game), GrowNote — these three apps serve different purposes but share the Flutter stack. Systematizing those shared touchpoints improves both development velocity and output quality.


Body

1. Shared UI Patterns — Theme, Settings, Widgets

Theme system: All three apps support dark and light mode. The theme-switching architecture first designed in QuietLeaf — a ThemeData factory function paired with user preference persistence — was applied as-is to Minesweeper and GrowNote. Only the color palette differs per app; the structure is identical.

// Common theme factory pattern (inject app-specific colors only)
ThemeData buildAppTheme({
  required ColorScheme colorScheme,
  required TextTheme textTheme,
}) {
  return ThemeData(
    colorScheme: colorScheme,
    textTheme: textTheme,
    useMaterial3: true,
  );
}

Settings panel: Settings screen layout patterns converge quickly. Section headers, toggle switches, sliders, option lists — this combination repeats across projects. The SettingsTile and SettingsSection widgets built in QuietLeaf were generalized and reused in the other projects.

Custom widgets: Common widgets such as loading indicators, empty state screens, and error screens follow the same pattern. Only the illustration assets or copy differ per app; the structure is the same.


2. State Management Comparison — Riverpod vs BLoC, When to Use Which

The three projects use two state management tools.

Project Tool State Characteristics
QuietLeaf Riverpod Reactive, settings-driven
Minesweeper BLoC Event-driven, transaction-oriented
GrowNote Riverpod Data-driven, CRUD-oriented

The selection criteria are clear. Data-flow apps — where a settings change propagates to the UI, or a DB update refreshes a list — benefit from Riverpod's declarative reactivity. Event-driven apps — where user input triggers complex logic and explicit state transitions — benefit from BLoC's explicit flow.

// Riverpod — reactive data flow example (GrowNote)
final growthEntriesProvider = StreamProvider<List<GrowthEntry>>((ref) {
  final db = ref.watch(databaseProvider);
  return db.watchAllEntries();
});

// BLoC — event → state transition example (Minesweeper)
class GameBloc extends Bloc<GameEvent, GameState> {
  GameBloc() : super(GameInitial()) {
    on<RevealCell>(_onRevealCell);
    on<FlagCell>(_onFlagCell);
    on<ResetGame>(_onResetGame);
  }
}

Using both tools in parallel does carry a learning overhead. However, choosing the right tool for the problem lowers long-term maintenance cost.


3. Ontology [tech] Relationships — Why Declare Them

The project ontology defines the following relationships: - QuietLeaf ↔ Minesweeper: Flutter stack shared — UI patterns, state management [tech] - QuietLeaf ↔ GrowNote: Flutter stack shared — UI patterns, state management [tech] - Minesweeper ↔ GrowNote: Flutter stack shared — widget pattern reuse [tech]

Why is this declaration necessary? When a theme-related bug is fixed in QuietLeaf, the context that "Minesweeper and GrowNote use the same pattern and require inspection" activates automatically. Relationship definitions in the ontology are not merely documentation — they are executable metadata that determines the scope of work.


4. Cross-Project Learning — Preventing Bug Propagation

A concrete case: QuietLeaf had a bug where certain widget colors were not immediately updated when switching dark mode. The root cause was const-declared widgets failing to detect theme changes.

// Problem — const widget does not detect theme changes
const Icon(Icons.settings, color: Colors.grey)  // ❌

// Fix — remove const to allow theme propagation
Icon(Icons.settings, color: Theme.of(context).iconTheme.color)  // ✓

After fixing this bug, Minesweeper and GrowNote — which use the same pattern — were inspected. The same issue was latent in Minesweeper and was resolved before reaching users.

This kind of cross-project learning is impossible without awareness of inter-project relationships. If each project were managed in complete isolation, the same bug would have been encountered three times.


5. Shared Test Patterns

Test patterns that repeat across all three projects:

Widget tests: Verify that settings screens render correctly and that toggle changes update state. The test structure is nearly identical — a test template written in one project is copied to another and modified.

State management tests: For Riverpod, the pattern of constructing a ProviderContainer and asserting state changes is standardized. For BLoC, using blocTest is the established pattern.

// Riverpod test pattern (common structure)
test('setting change reflects in state', () async {
  final container = ProviderContainer();
  addTearDown(container.dispose);
  // ...
});

// BLoC test pattern (common structure)
blocTest<GameBloc, GameState>(
  'emits [GamePlaying] when RevealCell added',
  build: () => GameBloc(),
  act: (bloc) => bloc.add(RevealCell(row: 0, col: 0)),
  expect: () => [isA<GamePlaying>()],
);

Golden tests: Screenshot comparison tests to catch UI regressions. Validates rendering across theme changes, dark/light mode switching, and various screen sizes. This pattern is applied identically across all three projects.

Sharing test patterns significantly reduces setup time when starting tests for a new project.


Lessons Learned

Over-abstraction attempt: An attempt was made to extract shared code across the three projects into a separate package. However, the volume of per-app customization made the abstraction cost exceed the reuse benefit. Copy-and-modify proved to be the more practical approach.

State management unification attempt: Operating under the assumption that using a single tool would reduce learning overhead, Minesweeper was initially started with Riverpod. Event handling felt unnatural, which led to a migration to BLoC. Choosing the right tool for the problem matters more than standardizing on one tool.


Conclusion

The core principle for operating multiple Flutter projects is: share patterns, but choose tools based on the problem. Patterns such as the theme system, settings panel, and test structure are transferable across projects. However, state management tool selection and storage choices must be determined by the data characteristics of each individual app.

Declaring [tech] relationships between projects in the ontology is not a documentation exercise. It creates a structure where experience from one project propagates automatically to others. As the number of projects grows, the value of these relationship definitions compounds.

Series overview: Series index

๋Œ“๊ธ€

์ด ๋ธ”๋กœ๊ทธ์˜ ์ธ๊ธฐ ๊ฒŒ์‹œ๋ฌผ

Agent Memory Engine (2/10) — Building an AI Agent Memory System with SQLite Alone

"ML Foundations (9/9) — PyTorch vs TensorFlow, and the Road to Local LLMs"

"RAG Core Study (14/26) — Evaluation Sets with RAGAS & DeepEval"

"ML Foundations (8/9) — Deep Learning Architectures: CNN, RNN, Attention"

"ML Foundations (7/9) — Deep Learning Training: Optimizers, Regularization, Initialization"

OpenClaw to Hermes Migration (2/13) — What to Preserve, Partially Port, or Discard

AI Agents I Built (5/7) — Building an Automated Blogger API Publishing System