Flutter Clean Architecture: Presentation Layer

October 29, 2024
Flutter Clean Architecture: Presentation Layer
Flutter Clean Architecture: Presentation Layer
Presentation Layer in Clean Architecture is responsible for showing UI (User Interface) to the user and to take user's input. It includes UI components such as screens, pages, widgets as well as different state management logic. The goal of this layer is to provide clean UI experience. It makes use of Domain Layer to get required data and populate those data in UI in appropriate manner.

Table of Contents

Screens (Pages)

Screen (Pages) represent different screens within your application. Each screen represent its own User Interface which is a collection of Widgets that make up a User Interface. It is also responsible for handling user interaction.

				
					import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_clean_architecture/feature/setting/presentation/cubit/setting/setting_cubit.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Setting')),
      body: ListView(
        children: [
          BlocBuilder<SettingCubit, SettingState>(
            buildWhen: (previous, current) => previous.darkMode != current.darkMode,
            builder: (context, settingState) {
              return SwitchListTile(
                value: settingState.darkMode,
                title: const Text('Dark Mode'),
                onChanged: (value) {
                  context.read<SettingCubit>().setDarkMode(value);
                },
              );
            },
          ),
        ],
      ),
    );
  }
}
				
			

In above example we have simple SettingScreen. It has some collection of widgets like Scaffold, AppBar, ListView etc. Here we can see that this screen is responsible to show user a Switch using which they can toggle dark mode on / off. Here we have used BlocBuilder to react to state change and update our UI accordingly.

Widgets

Widgets are building blocks of the UI. We combine different widgets to create a UI. Widgets are like LEGO Blocks, we combine it to create a Figure. Here LEGO Blocks represent widgets and Figure represent UI Interface.

				
					import 'package:flutter/material.dart';
import '../../domain/entities/user.dart';

class UserInfoWidget extends StatelessWidget {
  final User user;

  UserInfoWidget({required this.user});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Name: ${user.name}', style: TextStyle(fontSize: 20)),
        Text('Email: ${user.email}', style: TextStyle(fontSize: 16)),
      ],
    );
  }
}
				
			

Above example is a example of widget that shows User Information. It is a combination of other Text widgets. Which as a whole is a separate custom widget UserInfoWidget.

State Management

State Management is necessary to create interactive and responsive UI. We can use different types of state management tool like BLoC, Provider, Riverpod and many more. Any state management tool is good as long as we are comfortable with it. In this tutorial we are going to use BLoC State Management. To be more specific we are going to use Cubit provided within BLoC State Management.

				
					import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_clean_architecture/feature/todo/domain/entity/todo.dart';
import 'package:flutter_clean_architecture/feature/todo/domain/usecase/get_todo_list_usecase.dart';
import 'package:injectable/injectable.dart';

part 'todo_state.dart';

@LazySingleton()
class TodoCubit extends Cubit<TodoState> {
  final GetTodoListUsecase _getTodoListUsecase;
  TodoCubit(this._getTodoListUsecase) : super(TodoLoading());

  Future<void> getTodoList() async {
    try {
      emit(TodoLoading());
      var list = await _getTodoListUsecase();
      emit(TodoLoaded(list: list));
    } catch (e) {
      if (e is DioException) {
        emit(TodoError(message: e.message ?? e.toString()));
      } else {
        emit(TodoError(message: e.toString()));
      }
    }
  }
} 
				
			

In above example we have a cubit file.

				
					part of 'todo_cubit.dart';

sealed class TodoState extends Equatable {
  const TodoState();

  @override
  List<Object> get props => [];
}

final class TodoLoading extends TodoState {}

final class TodoError extends TodoState {
  final String message;

  const TodoError({required this.message});
}

final class TodoLoaded extends TodoState {
  final List<Todo> list;

  const TodoLoaded({required this.list});
}
				
			

In above example we have state file for this specific cubit.

We will learn more about BLoC State Management in our upcoming tutorials.

Real Project Implementation

Step 1: Creating cubits and states

Bloc extension for VS Code / Android Studio will help to create bloc / cubit by writing initial boilerplate code.

Here in this tutorial we are going to view VS Code specific implementation for this package. You can use this plugin in two ways in VS Code.

One is you can right click on the directory where you want to create a cubit / bloc. Then follow along the prompts.

VS Code Bloc Extension

The next method is you can use Command Palette from VS Code.

  • For Mac: CMD + Shift + p
  • For Mac: Ctrl + Shift + p

While in Command Palette write cubit or block to get the command to create cubit / bloc. Then follow along the prompts.

VS Code Bloc Extension

Create cubit for todo & setting.

				
					import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_clean_architecture/feature/todo/domain/entity/todo.dart';
import 'package:flutter_clean_architecture/feature/todo/domain/usecase/get_todo_list_usecase.dart';
import 'package:injectable/injectable.dart';

part 'todo_state.dart';

@LazySingleton()
class TodoCubit extends Cubit<TodoState> {
  final GetTodoListUsecase _getTodoListUsecase;
  TodoCubit(this._getTodoListUsecase) : super(TodoLoading());

