Pine: A lightweight architecture helper for your Flutter Projects

Angelo Cassano
7 min readSep 4, 2022

--

The Pine logo

When we speak about the architecture of a Flutter project, every single component resides in the widget tree. It could be a visible widget or something less tangible that has to do with the business logic like a repository or a service, but at the end of the day, everything is there.

Since the beginning, Dependency Injection has been the common design pattern used in Flutter to inject elements into the widget tree. Flutter’s early adopters will easily remember about InheritedWidgets, a particular type of widget mainly used to inject non-tangible elements into the widget tree. InheritedWidget is really easy to understand and pretty straightforward when it comes to its use. The main downside is the boilerplate code around its definition.

The revolution started with Provider, a library that simplified the injection of different types of elements into the widget tree. Provider has been widely adopted by the Flutter community and works pretty well in combination with ChangeNotifier. Provider aims to remove the boilerplate code from the definition of an InheritedWidget. In fact, in terms of the number of lines of code, injecting an element in the widget tree with Provider takes only a single line:

Provider<Service>(create: (context) => Service());

Even the most used state management system decided to rely on Provider to inject its component in the widget tree: we are talking about BLoC.

flutter_bloc logo

The flutter_bloc library created by Felix Angelov is using a custom provider to inject the blocs into the widget tree.

BlocProvider<Bloc>(create: (context) => Bloc());

The problem

Flutter is a relatively easy framework that gives everyone the ability to create applications. Most people out there can to create a product that works, but I strongly believe that as developers we should focus more on how things work.

Software engineering comes in help: if you ever attended a lesson in computer science at the University, one of the first things that will teach you is about software architectures.

When it comes to Flutter architecture, I love to apply a customized version of the Flutter Clean Code Architecture. Generally speaking, my projects are organized into four different layers as follows:

UI Layer

The UI layer is composed of screens, pages, and widgets. Here you can create a combination of widgets that reside in Stateful and Stateless widgets. Those widgets typically interact with the second layer which is the state manager, in my case I like to use BLoC.

Business Logic Component Layer

The business logic component (or BLoC) layer, is the layer that is typically a middleware and aims to translate the user interaction with the UI into a set of instructions for the repository layer.

Repository Layer

The repository layer is an abstraction layer, a single source/point of truth, that queries the lower level, the service layer, translates this information into app-readable data using a set of mappers to convert DTOs into models, and delivers back this data, typically to the upper layers like a BLoC.

Service Layer

The service layer is the bottom layer in this kind of architecture. Here it’s easy to find different services like a REST Client that queries a REST API to get data from a server, or access to a database as a DAO.

The solution

During these years I used a combination of MultiProvider, MultiRepositoryProvider, and MultiBlocProvider to inject items into the widget tree. But as soon as your project starts growing and your app becomes more complex, it’s easier to have tons of injections in your app widget.

To avoid an always-growing app widget, I decided to split these providers into different files referenced by a single widget that I decided to name DependencyInjector.

class DependencyInjector extends StatelessWidget {
final Widget child;

const DependencyInjector({
Key? key,
required this.child,
}) : super(key: key);

@override
Widget build(BuildContext context) => _Providers(
child: _Repositories(
child: _Blocs(
child: child,
),
),
);
}

Each nested element contained in the DependencyInjector widget is also a private Widget that wraps a MultiProvider, MultiRepositoryProvider, and MultiBlocProvider according to its reference layer.

This implementation sooner became a blocker when I wanted to start testing a single component coming from a particular layer. That’s why I decided to rewrite the dependency injector widget to simplify the process of injecting components into the widget tree.

Since all these widgets were continuously copied to newer projects, I decided to create a library for myself and for everyone who wants to simplify the injection process, and whose projects rely on Provider and BLoC as a state manager.

Pine

To add this library to your Flutter project, simply type this instruction in your command line:

flutter pub add pine

Once you’ve done this, you can start injecting your elements into the widget tree with Pine.

The Architecture

