This project is a template for quickly launching a Flutter application using BLoC.
- FVM (Flutter Version Management): Manages the Flutter SDK version through the
./fvm/fvm_config.jsonfile.
- Flutter: Official extension managed by the Flutter team ;
- bloc: Facilitates the generation of BLoC widgets and files. Developed by the creator of BLoC ;
- Dart Data Class Generator (optional): Simplifies the creation of data models ;
- dart-import (optional): Automatically cleans up imports, converting them to relative paths ;
- flutter-stylizer (optional): Automatically formats Flutter code. You can customize the order of objects in the options ;
- Json to Dart Model (optional): Generates a document model from JSON. Useful when creating API return classes ;
- Pubspec Dependency Search (optional): Links the
pubspec.yamlfile to the pub.dev website ; - Flutter Intl: Manages the application's translations.
- Effective Dart: Dart's best practices, all Flutter project should follow them ;
- Flutter's Perfomance & Optimization guidelines: Flutter's best practices in terms of performance and optimization ;
- BLoC's documentation: BLoC's documentation.
get_it: Enables dependency injection without using context ;flutter_bloc: BLoC package adapted for Flutter ;intl: Provides internationalization and localization facilities, including message translation, plurals and genders, date/number formatting and parsing, and bidirectional text ;flutter_localizations: Localizations for the Flutter library ;http: A composable, Future-based library for making HTTP requests.
shared_preferences: Manages phone shared preferences. Shared preferences do not replace a local database ;isar: Local database. I prefer to use Isar because it is faster than Hive and easier to use than Sqflite.
The BLoC (or Cubit) replaces a "traditional" view model. It is responsible for managing the screen logic and executing calls to the data layer. Like a view model, it can be defined when used, in the build method of a widget. But it can also be defined globally at the app initialization using a BlocProvider:
// app.dart
...
return MultiBlocProvider(
providers: BlocSetup.globalBlocs,
child: MaterialApp(
// ...
),
);In this example, our BLoCs are defined in the bloc_setup.dart file:
// bloc_setup.dart
class BlocSetup {
static List<BlocProvider> globalBlocs = [
BlocProvider(create: (context) => ConnectivityCubit())
// ...
];
}
Once defined, the BLoC can be retrieved using the context:
final bloc = context.read<ConnectivityCubit>();
// or
ConnectivityCubit bloc = context.read();It is also possible to use a BlocProvider in a widget to pass it to its children via the context. However, I prefer passing them as parameters because it makes debugging easier in case of omission, even if it complicates the screen.
Flutter's BLoC works by using a system of state (State) and events (Event) exchange.
The BLoC is equivalent to the view model. It intercepts events (events), performs logic, and sends states (states).
States (states) are objects that the BLoC sends to the screen. They are defined in a file and all inherit from an abstract class.
Events (events), like states (states), are objects. This time, they are sent from the screen to the BLoC. When the BLoC intercepts them, it performs logic defined by the developer and can send a state.
The Cubit is a simplified version of the BLoC, which does not use events to communicate but directly uses methods defined in the Cubit.
To link the BLoC (or cubit) with the screen, a BlocBuilder is used:
...
Widget build(BuildContext context) {
final Cubit = HomeCubit()..init();
return AppBody(
title: 'Home',
body: Padding(
padding: const EdgeInsets.all(24.0),
child: BlocBuilder(
bloc: Cubit,
builder: (BuildContext context, HomeState state) {
// ...
},
),
),
);
}In the builder of the BlocBuilder, we have access to the state returned by the BLoC. Whenever a new state is emitted, the content of the builder is refreshed, so it is possible to change the display based on the state.
...
builder: (BuildContext context, HomeState state) {
if (state is HomeLoadingState || state is HomeInitialState) {
return const Center(child: CircularProgressIndicator());
}
if (state is HomeLoadedState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: state.data.map((e) => Text(e)).toList(),
),
const SizedBox(height: 24.0),
OutlinedButton(
onPressed: Cubit.loadMore,
child: const Text('Load More'),
),
],
);
}
return const Center(child: Text('Error'));
},
...BLoC is more recommended when automated tests are in place because the State/Event architecture would simplify test writing. However, Cubits are simpler to manage, write, and understand. I recommend using them whenever possible.
├── assets/
├── configs/
└── lib/
├── config/
└── theme/
├──core/
├── api/
├── constants/
├── global_blocs/
├── models/
└── services/
├── helpers/
└── ui/
├── components/
├── screens/
└── example_screen/
├── components/
├── example_bloc/
└── example_screen.dart
└── ui_helpers/
assets: Contains the application's assets, such as images, fonts, etc... ;configs: Contains the application's environment variables, such as the API URL, the application name, etc...
config: Contains the application configuration, such as the router and definition of variants (flavors)... ;core: Includes the core logic of the application, with models, services, etc... ;helpers: Contains global functions and extensions that do not interact with the user interface ;ui: Contains the visible layer of the application, where shared components, screens, and utilities related to the user interface are found.
Note that a screen's folder includes its BLoC.
Difference between BLoC and Cubit for the HomeScreen screen.
// home_state.dart
part of 'home_Cubit.dart';
sealed class HomeState {}
final class HomeInitialState extends HomeState {}
final class HomeLoadingState extends HomeState {}
final class HomeLoadedState extends HomeState {
HomeLoadedState(this.data);
final List<String> data;
}// home_event.dart
part of 'home_bloc.dart';
sealed class HomeEvent {}
final class HomeInitEvent extends HomeEvent {}
final class HomeLoadMoreEvent extends HomeEvent {}
// home_bloc.dartimport 'package:flutter_bloc/flutter_bloc.dart';
part 'home_event.dart';
part 'home_state.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> {
HomeBloc() : super(HomeInitial()) {
on<HomeInitEvent>((event, emit) async {
emit(HomeLoadingState());
await _homeService.getHomeData().then(
(data) => emit(HomeLoadedState(data)),
);
});
on<HomeLoadMoreEvent>((event, emit) async {
if (state is! HomeLoadedState) return;
final currentData = [...((state as HomeLoadedState).data)];
emit(HomeLoadingState());
await _homeService.getMoreData().then((data) => emit(
HomeLoadedState([...currentData, ' ', ...data]),
));
});
}
}// home_screen.dartimport 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../components/app_body.dart';
import 'home_Cubit/home_Cubit.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final bloc = HomeBloc()..add(HomeInitEvent());
return AppBody(
title: 'Home',
body: Padding(
padding: const EdgeInsets.all(24.0),
child: BlocBuilder(
bloc: bloc,
builder: (BuildContext context, HomeState state) {
if (state is HomeLoadingState || state is HomeInitialState) {
return const Center(child: CircularProgressIndicator());
}
if (state is HomeLoadedState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: state.data.map((e) => Text(e)).toList(),
),
const SizedBox(height: 24.0),
OutlinedButton(
onPressed: () {
bloc.add(HomeLoadMoreEvent);
},
child: const Text('Load More'),
),
],
);
}
return const Center(child: Text('Error'));
}),
),
);
}
}
// home_state.dart
part of 'home_Cubit.dart';
sealed class HomeState {}
final class HomeInitialState extends HomeState {}
final class HomeLoadingState extends HomeState {}
final class HomeLoadedState extends HomeState {
HomeLoadedState(this.data);
final List<String> data;
}
// home_Cubit.dartimport 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/services/home_service.dart';
part 'home_state.dart';
class HomeCubit extends Cubit<HomeState> {
HomeCubit() : super(HomeInitialState());
final _homeService = HomeService();
Future init() async {
emit(HomeLoadingState());
await _homeService.getHomeData().then(
(data) => emit(HomeLoadedState(data)),
);
}
Future loadMore() async {
if (state is! HomeLoadedState) return;
final currentData = [...((state as HomeLoadedState).data)];
emit(HomeLoadingState());
await _homeService.getMoreData().then((data) => emit(
HomeLoadedState([...currentData, ' ', ...data]),
));
}
}// home_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../components/app_body.dart';
import 'home_Cubit/home_Cubit.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final Cubit = HomeCubit()..init();
return AppBody(
title: 'Home',
body: Padding(
padding: const EdgeInsets.all(24.0),
child: BlocBuilder(
bloc: Cubit,
builder: (BuildContext context, HomeState state) {
if (state is HomeLoadingState || state is HomeInitialState) {
return const Center(child: CircularProgressIndicator());
}
if (state is HomeLoadedState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: state.data.map((e) => Text(e)).toList(),
),
const SizedBox(height: 24.0),
OutlinedButton(
onPressed: Cubit.loadMore,
child: const Text('Load More'),
),
],
);
}
return const Center(child: Text('Error'));
}),
),
);
}
} "flutterStylizer": {
"memberOrdering": [
"public-constructor",
"named-constructors",
"private-static-variables",
"private-instance-variables",
"public-static-variables",
"public-instance-variables",
"public-override-variables",
"public-override-methods",
"private-other-methods",
"public-other-methods",
"build-method"
],
"groupAndSortGetterMethods": true,
"groupAndSortVariableTypes": true,
"sortOtherMethods": true,
"processEnumsLikeClasses": false,
"sortClassesWithinFile": false
},