  Future<void> getTodoList() async {
    try {
      emit(TodoLoading());
      var list = await _getTodoListUsecase();
      emit(TodoLoaded(list: list));
    } catch (e) {
      if (e is DioException) {
        emit(TodoError(message: e.message ?? e.toString()));
      } else {
        emit(TodoError(message: e.toString()));
      }
    }
  }
}
				
			
				
					part of 'todo_cubit.dart';

sealed class TodoState extends Equatable {
  const TodoState();

  @override
  List<Object> get props => [];
}

final class TodoLoading extends TodoState {}

final class TodoError extends TodoState {
  final String message;

  const TodoError({required this.message});
}

final class TodoLoaded extends TodoState {
  final List<Todo> list;

  const TodoLoaded({required this.list});
}
				
			
				
					import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_clean_architecture/feature/setting/domain/usecase/get_dark_mode_usecase.dart';
import 'package:flutter_clean_architecture/feature/setting/domain/usecase/set_dark_mode_usecase.dart';
import 'package:injectable/injectable.dart';

part 'setting_state.dart';

@LazySingleton()
class SettingCubit extends Cubit<SettingState> {
  final GetDarkModeUsecase _getDarkModeUsecase;
  final SetDarkModeUsecase _setDarkModeUsecase;
  SettingCubit(
    this._getDarkModeUsecase,
    this._setDarkModeUsecase,
  ) : super(SettingState(
          darkMode: _getDarkModeUsecase(),
        ));

  bool getDarkMode() => _getDarkModeUsecase();

  Future<void> setDarkMode(bool darkMode) async {
    await _setDarkModeUsecase(darkMode);
    emit(state.copyWith(darkMode: darkMode));
  }
}
				
			
				
					part of 'setting_cubit.dart';

class SettingState extends Equatable {
  final bool darkMode;
  const SettingState({
    required this.darkMode,
  });

  @override
  List<Object> get props => [darkMode];

  SettingState copyWith({
    bool? darkMode,
  }) {
    return SettingState(
      darkMode: darkMode ?? this.darkMode,
    );
  }
}
				
			

Step 2: Creating screens / pages

We will create required screens / pages required for our tutorial. Create todo_list_screen.dart file in lib/feature/todo/presentation/screen/todo_list_screen directory.

				
					import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_clean_architecture/feature/setting/presentation/screen/setting_screen/setting_screen.dart';
import 'package:flutter_clean_architecture/feature/todo/presentation/cubit/todo/todo_cubit.dart';

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

  @override
  State<TodoListScreen> createState() => _TodoListScreenState();
}

class _TodoListScreenState extends State<TodoListScreen> {
  _init() {
    context.read<TodoCubit>().getTodoList();
  }

  @override
  void initState() {
    _init();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Todo'),
        actions: [
          IconButton(
            onPressed: () {
              Navigator.of(context).push(
                MaterialPageRoute(builder: (context) => const SettingScreen()),
              );
            },
            icon: const Icon(Icons.settings),
          ),
        ],
      ),
      body: BlocBuilder<TodoCubit, TodoState>(
        builder: (context, state) {
          if (state is TodoError) {
            return Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(state.message, textAlign: TextAlign.center),
                Align(
                  child: FilledButton(
                    onPressed: () {
                      _init();
                    },
                    child: const Text('Retry'),
                  ),
                ),
              ],
            );
          } else if (state is TodoLoaded) {
            return ListView.builder(
              itemCount: state.list.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(state.list[index].title),
                  subtitle: Text('Completed: ${state.list[index].completed}'),
                );
              },
            );
          } else {
            return const Center(child: CircularProgressIndicator());
          }
        },
      ),
    );
  }
}
				
			

Then create setting_screen.dart file in lib/feature/setting/presentation/screen/setting_screen directory.

				
					import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_clean_architecture/feature/setting/presentation/cubit/setting/setting_cubit.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Setting')),
      body: ListView(
        children: [
          BlocBuilder<SettingCubit, SettingState>(
            buildWhen: (previous, current) => previous.darkMode != current.darkMode,
            builder: (context, settingState) {
              return SwitchListTile(
                value: settingState.darkMode,
                title: const Text('Dark Mode'),
                onChanged: (value) {
                  context.read<SettingCubit>().setDarkMode(value);
                },
              );
            },
          ),
        ],
      ),
    );
  }
}
				
			

This completes our Presentation Layer. In upcoming chapters we will learn about other specific implementation.

You can find all of the implementation in following Github link: Github Repository

Read more in upcoming chapters to learn more about all of the layers of Clean Architecture.

Flutter Clean Architecture: Introduction & Project Setup
Flutter Clean Architecture: Domain Layer
Flutter Clean Architecture: Data Layer
Flutter Clean Architecture: Presentation Layer
Flutter Clean Architecture: Dependency Injection

Related

Annapurna Circuit Trek

10 Best Treks in Nepal

Flutter Clean Architecture: Dependency Injection

Flutter Clean Architecture: Dependency Injection

Flutter Clean Architecture: Data Layer

Flutter Clean Architecture: Data Layer

Flutter Clean Architecture: Domain Layer

Flutter Clean Architecture: Domain Layer

Flutter Clean Architecture Introduction & Project Setup

Flutter Clean Architecture: Introduction & Project Setup