Elements are injected from top to bottom.

  1. The first elements added in the widget tree are mappers, particularly useful to convert data coming from data layers to something that should be used in the presentation layer.
  2. The second elements are providers: here you can inject services that manipulate data or access it like REST clients or DAOs interfaces.
  3. The third layer is used to inject the repositories that access the data layer using an abstraction layer.
  4. The last layer is used to inject the logic: Pine relies on BLoC as a state manager, that’s why we’ll inject global scoped BLoCs.
The Pine architecture

Each element might rely on the top level ones and are generally accessed from the bottom level ones: for instance, a repository may need access to a REST client service to gather data, save it into a database, and return it to a BLoC. To access top-level items, you can use the read and watch functions exposed by Provider.

The Interactions

The Pine interaction

How to use it

A pine architecture can be achieved by using the DependencyInjectorHelper widget, which helps you to inject different types of elements into the widget tree. If you are working on a simple project, you should use the DependencyInjectorHelper straight into your main app widget.

class App extends StatelessWidget {
const App({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) => DependencyInjectorHelper(
blocs: [
BlocProvider<Bloc>(
create: (context) => Bloc(
repository: context.read(),
)..action(),
),
],
mappers: [
Provider<DTOMapper<DTO, Model>>(
create: (_) => Mapper(),
),
],
providers: [
Provider<Service>(
create: (context) => Service(),
),
],
repositories: [
RepositoryProvider<Repository>(
create: (context) => RepositoryImpl(
service: context.read(),
mapper: context.read(),
),
),
],
child: MaterialApp(
title: 'App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const HomePage(),
),
);
}

As the project grows, it’s better to create a new widget that wraps all of these items in different files. We can name this widget DependencyInjector.

dependency_injector.dart

part 'blocs.dart';
part 'mappers.dart';
part 'providers.dart';
part 'repositories.dart';

class DependencyInjector extends StatelessWidget {
final Widget child;

const DependencyInjector({
Key? key,
required this.child,
}) : super(key: key);

@override
Widget build(BuildContext context) => DependencyInjectorHelper(
blocs: _blocs,
providers: _providers,
mappers: _mappers,
repositories: _repositories,
child: child,
);
}

In this widget, we need to define all the dependencies that are required in our project. I prefer splitting these elements into different files according to their type. In our example, we will create four different files because we inject blocs, mappers, providers, and repositories.

blocs.dart

part of 'dependency_injector.dart';

final List<BlocProvider> _blocs = [
BlocProvider<Bloc>(
create: (context) => Bloc(
repository: context.read(),
)..action(),
),
];

mappers.dart

part of 'dependency_injector.dart';

final List<SingleChildWidget> _mappers = [
Provider<DTOMapper<DTO, Model>>(
create: (_) => Mapper(),
),
];

providers.dart

part of 'dependency_injector.dart';

final List<SingleChildWidget> _providers = [
Provider<Service>(
create: (context) => Service(),
),
];

repositories.dart

part of 'dependency_injector.dart';

final List<RepositoryProvider> _repositories = [
RepositoryProvider<Repository>(
create: (context) => RepositoryImpl(
service: context.read(),
mapper: context.read(),
),
),
];

Once we finished defining the global dependencies to inject into the widget tree, we need to wrap our MaterialApp/CupertinoApp with the DependencyInjector widget as follows:

class App extends StatelessWidget {
const App({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) => DependencyInjector(
child: MaterialApp(
title: 'App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const HomePage(),
),
);
}

Testing

With the DependencyInjectorHelper it’s easy to inject dependencies into the widget tree. Simply wrap the widget you need to test with the DependencyInjectorHelper class and inject the dependencies you need.

In the following example, we will test the HomePage widget which relies on a Bloc. Before pumping the MaterialApp containing the HomePage, we will wrap it as follows:

await tester.pumpWidget(
DependencyInjectorHelper(
blocs: [
BlocProvider<Bloc>.value(value: bloc),
],
child: const MaterialApp(
home: HomePage(),
),
),
);

Of course, since we are testing the HomePage, we are injecting a mocked bloc.

Links and Examples

The Pine library is available under pub.dev and on GitHub. Please check the example project News App to better understand how to model a Pine based Flutter Architecture.

Thank you for reading 👋

I hope you enjoyed this article. If you have any queries or suggestions please let me know in the comments down below.

--

--