Bird
Raised Fist0
Fluttermobile~20 mins

Why clean architecture scales codebases in Flutter - Build It to Prove It

Choose your learning style10 modes available
Build: Clean Architecture Demo
This screen demonstrates a simple counter app built using clean architecture principles. It separates UI, business logic, and data layers to show how code can stay organized and scalable.
Target UI
┌─────────────────────────────┐
│       Clean Architecture     │
├─────────────────────────────┤
│                             │
│        Counter: 0            │
│                             │
│   [ Increment ] [ Reset ]   │
│                             │
└─────────────────────────────┘
Display a counter value starting at 0
Increment button increases the counter by 1
Reset button sets the counter back to 0
Separate UI, business logic, and data layers using clean architecture
Use a simple repository class to manage counter state
UI listens to changes and updates accordingly
Starter Code
Flutter
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CounterScreen(),
    );
  }
}

class CounterScreen extends StatefulWidget {
  const CounterScreen({super.key});

  @override
  State<CounterScreen> createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  // TODO: Add repository and business logic here

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Clean Architecture Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Counter:'),
            // TODO: Display counter value
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () {
                    // TODO: Increment counter
                  },
                  child: const Text('Increment'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () {
                    // TODO: Reset counter
                  },
                  child: const Text('Reset'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
Task 1
Task 2
Task 3
Task 4
Solution
Flutter
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

// Data Layer
class CounterRepository {
  int _counter = 0;

  int getCounter() => _counter;

  void increment() {
    _counter++;
  }

  void reset() {
    _counter = 0;
  }
}

// Domain Layer
class CounterUseCase {
  final CounterRepository repository;

  CounterUseCase(this.repository);

  int getCounter() => repository.getCounter();

  void incrementCounter() {
    repository.increment();
  }

  void resetCounter() {
    repository.reset();
  }
}

// Presentation Layer
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CounterScreen(),
    );
  }
}

class CounterScreen extends StatefulWidget {
  const CounterScreen({super.key});

  @override
  State<CounterScreen> createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  late final CounterRepository _repository;
  late final CounterUseCase _useCase;

  @override
  void initState() {
    super.initState();
    _repository = CounterRepository();
    _useCase = CounterUseCase(_repository);
  }

  void _increment() {
    setState(() {
      _useCase.incrementCounter();
    });
  }

