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(
buildWhen: (previous, current) => previous.darkMode != current.darkMode,
builder: (context, settingState) {
return SwitchListTile(
value: settingState.darkMode,
title: const Text('Dark Mode'),
onChanged: (value) {
context.read().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 {
final GetTodoListUsecase _getTodoListUsecase;
TodoCubit(this._getTodoListUsecase) : super(TodoLoading());
Future 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
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.
- VS Code: Extension Link
- Android Studio: Plugin Link
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.
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.
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 {
final GetTodoListUsecase _getTodoListUsecase;
TodoCubit(this._getTodoListUsecase) : super(TodoLoading());
Future 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
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 {
final GetDarkModeUsecase _getDarkModeUsecase;
final SetDarkModeUsecase _setDarkModeUsecase;
SettingCubit(
this._getDarkModeUsecase,
this._setDarkModeUsecase,
) : super(SettingState(
darkMode: _getDarkModeUsecase(),
));
bool getDarkMode() => _getDarkModeUsecase();
Future 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
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 createState() => _TodoListScreenState();
}
class _TodoListScreenState extends State {
_init() {
context.read().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(
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(
buildWhen: (previous, current) => previous.darkMode != current.darkMode,
builder: (context, settingState) {
return SwitchListTile(
value: settingState.darkMode,
title: const Text('Dark Mode'),
onChanged: (value) {
context.read().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.