0
0
Fluttermobile~20 mins

Clean Architecture layers in Flutter - Mini App: Build & Ship

Choose your learning style9 modes available
Build: Clean Architecture Layers Demo
This screen shows a simple Flutter app demonstrating the three main layers of Clean Architecture: Presentation, Domain, and Data. It displays a list of tasks fetched from a data source through the domain layer.
Target UI
-------------------------
| Clean Architecture    |
|-----------------------|
| Tasks:                |
| 1. Buy groceries      |
| 2. Walk the dog       |
| 3. Read a book        |
|                       |
| [Refresh]             |
-------------------------
Display a list of tasks on the screen.
Use three layers: Presentation (UI), Domain (business logic), Data (data source).
Presentation layer calls Domain layer to get tasks.
Domain layer defines a UseCase to fetch tasks.
Data layer provides a repository implementation returning a fixed list of tasks.
Add a Refresh button to reload tasks.
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: TasksScreen(),
    );
  }
}

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

  @override
  State<TasksScreen> createState() => _TasksScreenState();
}

class _TasksScreenState extends State<TasksScreen> {
  // TODO: Add domain layer use case instance
  // TODO: Add list of tasks state

  @override
  void initState() {
    super.initState();
    // TODO: Load tasks initially
  }

  Future<void> _loadTasks() async {
    // TODO: Call domain layer to get tasks and update state
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Clean Architecture')),
      body: Column(
        children: [
          const Padding(
            padding: EdgeInsets.all(8.0),
            child: Text('Tasks:', style: TextStyle(fontSize: 20)),
          ),
          Expanded(
            child: ListView(
              children: [
                // TODO: Show tasks here
              ],
            ),
          ),
          ElevatedButton(
            onPressed: _loadTasks,
            child: const Text('Refresh'),
          ),
        ],
      ),
    );
  }
}

// TODO: Define domain layer use case

// TODO: Define data layer repository
Task 1
Task 2
Task 3
Task 4
Task 5
Task 6
Solution
Flutter
import 'package:flutter/material.dart';

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

// Entity
class Task {
  final String title;
  Task(this.title);
}

// Domain Layer
abstract class TaskRepository {
  Future<List<Task>> fetchTasks();
}

class GetTasksUseCase {
  final TaskRepository repository;
  GetTasksUseCase(this.repository);

  Future<List<Task>> call() async {
    return await repository.fetchTasks();
  }
}

// Data Layer
class DataTaskRepository implements TaskRepository {
  @override
  Future<List<Task>> fetchTasks() async {
    // Simulate network delay
    await Future.delayed(const Duration(milliseconds: 500));
    return [
      Task('Buy groceries'),
      Task('Walk the dog'),
      Task('Read a book'),
    ];
  }
}

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

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

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

  @override
  State<TasksScreen> createState() => _TasksScreenState();
}

class _TasksScreenState extends State<TasksScreen> {
  late final GetTasksUseCase _getTasksUseCase;
  List<Task> _tasks = [];
  bool _loading = false;

  @override
  void initState() {
    super.initState();
    _getTasksUseCase = GetTasksUseCase(DataTaskRepository());
    _loadTasks();
  }

  Future<void> _loadTasks() async {
    setState(() {
      _loading = true;
    });
    final tasks = await _getTasksUseCase();
    setState(() {
      _tasks = tasks;
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Clean Architecture')),
      body: Column(
        children: [
          const Padding(
            padding: EdgeInsets.all(8.0),
            child: Text('Tasks:', style: TextStyle(fontSize: 20)),
          ),
          Expanded(
            child: _loading
                ? const Center(child: CircularProgressIndicator())
                : ListView.builder(
                    itemCount: _tasks.length,
                    itemBuilder: (context, index) {
                      return ListTile(
                        title: Text(_tasks[index].title),
                      );
                    },
                  ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: ElevatedButton(
              onPressed: _loadTasks,
              child: const Text('Refresh'),
            ),
          ),
        ],
      ),
    );
  }
}

This Flutter app demonstrates Clean Architecture by separating the code into three layers:

  • Data Layer: DataTaskRepository implements TaskRepository and returns a fixed list of tasks simulating a data source.
  • Domain Layer: Defines the Task entity, the TaskRepository interface, and the GetTasksUseCase which fetches tasks from the repository.
  • Presentation Layer: The TasksScreen widget uses the use case to load tasks and display them in a list. It shows a loading spinner while fetching and a Refresh button to reload tasks.

This clear separation helps keep business logic independent from UI and data sources, making the app easier to maintain and test.

Final Result
Completed Screen
-------------------------
| Clean Architecture    |
|-----------------------|
| Tasks:                |
| 1. Buy groceries      |
| 2. Walk the dog       |
| 3. Read a book        |
|                       |
| [Refresh]             |
-------------------------
When the screen loads, tasks appear after a short loading spinner.
Tapping the Refresh button reloads the tasks and shows the spinner briefly.
Stretch Goal
Add a dark mode toggle button in the app bar that switches the app theme between light and dark.
💡 Hint
Use a StatefulWidget to hold the theme mode state and wrap MaterialApp with a ThemeMode property. Add an IconButton in the AppBar to toggle the mode.