  void _reset() {
    setState(() {
      _useCase.resetCounter();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Clean Architecture Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Counter:'),
            Text(
              '${_useCase.getCounter()}',
              style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _increment,
                  child: const Text('Increment'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _reset,
                  child: const Text('Reset'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

This example shows clean architecture by splitting the app into three layers:

  • Data Layer: CounterRepository holds the counter state and methods to change it.
  • Domain Layer: CounterUseCase contains business logic and uses the repository to get or update data.
  • Presentation Layer: The Flutter UI listens to user taps and calls use case methods. It updates the screen when the counter changes.

This separation helps keep code organized and easy to maintain as the app grows. Each layer has a clear job, so changes in one place don’t break others.

Final Result
Completed Screen
┌─────────────────────────────┐
│       Clean Architecture     │
├─────────────────────────────┤
│                             │
│        Counter: 0            │
│                             │
│   [ Increment ] [ Reset ]   │
│                             │
└─────────────────────────────┘
Tapping 'Increment' increases the counter number by 1 and updates the display.
Tapping 'Reset' sets the counter back to 0 and updates the display.
Stretch Goal
Add a feature to save the counter value persistently so it remains after app restart.
💡 Hint
Use Flutter's shared_preferences package to store and load the counter value in the repository.

Practice

(1/5)
1. Why does clean architecture help when a Flutter app grows bigger?
easy
A. It organizes code into layers with clear roles, making it easier to manage.
B. It makes the app run faster by using less memory.
C. It removes all comments to make code shorter.
D. It forces all code to be in one file for simplicity.

Solution

  1. Step 1: Understand the purpose of clean architecture

    Clean architecture divides code into layers, each with a clear responsibility.
  2. Step 2: Connect layering to app growth

    This separation helps manage complexity as the app grows, making it easier to maintain and extend.
  3. Final Answer:

    It organizes code into layers with clear roles, making it easier to manage. -> Option A
  4. Quick Check:

    Layered code = easier management [OK]
Hint: Think: clear roles help manage bigger apps [OK]
Common Mistakes:
  • Confusing performance with architecture benefits
  • Thinking clean architecture means fewer files
  • Believing it removes comments or code
2. Which of these is the correct way to separate layers in Flutter clean architecture?
easy
A. Put UI, business logic, and data access in separate folders.
B. Write all code in main.dart for simplicity.
C. Mix UI and data code in the same file to reduce files.
D. Only separate code by widget size, not by function.

Solution

  1. Step 1: Identify proper layer separation

    Clean architecture separates UI, business logic, and data access into different folders or layers.
  2. Step 2: Evaluate options

    Only Put UI, business logic, and data access in separate folders. correctly describes this separation; others mix concerns or ignore layering.
  3. Final Answer:

    Put UI, business logic, and data access in separate folders. -> Option A
  4. Quick Check:

    Separate by function = correct layering [OK]
Hint: Separate code by role, not by size or file count [OK]
Common Mistakes:
  • Putting all code in one file
  • Mixing UI and data logic
  • Separating by widget size instead of responsibility
3. Given this Flutter app structure using clean architecture, what happens if you change the data layer only?
lib/
  ui/
  domain/
  data/
medium
A. Domain layer must be rewritten to match data changes.
B. UI layer breaks because it depends directly on data layer.
C. The whole app crashes because layers are tightly coupled.
D. UI and domain layers remain unchanged and continue working.

Solution

  1. Step 1: Understand layer independence in clean architecture

    Each layer depends only on the layer below it, so data changes don't affect UI or domain directly.
  2. Step 2: Analyze impact of changing data layer

    Changing data layer alone won't break UI or domain if interfaces are respected.
  3. Final Answer:

    UI and domain layers remain unchanged and continue working. -> Option D
  4. Quick Check:

    Layer independence = safe data changes [OK]
Hint: Layers depend only on interfaces, not implementations [OK]
Common Mistakes:
  • Assuming UI depends directly on data
  • Thinking domain layer must change with data
  • Believing all layers break on data change
4. You see this error in a Flutter app using clean architecture:
Unhandled exception: Null check operator used on a null value
What is the most likely cause related to architecture?
medium
A. UI layer forgot to import the domain layer.
B. The data layer returned null but domain layer did not handle it.
C. Business logic is written inside UI widgets.
D. All layers are combined in one file causing confusion.

Solution

  1. Step 1: Identify cause of null check error

    The error means a null value was forced to be non-null, likely from data layer output.
  2. Step 2: Connect error to architecture layers

    If domain layer does not handle null from data layer properly, this error occurs.
  3. Final Answer:

    The data layer returned null but domain layer did not handle it. -> Option B
  4. Quick Check:

    Null safety requires domain to handle data nulls [OK]
Hint: Check null handling between data and domain layers [OK]
Common Mistakes:
  • Blaming UI import errors
  • Mixing business logic inside UI
  • Ignoring null safety in domain layer
5. You want to add a new feature to a large Flutter app using clean architecture. Which approach best keeps the code scalable?
hard
A. Skip domain layer and connect UI directly to data for simplicity.
B. Add all new code inside existing UI widgets to avoid creating new files.
C. Add new UI widgets, create domain use cases, and implement data repositories separately.
D. Modify the data layer to include UI logic for faster development.

Solution

  1. Step 1: Recall clean architecture roles

    UI handles display, domain handles business rules, data handles data access.
  2. Step 2: Apply layering to new feature

    Adding feature means updating each layer separately to keep code organized and scalable.
  3. Step 3: Avoid mixing layers

    Skipping the domain layer, adding all new code inside UI widgets, or modifying the data layer to include UI logic all mix concerns, breaking scalability and maintainability.
  4. Final Answer:

    Add new UI widgets, create domain use cases, and implement data repositories separately. -> Option C
  5. Quick Check:

    Separate layers for new features = scalable code [OK]
Hint: Add features layer by layer, never mix responsibilities [OK]
Common Mistakes:
  • Putting all new code in UI widgets
  • Mixing UI logic into data layer
  • Skipping domain layer for shortcuts
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard