This repository provides you to learn following tech stacks in Flutter.
- How to manage application state
- How to add ads into your application
- How to manage persistent data
- How to do unit test by using bloc
See Architecture.png
- You can copy this repository without any permission.
- You can make comment in issue.
Pros of using bloc pattern are the following things.
- You can definetly separate concerns as 3 Layers
- UI Layer
- Buisiness logic layer
- Data source layer This approach helps developers to develop maintanable, stable and robust applications.
-
Unified coding style in your team.
-
You can easily find where the logic implement
A complehensive document is written in this link.
Before you learn Bloc in Flutter, you should know the notion of Stream and Provider.
Bloc has two features of Bloc and Cubit to manage states.
In my point of view, Difference of Cubit and Bloc is event driven or function driven. Cubit doesn't have event in itself. So if you want to change your state that is managed by Cubit, you have to call function in Cubit. So Cubit is be able to create as simply than Bloc but Bloc is able to provide complicated logics. The following sections are some my opinions to decide which you use Cubit or Bloc.
Simple state like a counter. The state is simple structure and is decided simple logic.
Pros is that you can make it less boiler plate than Bloc.
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() {
addError(Exception('increment error!'), StackTrace.current);
emit(state + 1);
}
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
@override
void onError(Object error, StackTrace stackTrace) {
print('$error, $stackTrace');
super.onError(error, stackTrace);
}
}
The state whose data has to be retrieved by repository layer or complicated logic.
The following sample can be seen at /lib/blocs/country_list_bloc.dart
class CountryListBloc
extends Bloc<CountryListStateChangeEvent, CountryListState> {
CountryListBloc(FavoriteCountryIsarRepository repository)
: super(CountryListState.initialize()) {
on<CountryListStateChangeEvent>((event, emit) async {
await event.when(
countryListStateChangeEvent: (isFavorite, code) {
final List<Country> newCountryList = state.countryList.map((it) {
if (it.code == code) {
return it.copyWith(code: code, isFavorite: isFavorite);
}
return it;
}).toList(growable: false);
emit(state.copyWith(countryList: newCountryList));
},
countryListInitializeEvent: () async {
final codeList = await repository.getAllFavoriteCountries();
final unwrappedList =
codeList.whereType<String>().toList(growable: false);
final newState = CountryListState.initializeState(unwrappedList);
emit(newState);
},
);
}, transformer: concurrent());
}
}
All of states in this application are managed by Bloc
.
At least you should know the following built-in bloc functions.
- BlocProvider
- BlocBuilder
- BlocListener
You have to wrap your root or almost top level widget to valid bloc in your project.
Most project has up to two Bloc or Cubit instance. So you can use MultiBlocProvider
.
Then it would be said Deep Injection
. You can use Bloc instance throght BuildContext
object. like the below.
final favoriteBloc = context.read<FavoriteFilterBloc>();
You can see the following sample at /lib/main.dart
.
child: MultiBlocProvider(
providers: [
BlocProvider<MainScreenStateBloc>(
create: (context) => MainScreenStateBloc(),
),
BlocProvider<TopCountrySelectBloc>(
create: (context) => TopCountrySelectBloc(),
),
BlocProvider<BottomCountrySelectBloc>(
create: (context) => BottomCountrySelectBloc(),
),
BlocProvider<PositionSelectBloc>(
create: (context) => PositionSelectBloc()),
BlocProvider<CountryListBloc>(
create: (context) => CountryListBloc(
context.read<FavoriteCountryIsarRepository>())),
BlocProvider<FavoriteFilterBloc>(
create: (context) => FavoriteFilterBloc()),
BlocProvider<AdWatchBloc>(create: (context) => AdWatchBloc()),
],
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
routerConfig: AppRouter.goRouter,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.white),
useMaterial3: true,
),
)),
If you wrap your widget in BlocBuilder, you can get the state that specified by type of state you defined and when the state is changed, Flutter detect the mutation and run build function again.
BlocBuilder<BottomCountrySelectBloc, BottomCountrySelectState>(
builder: (context, state) {
return CountryView(
country: state.country!,
onTap: () {
context.read<PositionSelectBloc>().add(
const PositionSelectEvent(
position: PositionSelect.bottom()));
context.read<MainScreenStateBloc>().add(
const MainScreenStateEvent.screenStateChangeEvent(
screenType: MainScreenType.select()));
},
);
}),
It is similar to BlocBuilder but a bit differenct.
BlocBuilder and BlocListener detect mutation of its state. BlocBuilder rebuilds in the child widget, on the other hand BlocListener doesn't rebuild its child. Only detect mutation. BlocListener can detect multipul state mutation by using MultiBlocListener
return MultiBlocListener(
listeners: [
BlocListener<BottomCountrySelectBloc, BottomCountrySelectState>(
listener: (context, state) {
topCountryCode.value = state.country!.code;
}),
BlocListener<TopCountrySelectBloc, TopCountrySelectState>(
listener: (context, state) {
bottomCountryCode.value = state.country!.code;
}),
],
child: BlocBuilder<MainScreenStateBloc, MainScreenState>(
builder: (context, state) {
return state.screenType == const MainScreenType.top()
? TopViewScaffold(
isComparable: topCountryCode.value != CountryCode.UNTIL &&
bottomCountryCode.value != CountryCode.UNTIL,
)
: CountryListView();
}),
);
BlocListener doesn't work inside BlocBuilder. You have to wrap it outside of all BlocBuilder.
// BAD
return BlocBuilder<T, U>(
builder: (context,state) {
return MultiBlocListener(
listeners: [
// some listeners...
],
child: // Any widgets...,
);
}
);
- I could detect state changing by a BlocListener inside a BlocBuilder.
- I know why the exapmle doesn't work.
Feel free to open issue or give me your opinion by email!
You can use built-in features in bloc when you develop repository layer.
For example, you made repository like the following code.
class CountryAttributesRepositoryImpl implements CountryAttributesRepository {
final dio = Dio();
@override
Future<CountryAttributes> getAttribute(CountryCode countryCode) async {
try {
final url = '${GlobalConfig.apiUrl}countryinformation';
final response = await dio.get(
url,
queryParameters: {'countryCode': countryCode.codeString},
);
final json = response.data as Map<String, dynamic>;
return CountryAttributes.fromJson(json);
} catch (error) {
print(error);
return CountryAttributes.fromJson({});
}
}
}
How do you implement Deep Injection
?. You can use Repository Provider which Bloc produces by default. As you wrap your almost root widget by BlocProvider, you also have to wrap your almost root widget by RepositoryProvider to valid Deep Injection your repository.
If you have only one repository, you don't have to implement MultiRepositoryProvider
, but almost cases you have to implement multiple repository so the following code shows the example of MultiRepositoryProvider
.
class MyApp extends StatelessWidget {
MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiRepositoryProvider(
providers: [
RepositoryProvider<CountryAttributesRepository>(
create: (context) => CountryAttributesRepositoryImpl(),
),
RepositoryProvider<FavoriteCountryIsarRepository>(
create: (context) => FavoriteCountryIsarRepository(),
),
RepositoryProvider<ShowMoreInterstitialAdRepository>(
lazy: false,
create: (context) => ShowMoreInterstitialAdRepository(),
),
RepositoryProvider<ShowRateInterstitialAdRepository>(
lazy: false,
create: (context) => ShowRateInterstitialAdRepository(),
),
],
// countinue codes
The important thing you should know is that Repository providers are not created when the build
function that is in MyApp
widget.
You can see the lazy
parameter in RepositoryProvider. You can imagine that if RepositoryProcivider doesn't set lazy:false
it will be created when it is used for the first time.(the meaning lazy
is very intuitive in this case, I think ๐ค).
In this case, I have to create instance becase these repositories that have lazy: false
have initialize logic. You should choose whether the repository has to have lazy
or not. It's definetly case-by-case. I would say you could find during developing your application.
Once you implement RepositoryProviders in your project, you can use these repositories through the BuildContext
.
context.read<CountryAttributesRepository>().getAttribute(topCountryCode),
And you can inject them to your Bloc objects.
BlocProvider<AdWatchBloc>(create: (context) => AdWatchBloc()),
The example of implementing Bloc by using repository DI.
on<T>()
function, don't forget putting await
keyword before event.when
!!!
If you forget, you might come across async gap error!!!
class CountryListBloc
extends Bloc<CountryListStateChangeEvent, CountryListState> {
CountryListBloc(FavoriteCountryIsarRepository repository)
: super(CountryListState.initialize()) {
on<CountryListStateChangeEvent>((event, emit) async {
await event.when( // Don't forget await!
countryListStateChangeEvent: (isFavorite, code) {
final List<Country> newCountryList = state.countryList.map((it) {
if (it.code == code) {
return it.copyWith(code: code, isFavorite: isFavorite);
}
return it;
}).toList(growable: false);
emit(state.copyWith(countryList: newCountryList));
},
countryListInitializeEvent: () async {
final codeList = await repository.getAllFavoriteCountries();
final unwrappedList =
codeList.whereType<String>().toList(growable: false);
final newState = CountryListState.initializeState(unwrappedList);
emit(newState);
},
);
}, transformer: concurrent());
}
}
In this project, I use Isar to store the permanent data in local strage.
But in the middle of developing, You should have used Bloc Hidrate๐ .
Isar is also resource layer so I implemented it as repository.
At first, I made IsarRepository
that is abstract class to share the initialze login of all Isar repositories.
But you can provide better solutions that the following soulution. (Use mixin etc...)
abstract class IsarRepository {
Isar? isar;
Future<void> initializeIsarInstance(
{required CollectionSchema schema}) async {
final dir = await getApplicationCacheDirectory();
isar = await Isar.open(
[schema],
directory: dir.path,
);
}
}
Inherit it
import 'package:isar/isar.dart';
import 'package:rate_converter_flutter/isar/isar_favorite_country.dart';
import 'package:rate_converter_flutter/resources/isar_repository.dart';
import '../constant/country_code_constant.dart';
class FavoriteCountryIsarRepository extends IsarRepository {
FavoriteCountryIsarRepository() : super() {
print('instantiate');
}
Future<List<String?>> getAllFavoriteCountries() async {
final result = await isar?.collection<FavoriteCountry>().where().findAll();
if (result == null) {
return [];
}
return result.map((it) => it.favoriteCountry).toList(growable: false);
}
Future<void> add(CountryCode code) async {
final favoriteCountry = FavoriteCountry()
..favoriteCountry = code.codeString;
await isar?.writeTxn(() async {
await isar?.collection<FavoriteCountry>().put(favoriteCountry);
print('isar: add ${code.codeString}');
});
}
Future<void> delete(CountryCode code) async {
await isar?.writeTxn(() async {
await isar
?.collection<FavoriteCountry>()
.filter()
.favoriteCountryEqualTo(code.codeString)
.deleteAll();
print('isar: delete ${code.codeString}');
});
}
}
Unit test is very import part of the programming. Especially in Flutter bloc, I'll explain how you make unit test and mocked repositories.
First, you can easily make mocked blocs by bloc_test package. The following example shows how you make mocked bloc objects.
class MockAdWatchBloc extends MockBloc<AdWatchEvent, AdWatchState> implements AdWatchBloc {}
You only have to do two things to make mocked ones.
- Inherit
MockedBloc
Type - Implement bloc type you want to mock.
If you have done making mocked bloc. Let's get started to do testing!
Some kinds of blocs I have already created are shown. And you can make stubed stream
and implement the behaivior that the bloc's state wants to be. You can check whether
the bloc has the correct state or not by expect()
function.
group('whenListen', () {
test("Ad Watch Bloc Test", () async {
// Create Mock Instance
final adWatchBloc = MockAdWatchBloc();
// Stub the listen with a fake Stream
whenListen(
adWatchBloc,
Stream.fromIterable([
// Initial State
const AdWatchState(isWatchRate: false, isWatchShowMore: false),
const AdWatchState(isWatchRate: true, isWatchShowMore: false),
const AdWatchState(isWatchRate: false, isWatchShowMore: true),
]),
initialState:
const AdWatchState(isWatchRate: false, isWatchShowMore: false));
expect(adWatchBloc.state,
const AdWatchState(isWatchRate: false, isWatchShowMore: false));
// Expect that the
await expectLater(
adWatchBloc.stream,
emitsInOrder(<AdWatchState>[
const AdWatchState(isWatchRate: false, isWatchShowMore: false),
const AdWatchState(isWatchRate: true, isWatchShowMore: false),
const AdWatchState(isWatchRate: false, isWatchShowMore: true),
]));
expect(adWatchBloc.state,
const AdWatchState(isWatchRate: false, isWatchShowMore: true));
});
});
You also do test by blocTest<T, U>()
. This can do test as getting event behaivior.
Then you may come across how to inject reposities in bloc. The packaget I recommend is
mockito
. Mockito is the package to mock the module that make connection to outbound.
In my case, I will mock repositories by using this package.
test/unit_test/unit_test.dart
@GenerateNiceMocks([MockSpec<FavoriteCountryIsarRepository>()])
void mainBloc() {
///
}
Once you make annotation on your test function, you have to run dart pub run build_runner
.
This commaned generates mocking repositories. In my case, it creates MockFavoriteCountryIsarRepository
And just use it as the following code.
blocTest('Country List Bloc Test',
setUp: () {
mockedFavoriteCountryIsarRepository =
MockFavoriteCountryIsarRepository();
when(mockedFavoriteCountryIsarRepository.getAllFavoriteCountries())
.thenAnswer((_) => Future<List<String?>>.value([]));
},
build: () => CountryListBloc(mockedFavoriteCountryIsarRepository),
act: (countryListBloc) => countryListBloc.add(
const CountryListStateChangeEvent.countryListInitializeEvent()),
expect: () => <CountryListState>[CountryListState.initializeState([])]);
If you have any ideas, let me know ๐
There some tips I want to tell you.