Development+of+a+Large-Scale+Flutter+App
Development+of+a+Large-Scale+Flutter+App
Development+of+a+Large-Scale+Flutter+App
Flutter App
Abstract
The sustained growth of the Flutter framework since its first stable release has drawn the
attention of companies from all sectors and sizes, enabling them to build cross-platform
applications from a single codebase. This thesis aims at providing tangible insights derived
from the development of large-scale Flutter applications. In particular, its purpose is to
analyze essential architectural choices, patterns, and opinionated best practices affecting
its maintainability, testability, and scalability.
This thesis work introduces a research work about the relevant software techniques, con-
cepts, background, and decisions applied within the scope of the empirical part of the
thesis. Said research includes, but is not limited to, knowledge of Cross-platform App
Development, Application State Management, Software Architectural Patterns, Software
Modularization, and Software Testing.
Furthermore, this thesis work leverages a large-scale Flutter project developed in a dis-
tributed software environment for a world-leading company in the domotics and home
automation industry. Thus, readers will find detailed descriptions of the choices taken
throughout its development, discussions of production-ready code samples, contributions
to the existing Flutter literature from an empirical perspective, and carefully argued so-
lutions to non-trivial problems.
Ultimately, this document presents a thorough study and conclusions focused on a specific
project. However, its contents are sufficiently general and valuable for other developers
to incorporate its applicability into their own by extrapolating the gathered information
and adjusting it to their particular requirements.
Questo lavoro di tesi introduce un lavoro di ricerca sulle tecniche software rilevanti, i
concetti, il background e le decisioni applicate nell’ambito della parte empirica della tesi.
Tale ricerca include, a titolo esemplificativo, la conoscenza dello sviluppo di app mul-
tipiattaforma, della gestione dello stato delle applicazioni, dei modelli architettonici del
software, della modularizzazione del software e del test del software.
Inoltre, questo lavoro di tesi sfrutta un progetto Flutter su larga scala sviluppato in
un ambiente software distribuito per un’azienda leader a livello mondiale nel settore della
domotica. Pertanto, i lettori troveranno descrizioni dettagliate delle scelte effettuate du-
rante il suo sviluppo, discussioni su campioni di codice pronti per la produzione, contributi
alla letteratura Flutter esistente da una prospettiva empirica e soluzioni attentamente ar-
gomentate a problemi non banali.
Contents
Abstract i
Contents v
Introduction 1
0.1 Native vs. Cross-Platform . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
0.2 Flutter & App Architecture . . . . . . . . . . . . . . . . . . . . . . . . . . 3
0.3 Document structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.5 Motivations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2 Implementation 35
2.1 Context . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.1.1 Technical aspects . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.1.2 Development Process . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2.2 Hybrid Architecture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.2.1 Layered . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.2.2 Feature-Oriented . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.2.3 Packaging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
2.3 State Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
2.3.1 Selection Criteria . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
2.3.2 Code Samples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
2.4 Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
2.4.1 Preamble & Considerations . . . . . . . . . . . . . . . . . . . . . . 65
2.4.2 Package Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
2.4.3 Bloc Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
2.4.4 Widget Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
2.4.5 Remarks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
3 Results 83
3.1 Quantitative Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
3.2 Qualitative Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
4 Related Work 91
5 Conclusions 93
Bibliography 95
Introduction
The software application (App) industry has undergone a notable transformation in
software development technologies in the last decade. Companies from all sectors and
sizes have shifted from native-platform programming languages to their modern, cross-
platform, typically-open-sourced counterparts to develop their suite of software products.
This transformation responds to the rising need to address the problems of maintaining
different native codebases. However, embracing youthful cross-platform technologies also
comes at a cost, requiring further evaluation when considering this trade-off.
Cross-platform development refers to the approach that does not consider a concrete
software implementation but rather a general solution to running an App on several
platforms from a single codebase [16]. These versatile and flexible characteristics have
compelled top companies to develop their suite of software products or a part of it using
cross-platform technologies [70] [32]. Furthermore, these technologies’ open-sourced na-
ture poses yet another significant factor in their adoption, leading to relevant academic re-
search on the evolution of mobile software [35]. Figure 1 below illustrates the taxonomy of
the main cross-platform approaches. Among the most popular and widely used tools and
2 | Introduction
frameworks derived from this categorization, we find Angular1 , React2 , and Vue.js3 (Pro-
gressive Web Apps), Cordova4 (Hybrid App), React Native5 , NativeScript6 , Xamarin7 ,
and Qt8 (Self-contained runtime), DSLs9 , and UML10 (MDSD), and lastly, XMLVM11 ,
J2ObjC12 , and Flutter13 (Transpiling). Moreover, Flutter, React Native, Xamaring, Qt,
and Cordova represent about 80% of the most used frameworks among developers world-
wide [45], with Flutter and React alone representing roughly 80% of the cross-platform
mobile framework market share [44].
Nonetheless, some of these solutions may suffer from limited functionality and perfor-
mance issues compared to native development approaches. An all-too-common topic for
discussion is the inferior performance exhibited by Apps developed with cross-platform
technologies compared to natively-developed Apps. However, some studies demonstrate
that, in particular cases, using frameworks that generate native apps might yield code that
outperforms hand-written code due to optimization; interpreted apps could undergo run-
time optimization that leads to better performance than apps optimized at compile-time
[3].
1
https://angular.io/
2
https://reactjs.org/
3
https://vuejs.org/
4
https://cordova.apache.org/
5
https://reactnative.dev/
6
https://nativescript.org/
7
https://dotnet.microsoft.com/en-us/apps/xamarin
8
https://www.qt.io/
9
https://en.wikipedia.org/wiki/Domain-specific_language
10
https://www.uml.org/
11
http://www.xmlvm.org/overview/
12
https://developers.google.com/j2objc
13
https://flutter.dev/
| Introduction 3
Flutter code compiles to ARM or Intel machine code and JavaScript for fast performance
on any device. Additionally, it supports Hot Reload [21] for enhanced productivity and
allows developers to control every pixel to create customized, adaptive designs.
However, due to the young nature of this framework, there is no official consensus on how
to architect Flutter Apps [64] [59]. Software architecture for mobile applications mainly
focuses on the decisions and patterns applied to handle the app state and code modu-
larization, which affects the app’s testability support. Regarding state management, the
Flutter team provides developers with numerous approaches [22] but does not enforce their
usage or suggest which option is more fitting for a given scenario. As far as code mod-
ularization, Dart offers powerful capabilities to break up the codebase into collections of
well-organized, independent, and reusable units called packages [23]. Nevertheless, there
is no formal recommendation on which software architecture facilitates better compart-
mentalization means or a precise procedure to organize these packages. Lastly, despite
the phenomenal tooling and resources available for testing Flutter applications [24], there
exists a lack of best practices enabling a systematic methodology for thoroughly testing
a Flutter App.
These factors motivated our decision to contribute to the current growing Flutter lit-
erature by proposing a solution that serves as a guide to navigating these ambiguous
decisions when working on large-scale Flutter Apps. This proposal introduces a hybrid
architecture, which leverages the strengths of layered and featured-oriented architectures.
It implements the BLoC architectural pattern to manage the App’s state. It provides
precise modularization criteria to arrange the App’s codebase into more manageable code
units. Lastly, it offers valuable insights on the systematic testing techniques applied to
obtain one hundred percent code coverage through unit and widget testing. Moreover, the
analysis and development of this proposal took place within the context of a distributed
software environment at a world-leading company in the domotics and home automation
industry working on a cross-platform mobile App. Ultimately, the results of this analysis
and implementation met the stakeholders’ common high expectations and demands for
the quality outcome of the project, who were professionals with different backgrounds and
interests (software engineers, QA specialists, project managers, product owners, and beta
testers).
| Introduction 5
• Chapter 3: Results - This chapter navigates the reader through the results de-
rived from the implementation endeavors presented in the previous chapter, provid-
ing quantitative and qualitative data supporting the objectives of this thesis.
• Chapter 4: Related Work - This chapter wraps up the work presented in this
thesis by providing the reader with additional insights into other authors’ academic
endeavors related to the knowledge presented in this document. It reviews a series
of research and thesis papers focused on software architecture, state management
solutions, and testability in Flutter applications.
1.1. Flutter
Flutter [25] is Google’s portable framework for crafting cross-platform, natively compiled
applications from a single codebase. Furthermore, it is open-sourced and, hence, free to
use. Flutter consists of two essential parts [72]:
"A powerful, general-purpose, open UI toolkit for building stunning experiences on any
device, embedded, mobile, desktop, or beyond."
— Tim Sneath, Announcing Flutter 1.0 Keynote
Developers building Flutter Apps use the Dart programming language, further analyzed
in subsection A.2. Google created this typed, object programming language that focuses
on front-end development and released it in October 2021. Overall, Flutter combines ease
8 1| Background and Motivations
"The goal is to enable developers to deliver high-performance apps that feel natural on
different platforms, embracing differences where they exist while sharing as much code
as possible."
— Flutter Team, Flutter architectural overview [26]
Layers
An extensible and layered system underpins Flutter’s design. These layers behave as
independent libraries and mount one on top of the other, creating a bottom-up structure
where each layer relies on the layer right below. It is worth noting that this architecture
does not allow for privileged access from one layer to the one below and treats each
framework level as optional and replaceable pieces.
Moving up the layered structure, we find the Flutter engine placed at the core of this
architecture. Written in C++, this engine supports the necessary primitives to build all
Flutter applications and is responsible for frame rendering. Skia1 , an open-source 2D
graphics library that provides APIs that work across a variety of hardware and software
platforms, powers the graphics capabilities of this engine. Furthermore, the low-level im-
plementation of Flutter’s core API includes text layout, file and network I/O, accessibility
support, plugin architecture, and a Dart runtime and compile toolchain.
1
https://skia.org/
1| Background and Motivations 9
Lastly, we find the Flutter framework at the top of this architecture, which is essentially
the layer developers interact with the most. The following subsections contain further
descriptions about the most relevant sub-layers composing this modern, reactive, and
Dart-written framework.
It is worth noting that the Flutter framework is relatively lightweight and small, leaving
the implementation of many higher-level features to developers through the use of packages
- further explanations included in section 1.2.
Flutter uses immutable classes to configure a tree of objects, also known as widgets.
These widgets allow for the management of layout and compositing separately through
two different object trees where the former manages the latter.
"Flutter is, at its core, a series of mechanisms for efficiently walking the modified parts
of trees, converting trees of objects into lower-level trees of objects, and propagating
changes across these trees."
— Flutter Team, Flutter architectural overview [26]
Widgets override the build() method to declare their user interface. This function essen-
tially converts state to UI: UI = f(state).
Thanks to the fast object instantiation and deletion provided by Dart [63], the Flutter
framework calls this method as many times as needed without negatively impacting the
app’s performance.
Widgets
As mentioned before, a widget is an immutable declaration part and the building block
of a Flutter App’s UI. The combination of small, single-purpose widgets is a powerful
composition technique emphasized by Flutter. Furthermore, this composition creates a
hierarchy of nested widgets where the root is typically a MaterialApp or a CupertinoApp.
The code snippet below shows a simple example where all the instantiated classes are
widgets forming this described structure.
8 @override
9 Widget build(BuildContext context) {
1| Background and Motivations 11
10 return MaterialApp(
11 home: Scaffold(
12 appBar: AppBar(
13 title: const Text('My Home Page'),
14 ),
15 body: Center(
16 child: Builder(
17 builder: (BuildContext context) {
18 return Column(
19 children: [
20 const Text('Hello World'),
21 const SizedBox(height: 20),
22 ElevatedButton(
23 onPressed: () {
24 print('Click!');
25 },
26 child: const Text('A button'),
27 ),
28 ],
29 );
30 },
31 ),
32 ),
33 ),
34 );
35 }
36 }
Moreover, events prompt the framework to update the App’s UI. This update derives
from an efficient comparison between the old and new widgets, which updates the widget
hierarchy. Most importantly, Flutter features unique widget implementations, rather than
deferring to those provided by the system, leading to substantial benefits:
• Provides unlimited extensibility
• Avoids a significant performance bottleneck
• Decouples the application behavior from any operating system dependencies
Recall that overriding the build()2 method determines a widget’s visual representation.
This function must be free of side effects and return a new tree of widgets. Then, the
framework calls determined build methods based on the render object tree. Notice that
2
https://api.flutter.dev/flutter/widgets/StatelessWidget/build.html
12 1| Background and Motivations
this building process relies on returning quickly from the method and the asynchronous
execution of the computational work. Ultimately, Flutter rerenders only the widgets
whose state has changed by calling their build method thanks to this effective automated
comparison, enabling high-performance, interactive apps.
3
https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html
4
https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html
5
https://api.flutter.dev/flutter/widgets/State-class.html
6
https://api.flutter.dev/flutter/widgets/State/setState.html
7
https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html
1| Background and Motivations 13
Rendering
To better understand Flutter’s rendering pipeline shown in Figure 1.3 below, we introduce
a high-level comparison to other common approaches [43] [62] [31].
Figure 1.4 below depicts the native approach to building mobile Apps where developers
must use a platform-specific programming language. Thereby, Apps communicate with
the platform to create widgets or access other services, like Bluetooth. The canvas then
renders these widgets, which in turn receive the events. The main limitation of this
approach is that, despite this straightforward and efficient architecture, developers must
create different applications to target distinct platforms since widgets and native languages
differ considerably.
8
https://api.flutter.dev/flutter/widgets/Navigator-class.html
9
https://api.flutter.dev/flutter/widgets/MediaQuery-class.html
14 1| Background and Motivations
Figure 1.5 displays the WebViews approach, an early effort at enabling cross-platform de-
velopment. Apps based on this approach render a bundle of HTML, CSS, and JavaScript
onto a Web View much as it occurs on a mobile browser. This approach’s main drawbacks
are the web technology stack’s limitations and the need to use a bridge to communicate
with the platform’s services. The latter consumes resources and time, worsening the
application’s performance.
Reactive Views, shown in Figure 1.8, also known as Interpreted, attempt to create cross-
platform solutions leveraging JavaScript. Frameworks like ReactJS and React Native use
this approach where the creation of web views relies on reactive programming patterns [54].
This approach’s main limitation is, once again, the need to use a bridge to communicate
with the platform, negatively impacting the application’s performance.
1| Background and Motivations 15
The Flutter team emphasizes that broad definitions for the state of an application, though
valid, are not suitable when architecting an App as developers do not manage everything
that fits these definitions [28]. Therefore, they focus on the state that developers do
manage and make the following differentiation:
• Ephemeral state, also known as UI or Local state, refers to the state efficiently
contained within a widget. It does not perform complex changes, does not require
serialization, and other parts of the widget tree hardly ever need to access it.
• App state, or shared state, is not ephemeral. There is a need to share this state
across different parts of the App and maintain it between user sessions. Managing
this state is more involved and typically requires a state management solution to
deal with its complexity within the context of an application’s nature.
Nonetheless, the Flutter team considers "there is no clear-cut rule" to distinguishing be-
tween ephemeral and App states. Indeed, they do not even enforce a systematic approach
to choosing a state management solution for a large-scale application. Thus, they provide
1| Background and Motivations 17
a list of state management approaches [22], though they leave it up to developers which
option to choose. Therefore, we picked the BLoC pattern to manage the state of the
Flutter application evaluated and discussed in this document. We based the outcome of
our decision on a thorough analysis of the BLoC pattern, and the bloc library packages
[4]. Moreover, we also considered previous research studies that examined the most rele-
vant state management solutions for Flutter applications [64] [59] [31] [39]. These studies
determined that the BLoC pattern made Flutter Apps more scalable and testable, making
it the most suitable solution for our project needs.
BLoC Pattern
Paolo Soares introduced BLoC in 2018, a state management solution based on a clear
architectural pattern [61]. He aimed at sharing as much code as possible for an application
implemented with two different Dart-based frameworks by reusing the code in charge of
the business logic. Hence, he placed all the business logic into independent Business
Logic Components (BLoCs) away from the framework-specific UI components. Lastly,
he presented the following strict rules for BLoCs:
1. Inputs and outputs are simple Streams/Sinks only
2. Dependencies must be injectable and platform agnostic
3. No platform branching allowed
4. Implementation can be whatever you want if you follow the previous rules
5. Though, he suggests reactive programming
As for the UI design guidelines:
1. Each "complex enough" component has a corresponding BLoC
2. Components should send inputs "as is"
3. Components should show outputs as close as possible to "as is"
4. All branching should be based on simple BLoC boolean outputs
"Design rules are not negotiable, and that’s for the sanity of everyone."
— Paolo Soares, BLoC presentation at DartConf 2018 [61]
18 1| Background and Motivations
By following this pattern and its rules, an App will feature the following architectural
advantages [31]:
1. Dedicated business logic components
2. Simple, logic-agnostic UI components
3. Testable business logic
4. BLoCs control widget rebuilding
5. Predictable state changes which must come from a single place
bloc library
Felix Angelov released the bloc package’s first version in October 2018 [5]. This package
is the centerpiece of a library of seven other packages whose focus is to facilitate the
implementation of the BLoC design pattern in a simple, lightweight, and highly-testable
manner. Thus, we introduce the reader to the subset of packages implemented in the
Flutter App discussed in section 2.3, where we will further demonstrate and analyze their
implementation and use-cases.
"A predictable state management library that helps implement the BLoC design
pattern."
— Felix Angelov, bloc library [4]
The bloc package represents a high-level abstraction of the BLoC pattern. It enables
developers to focus on writing the business logic rather than on the complex reactive
aspects and boilerplate required to implement this pattern from scratch. Thereby, this
package’s public API exposes two classes to implement the bloc pattern, Cubit10 and
Bloc11 , both extending the base class BlocBase12 . Cubits and Blocs are reasonably similar,
though the former relies on methods to emit new states, while the latter leverages Streams
and Events as inputs to output a Stream of States.
10
https://pub.dev/documentation/bloc/latest/bloc/Cubit-class.html
11
https://pub.dev/documentation/bloc/latest/bloc/Bloc-class.html
12
https://pub.dev/documentation/bloc/latest/bloc/BlocBase-class.html
1| Background and Motivations 19
Moreover, the flutter_bloc package [6], built to work with the previously reviewed
bloc package, provides developers with a collection of Flutter Widgets that facilitate the
implementation of the BLoC design pattern. Among its most relevant Widgets, we find
• BlocBuilder13 , which handles building a widget in response to new states
• BlocSelector14 , which allows developers to filter updates by selecting a new value
based on the bloc state and prevents unnecessary builds if the selected value does
not change
• BlocProvider15 , which is responsible for creating the Bloc or Cubit and the child
having access to either instance via BlocProvider.of(context)
• MultiBlocProvider16 , which merges multiple BlocProvider widgets into one widget
13
https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocBuilder-class.
html
14
https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocSelector-class.
html
15
https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocProvider-class.
html
https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/
16
MultiBlocProvider-class.html
1| Background and Motivations 21
17
https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocListener-class.
html
18
https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/
MultiBlocListener-class.html
19
https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocConsumer-class.
html
20
https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/
RepositoryProvider-class.html
21
https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/
MultiRepositoryProvider-class.html
22
https://pub.dev/packages/hive
23
https://pub.dev/documentation/hydrated_bloc/latest/hydrated_bloc/
HydratedStorage-class.html
22 1| Background and Motivations
1.2. Dart
This section presents the reader with an overview of Dart and its main characteristics and
powerful features.
1.2.1. Overview
Dart [65] forms Flutter’s foundation by providing the language and runtimes to power this
framework’s Apps. Its technical envelope’s24 design is particularly well-suited for client de-
velopment, prioritizing both development and high-quality production experiences across
a wide variety of compilation targets (web, mobile, desktop, and embedded).
Moreover, Dart uses static type checking to ensure that a variable’s value always matches
the variable’s static type, making it type-safe. It also supports dynamic types combined
with runtime checks, offering further flexibility to the language’s typing system. Overall,
Dart is a client-optimized language for developing fast apps on any platform. Thereby,
Dart allows code compiling into these different platforms:
• Native - For applications targeting mobile and desktop devices, Dart includes both
a Dart VM with just-in-time (JIT) compilation and an ahead-of-time (AOT) com-
piler for producing machine code
• Web - For apps targeting the web, Dart includes a development time compiler
(dartdevc) and a production time compiler (dart2js). Both compilers translate Dart
into JavaScript.
24
Choices made during development that shape the capabilities and strengths of a language
1| Background and Motivations 23
Future
A future is the result of an asynchronous computation, which is a computation that cannot
return an immediate result after being executed [66]. Thereby, this result may have two
states, uncompleted or completed. The former refers to a future waiting for a function’s
asynchronous operation to finish or throw an error, while the latter refers to a successful
or failed completed computation.
Stream
A stream is a sequence of asynchronous events, distinguished as data or error events [67].
To simplify this concept, Didier Boelens [13] considers a pipe with two ends, where data
25
https://api.dart.dev/stable/2.16.1/dart-async/Future-class.html
26
https://api.dart.dev/stable/2.16.1/dart-async/Stream-class.html
24 1| Background and Motivations
and events always flow in the same direction from one end to the other, never vice versa.
"The pipe is called a Stream. To control the Stream, we typically use a StreamController.
To insert something into the Stream, the StreamController exposes the entrance, called
a StreamSink, accessible via the sink property. The Stream’s way out is exposed by the
StreamController27 via the stream property."
Notice Streams may convey any type of data, such as a value, an event, an object, a
collection, a map, an error, or even another Stream. Moreover, there are two types of
Streams:
• Single-subscription - Only allows a single listener throughout the Stream’s life-
time. Hence, it is impossible to listen twice on such a Stream, even after canceling
the first subscription.
• Broadcast - Allows any number of listeners. It is possible to add new listeners to
a Broadcast Stream at any given point, having new listeners receive the events as
soon as they start listening to the Stream.
1.2.4. Packages
Understanding the parts that compose more complex and larger entities enable software
engineers to architect applications leveraging functional and behavioral modularity princi-
ples. Functional modularity refers to the composition of smaller independent components
with clear boundaries and functions. On the other hand, behavioral modularity addresses
traits and attributes that can evolve independently. Thus, complex software applica-
tions may be broken into functional parts called modules, which can be created, changed,
tested, used, and replaced separately. Software modularity brings the following benefits
to software systems [2]:
• More lightweight modules with less code
• Introduction of new features or changes to modules in isolation, separate from the
other modules
• Easy identification and fixing of module-specific errors
• Modules can be built and tested independently
• Enhanced collaboration for developers working on different modules for the same
application
• Reusability of modules across various applications
• Modules kept in a versioning system can be easily maintained and tested
• Module fixes and noninfrastructural changes do not affect other modules
Thereby, Dart favors modularity and provides an ecosystem based on packages to manage
27
https://api.dart.dev/stable/2.16.1/dart-async/StreamController-class.html
1| Background and Motivations 25
shared software such as libraries and tools [68]. At a minimum, a Dart package is a
directory containing a pubspec28 file, a yaml29 -based file containing metadata about the
package. Notice packages may contain dependencies30 , libraries, applications, resources,
tests, images, and examples. Moreover, the concept of separation of concerns between
objects in object-oriented programming (OOP) resembles a Dart library, which exposes
functionality as a set of interfaces and hides the implementation from the rest of the world.
Libraries allow for a better application structure, tight coupling minimization, and more
maintainable code. Ultimately, a Dart application is a library, as well.
1.3.1. Overview
Shlaer and Mellor might have been the first to use the expression software architecture in
academic literature [58]. Software architecture involves a series of decisions based on many
factors in a wide range of software development [30], representing the highest abstraction
level [14] at which we construct and design software systems. Moreover, it enhances the
traceability between requirements and technical solutions, reducing risks associated with
building the technical solution and facilitating the satisfaction to systemic qualities. In
other words, the software architecture sets the boundaries for the quality levels resulting
systems can achieve and represents an early opportunity to design for software quality
requirements such as reusability, performance, safety, and reliability [15].
"To design the software architecture to meet the quality requirements is to reduce the
risks of not achieving the required quality levels."
— PerOlof Bengtsson, Software Architecture - Design and Evaluation [15]
M. Kassab et al. provided an empirical study where they conducted a comprehensive
28
https://dart.dev/tools/pub/pubspec
29
https://yaml.org/
30
https://dart.dev/tools/pub/glossary#dependency
26 1| Background and Motivations
survey to document the practices used by software professionals when selecting and in-
corporating architectural patterns for their projects in the industry [30]. Based on the
survey results and the quality requirement criteria that led to the most used approaches,
we considered that a combination of flexible yet powerful architectural patterns would
ensure and improve a given software application’s maintainability, testability, and scala-
bility. Thus, we decided to employ a hybrid architecture combining layered and feature
modularization to implement our Flutter App, among other patterns further discussed in
section 2.2.
"The architecture of a software system is almost never limited to a single architectural
pattern."
— Microsoft, Microsoft Application Architecture Guide: Patterns & Practices [1]
1.3.2. Layered
The layered software architecture pattern, also known as the n-tier architecture style or
the multi-layered architecture style, is arguably the most commonly used architectural
pattern in software engineering. In essence, the layered architecture’s goal is to organize
the components of an application into horizontal logical layers and physical tiers [71].
Layers are role-and-responsibility separated logical units within an application which man-
age their specific software dependencies. Higher layers may exploit services from a lower
layer, but never the other way around. Moreover, tiers are physical units whose funda-
mental purpose is to run code, like a web server or a database. Notice layers may be
hosted in dedicated and exclusive tiers, though this is not required. More importantly,
the physical separation of tiers enables better scalability, maintainability, and resiliency.
However, it may increase latency due to network communication overhead.
A traditional layered software architecture features three tiers and four layers. Notice
that introducing multiple layers for different software components enhances the separa-
tion of concerns, and hence, it improves the components’ simplicity, maintainability, and
testability.
Regarding the four common layers,
• Presentation - This layer contains the user-exposed UIs.
• Business Logic - This layer handles all business logic, validations, and business
processes.
• Data Access - Also known as the persistence layer, this layer is responsible for
interacting with a database.
• Data Store - This layer is the actual data store for the application.
As for the three tiers,
1| Background and Motivations 27
• Presentation - This tier hosts the front-end codebase. It is the application’s top-
most level, and it enables user interaction.
• Application - This tier hosts the business logic layer and data access layer. It is
also known as the middle tier.
• Data - This tier hosts the data store layer. Databases, file systems, blob storage,
or document databases are examples of resources found in a data store.
1.3.3. Feature-oriented
The number of notions and interpretations of a feature is as broad and abstract as defi-
nitions may have. We propose a broad and fundamental-concept-encompassing definition
combining the descriptions provided by Kang et al. [42] and S. Apel and C. Kästner [9].
A feature is a prominent or distinctive user-visible aspect, quality, or characteristic of a
software system or systems representing a functionality entity that satisfies a requirement,
serves a design decision, and provides a potential configuration option.
"The feature-oriented concept is based on the emphasis placed by the method on
identifying those features a user commonly expects in applications in a domain."
— Kyo C. Kang et al, Feature-Oriented Domain Analysis (FODA) Feasibility Study
[42]
28 1| Background and Motivations
imperative to thoroughly understand the testing part of the project found in section 2.4.
1.4.1. Overview
Software literature dates back to the early 1970s when Hetzel Hetzel organized the
first conference about software testing. Back then, engineers viewed software testing
as destructive, meaning they aimed at finding errors within a given problem rather than
constructive, which would have urged engineers to have working and fault-free software
systems. With the gain in popularity, software testing’s view shifted towards error preven-
tion, maintainability, and capability measuring during the 1980s. Most importantly, due
to its importance in assuring software quality, software safety, and customers satisfaction,
software development has integrated software testing into its life cycle in recent years [73].
Moreover, we find two terms that define the nature of a given test. Black-box testing
refers to those tests where the tester does not know the internal structure and algorithms
of the software. On the other hand, white-box testing assumes testers know the internal
structure and understand the detailed process of the software. Thus, we find the following
types of tests:
• Unit test - assesses the smallest unit of a system, meaning a method, function, or
procedure. Theoretically, it does not involve any other components, though avoiding
connection to external resources is not always feasible in practice. As we will see
in section 2.4, mocking techniques may assist developers in complying with unit
testing principles by keeping each unit isolated.
• Module test - is a class or package test concerning many programmers or a whole
team. However, its differences from a unit test are ambiguous and even subjective,
as testing all units within a module is equivalent to testing the module. Therefore,
we will treat module and unit testing as the same procedure and refer to both as
unit testing here forth.
• Integration test - combines all components which build up the whole system into a
test targeting the system workflow, ensuring that the system’s functionalities meet
the user requirements. Notice integration testing usually takes longer than unit
testing due to connection to external resources and dependencies.
• Acceptance test - corresponds to the user acceptance of the system. Users perform
the required acceptance test to ensure the working order and the correctness of the
system.
It is worth acknowledging that software engineers and developers perform unit and
integration testing. Hence, we can consider these tests as white-box testing since they
are aware of the implementation and inner workings of the system. Conversely, the
30 1| Background and Motivations
acceptance test is a type of black-box testing since the users who carry out these tasks
are unaware of the underlying system dependencies.
Furthermore, Figure 1.15 illustrates fundamental concepts, which will allow the
reader to dive deeper into more specific and involved testing aspects after the following
review.
The structure design corresponds to the software system’s location where component
organization and interaction occurs. Furthermore, it indicates the internal code structure
found in a software system. This code structure carries an associated complexity and
maintainability, impacting the overall system.
"The ease of modifying software components to include or remove capability, to tweak
performance, and to fix defects."
— Steve McConnell, Code complete: A practical handbook of software construction
[50]
From the definition of maintainability, we can observe the close relationship between
complexity and maintainability. Moreover, the collection of test cases defined for a given
software system composes the system’s test suite. The test suite’s objective is threefold,
as it reveals system faults, assesses the quality, and ensures the correctness of its func-
tionalities. Thus, maintainability and complexity are tightly related to the number of test
cases found in a system’s test suite. In other words, more complex systems require higher
maintainability efforts and a larger number of test cases.
"The ease of a system, component or requirement which allows test criteria to be
created and the ease of performing the test execution to verify the satisfaction of the
test criteria."
— IEEE, IEEE Standard Glossary of Software Engineering Terminology [41]
In simple terms, testability addresses the ease with which a system can be tested. Notice
that the test suite, complexity, and maintainability affect a given system’s testability.
Additionally, there are two essential facets of testability [12] [33]:
1| Background and Motivations 31
• Controllability - Degree of control software testers have over the system’s input
enabling testers to predict the system’s output value.
• Observability - Degree of verification software testers have over the system’s out-
put value provided a given input value enabling testers to verify the test creation
and execution correctness.
Ultimately, testability is one of the core features under maintainability. Thus, studying
testability relates to maintainability and vice versa.
loosely-coupled code, which increases the controllability and observability of test cases,
enhances software extensibility and reusability, and ultimately affecting positively software
quality attributes such as maintainability and testability.
1.4.4. Mock
Due to the remarkable weight unit testing has in this Thesis work, it is worth focus-
ing on mocks. Mocks allow programmers to use mock objects as fake components or
services instead of the real ones to ensure the correct functioning of unit tests. This
practice reinforces the fundamental principle of unit tests, which is testing the smallest
unit in isolation. However, real software applications exhibit classes that communicate
with other components and services, precluding isolated method testing and breaking the
original goal of unit testing. Thus, mocking said components and services allows develop-
ers to decouple external dependencies and execute unit tests in isolation, facilitating the
construction and execution of a system’s test suite.
Moreover, mock is a general term encompassing a family of similar implementations to
replace real external resources during unit testing [11]. There are other similar terms,
such as dummy, stub, fake, and mock, causing confusion for developers and readers due
to their vague differences. Therefore, M. Veng discerns them into the stub group, in-
cluding dummy, fake, and stub, and the mock group, including itself [73]. To clarify this
separation, he argues that stubs are stand-in resources providing only the necessary data,
while mocks extend this concept with object behavior. Hence, developers use stubs to
create state verification tests and mocks to build tests that verify method calls, calling
frequency, and calling order. Thereby, Mackinnon et al. [47] concluded that the mock
pattern includes the following five steps:
1. Create mock object
2. Set up mock object state
3. Set up mock object expectation
4. Supply mock object to domain code
5. Verify behavior on the mock object
Notice that this pattern highlights how steps one through four are common to both stubs
and mocks, while step five only applies to mocks as it includes behavior verification.
1.5. Motivations
The fundamental motivation for our contribution resides in the lack of consensus for
architecting large-scale Flutter applications observed in ??. The team behind this
technology does not provide an official approach for architecting a Flutter App, leaving
these decisions up to the implementers. Therefore, the powerful flexibility offered by
Flutter, along with the absence of a widely agreed solution for a clean architecture, leads
to significant problems when maintaining, scaling, and testing a codebase. Thus, we
identified the need for a standardized, systematic, and consistent approach to architecting
Flutter apps focused on separation of concerns and modularization, state management,
and testability.
2| Implementation
This chapter introduces the core aspects of the large-scale Flutter App implemented dur-
ing this thesis endeavor. This implementation serves as a tangible and practical solution
built on top of the theoretical concepts and motivations presented in chapter 1 and the
weaknesses found therein. Hence, we firstly provide the reader with a section covering
the description and details of the application and its development, to then introduce the
core aspects of the proposed application, such as the architectural patterns underpinning
said implementation, the techniques and code arrangements applied to incorporate an
effective state management system, and the testing approaches leveraged throughout the
application.
2.1. Context
This section reviews the most relevant aspects of the implemented large-scale Flutter
application, allowing the reader to learn essential knowledge which shall help them un-
derstand the upcoming content.
The development of the proposed application responds to a domotics company’s need
to have an accessible, capable, and intuitive system to manage a given smart home and
the devices inside. A smart home is a home setup where a networked device enables
users to remotely and automatically control internet-enabled appliances and devices. The
company’s goal is to implement this system in a fully automated home, featuring home se-
curity, attribute control and monitoring, access control, and alarm systems. Furthermore,
the desired system is a cross-platform (Android and iOS), cross-device mobile application
built with Flutter enabling users to interact with a given smart home, its devices, and
data. Notice these devices belong to the realm of the Internet of Things, a system of
interrelated computing devices uniquely identified facilitating data transfer over a net-
work without requiring human-to-human or human-to-computer interaction. It is worth
noting that the company requesting this system had already developed a Flutter App
aiming at fulfilling the previously mentioned desired functionality. However, this appli-
cation’s maintainability was below any acceptable standards as any of its functionality
included proper testing, and its codebase lacked consistency on critical aspects such as
36 2| Implementation
• Cache10 - Provides a generic LRU cache for JavaScript developers to store data
with priority and expiration settings.
• Predictions11 - Provides a solution for using AI and ML cloud services to enhance
your application.
More specifically, the proposed large-scale application leverages the following packages:
• amplify_flutter12 - The top-level module for Amplify Flutter.
• amplify_api13 - The Amplify Flutter API category plugin.
• amplify_auth_cognito14 - The Amplify Flutter Auth category plugin using the
AWS Cognito provider.
• amplify_storage_s315 - The Amplify Flutter Storage category plugin using the
AWS S3 provider.
Let us now discuss the application’s core functionality. The App provides users with au-
thentication functionality such as sign-up, sign-in, sign-out, and unregistering. Signed-up
users can create one or many homes, which behave as virtual representations of a given
physical smart home. Notice that these homes include multiple attributes that allow
users to customize and modify specific details for a particular home, such as its address,
apartment and unit number, type of home, size, internal systems (cooling, heating),
number of people, etc. More importantly, the application allows users to switch from
one home to another and access their specific IoT devices and data. Regarding IoT
devices, the App includes virtual representations of any given physical device registered
to a particular home. The application enables users to connect to a physical Hub, a
specific type of device providing access to the rest of IoT devices, manually, via WiFi,
or leveraging native software such as Apple HomeKit16 for iOS users or Easy Connect17
for Android users. Notice that the App integrates proper permission handling that
allows access to this functionality and services provided by native low-level components
and software. As far as the other IoT devices, the application features device-specific
flows to pair any supported device to a particular Hub by leveraging Bluetooth, QR
Code Scanning, or manual triggers. Furthermore, the application includes complex yet
intuitive views enabling users to control paired devices via device-specific commands,
which manifest the relevance of reactiveness in the proposed application. Moreover,
this application leverages Dart streams to support alarm notifications which inform
10
https://aws.github.io/aws-amplify/media/cache_guide
11
https://aws-amplify.github.io/docs/js/predictions
12
https://pub.dev/packages/amplify_flutter
13
https://pub.dev/packages/amplify_api
14
https://pub.dev/packages/amplify_auth_cognito
15
https://pub.dev/packages/amplify_storage_s3
16
https://www.apple.com/ios/home/
17
https://source.android.com/devices/tech/connect/wifi-easy-connect
38 2| Implementation
the users about critical activities and statuses of a given home and its devices. Notice
the App leverages Dart streams across the entire application to implement different
features and functionality, but alarm notifications are a notably interesting and complex
case as the application may be idle or in background mode, yet the user still needs to
receive such notifications. Lastly, the application also provides virtual representations of
rooms to allow users to place devices in the desired location, creating a fully immersive
smart-home experience through a mobile application. It is worth observing that the
application enables users to add, remove, or edit any given virtual entity, providing a
greater sense of ownership and customization while ensuring a sound and safe experience
through meticulous and accurate error handling.
Lastly, Appendix B includes screenshots of the most relevant screens and pro-
cesses included in the developed Flutter application. Notice these screenshots are not
a complete collection of all the different screens implemented in the App, but rather a
careful and deliberate selection of those screens highlighting the core functionality and
aspects described before. Moreover, although this is the UI implemented by the thesis’
author, the proposed architecture, modularization techniques, and state management
solution allow for total flexibility regarding UI design since the presentation layer is
decoupled from the other lower layers, as we will see in the upcoming sections. Thus,
any given Flutter developer could provide a different look and feel to our proposed
application, exhibiting the powerful general concepts and practices introduced in this
thesis work that can be transferred to another large-scale Flutter.
18
https://git-scm.com/
2| Implementation 39
the implementers, while the advantages of this approach are more tangible and directly
address two of our main objectives: high maintainability and testability. Following this
overview, we present the reader with the fundamental characteristics taken from each
approach and applied to our implementation.
2.2.1. Layered
Edsger W. Dijkstra [19] was the first to propose the fragmentation of software programs
into responsibility-based hierarchical levels, called layers, and has since become a com-
mon standard in large-scale applications [55]. Thereby, we leveraged this industry-tested
approach to propose a layered architecture based on four distinct levels: User Interface
Layer, Business Logic Layer, Repository Layer, and Data Layer.
• Data Layer - It is the lowest layer of our four-tier architecture. It is responsible
for communicating with external sources (databases or APIs) to retrieve raw data.
Moreover, this layer exposes clients free of any UI-specific dependencies. Lastly, we
can consider this layer the engineering layer since it serves the purpose of efficiently
processing and transforming data.
• Repository Layer - Decouples the business logic and data layers and composes
one or more clients from the data layer to apply domain-specific business rules to the
retrieved data. Domain-based repositories compose this layer whose principal role
is centralizing shared data access functionality, acting as a middleman between the
business logic and data layers. Furthermore, there should only be one repository per
domain, which must be free of any Flutter dependencies, and it can only interact
with the data layer. Lastly, we can consider this layer the product layer since it
brings value to the user through the exposure of refined data.
• Business Logic Layer - Vessels the business logic of the application. Blocs and
Cubits compose one or more repositories and include the logic to surface the business
rules via a specific feature or use-case. Moreover, this layer employs the bloc library
to manage the logic associated with each feature. Hence, we can consider this layer
the feature layer as it determines the correct functioning of any given feature. The
state management section 2.3 provides more comprehensive information about this
layer and its implementation.
• Presentation Layer - It is the top-most layer of our four-tier architecture. It
serves as the UI of the application, allowing users to interact directly with it. These
interactions generate events, which are then forwarded to the business logic layer.
Moreover, it reacts to state changes from the business logic layer, prompting the
UI to trigger rendering modifications via Flutter Widgets. Furthermore, this layer
2| Implementation 41
should only interact with the business logic layer. Lastly, we can consider this
layer the design layer since it aims to provide the best possible experience for users
through visual components and effects.
Notice that the external service layer, the bottom-most layer, depicted in Figure 2.1 does
not depend on us, and hence, we do not account for it when describing this layered
architecture. Additionally, the repository layer adheres to the essential premises of the
Repository Pattern [34]. However, our proposed implementation does not rely on agnostic
interfaces that enable its inheritance at the data layer, but we leverage client-oriented
composability to craft these domain-based repositories.
2.2.2. Feature-Oriented
Before diving into the details of how our architecture implements the feature-oriented
approach, it is necessary to distinguish the concept of feature by layer:
• Infrastructure Feature - It is a feature found in the data layer and presented as a
client module that adheres to the role of this layer by communicating with external
42 2| Implementation
sources and fetching data. It adds functionality at a low level within the application
structure, and hence, it does not provide direct value to the application users as
they perceive it as a black box.
• Domain Feature - It is a feature found in the domain layer and presented as a
domain-based repository that adheres to the role of this layer by applying domain-
specific business rules to the retrieved data. It adds functionality at a middle level
within the application structure, and hence, it does not provide direct value to the
application users as they still perceive it as a black box.
• Application Feature - It is a feature found within the business logic layer (logic
component), the presentation layer (design component), or both layers (combined
component) that adheres to the role of this layer by providing either functionality
or visual value, or both. Notice that this feature sits at the highest level within the
application structure, and hence, it exhibits direct value to the application users as
they can interact with this feature.
Infrastructure Features
We find infrastructure features at the bottom-most architectural layer of the application.
As previously mentioned, we use modules called clients to implement these features, which
we differentiate between pure or involved clients depending on their intrinsic complexity.
Notice that said complexity refers to the client’s internal functionality and data manip-
ulation. Thus, we present the reader with one example of an involved client, which is
paramount to the proper functionality of the proposed application, and another example
of a simple client, which serves as a basis and reference for other simple clients imple-
mented in the application.
Firstly, we proceed to discuss our API Client. This module features the most involved
functionality, data manipulation, and thus, code structure among all the implemented
clients. Its essential role is to communicate with a specific API to fetch, create, and
update data and subscribe to data changes. In this particular case, our application must
interact with a GraphQL API19 that accepts queries to fetch data, mutations to create
and update data, and subscriptions to subscribe to data changes in the back-end. It is
worth noting that the structure of this client would require minor modifications to serve
the same purpose should the back-end expose a REST API instead, although the libraries
and methods employed to communicate with one or the other API would change signifi-
cantly since the interaction with GraphQL and REST APIs differs significantly. However,
none of these changes should affect the layers above since the implementation is abstracted
19
https://graphql.org/
2| Implementation 43
From Figure 2.2 above, we observe that domains influence how we structure this client
module. These domains correspond to a representation of a meaningful concept or object
retrieved from the API. Notice that the analysis, definition, and design of these domains
are outside the scope of this thesis work as they were the responsibility of the requirements
and embedded teams, and we solely needed to incorporate them into the application for
further manipulation. Moreover, these domains may introduce specific errors in the sys-
tem, which we need to address and handle accordingly. Thereby, the error submodule
44 2| Implementation
and the different error files therein allow mapping said errors, providing effective error
handling within this client, and avoiding exception leakage to upper layers. Additionally,
the model submodule contains the files that allow the mapping of raw data into more
structured and valuable data objects. These models need not match precisely the repre-
sentation of the domain models in the back-end but rather provide a solid basis for upper
layers to consume and further manipulate this data. Furthermore, the operation submod-
ule contains files including the different operations the API Client can perform against
the GraphQL API, while the payload submodule contains the different objects (payloads)
returned by any of these given operations. Notice that these payloads include the raw
data to be manipulated and mapped into their corresponding model and error objects.
Lastly, the domain_n.dart file leverages all the submodules previously described and adds
the necessary functionality to interact with the external source through dedicated class
methods.
Listing 2: Api Client
1 /// {@template api_client}
2 /// An API client to communicate with GraphQL API
3 /// {@endtemplate}
4 class ApiClient {
5 /// {@macro api_client}
6 ApiClient({
7 required GraphQLCategory graphQLCategory,
8 required Client client,
9 }) : _accountResource = AccountResource(graphQLCategory: graphQLCategory),
10 _homeResource = HomeResource(
11 graphQLCategory: graphQLCategory,
12 http: client,
13 ),
14 _deviceResource = DeviceResource(graphQLCategory: graphQLCategory),
15 _deviceCommandResource = DeviceCommandResource(
16 graphQLCategory: graphQLCategory,
17 ),
18 _hubResource = HubResource(graphQLCategory: graphQLCategory),
19 _areaResource = AreaResource(graphQLCategory: graphQLCategory),
20 _alarmResource = AlarmResource(graphQLCategory: graphQLCategory),
21 _homeMemberResource = HomeMemberResource(
22 graphQLCategory: graphQLCategory,
23 );
24
2| Implementation 45
Domain Features
We find domain features within the domain-architectural layer of the application. As pre-
viously stated, we use modules known as repositories to implement these features. Much
like the clients in the data layer, repositories do not provide direct value to application
users. However, they represent a fundamental piece of the proposed software architec-
ture, insulating the application from changes in the persistence store (service layer) and
facilitating automated unit testing, as we will see in section 2.4. Furthermore, we created
a component that allows emitting events from a Future response or another Stream of
events without blocking the data flow. We called this component AsyncBehaviorSubject,
enabling repositories to become reactive components that provide the business logic layer
with continuous data streams. We consider this feature a novel approach that allows us
to implement reactive principles leveraged by the blocs at the business logic layer instead
of implementing a typical imperative behavior. Thus, we present the reader with the con-
siderably straightforward structures of said AsyncBehaviorSubject and repositories, and
a code snippet for the AreaRepository, while the code snippet for AsyncBehaviorSubject
is listed under the List of Source Codes 2.4.
28 } catch (e) {
29 throw AreaResourceFailure(e);
30 }
31 }
32
17 @override
18 String toString() => 'AreaResourceFailure - $originalException';
19 }
2| Implementation 49
Application Features
We find applications features at the top-most architectural layer of the application. As
previously described, these features add either functional or visual value, or both, directly
to the application user. Thus, we introduce the reader with three different examples of a
feature solely adding visual value, another one solely adding functional value, and lastly,
one providing both visual and functional value to the end-user.
4 @override
5 Widget build(BuildContext context) {
6 return const Scaffold(
7 extendBodyBehindAppBar: true,
8 appBar: CustomAppBar(
9 automaticallyImplyLeading: false,
10 ),
11 body: SafeArea(
12 child: WelcomeView(),
13 ),
14 );
15 }
16 }
The code presented above shows the code employed to create a visual feature. The main
50 2| Implementation
characteristic of this type of feature is that it only uses Flutter Widgets to provide its value,
and hence, it does not require state management or additional complex functionality. In
this particular case, we observe the top-most widget of the feature, WelcomePage, which
provides the initial Scaffold and body structure for nested Widgets down the Widget Tree
to complete the visual feature. As shown in Figure 2.6, developers can implement as many
Widgets as they may need to implement a visual feature.
16 on<HomeUpdated>(_onHomeUpdated);
17 on<HomeNotFound>(_onHomeNotFound);
18 }
19
23
38 @override
39 Future<void> close() {
40 _sub.cancel();
41 return super.close();
42 }
43 }
Notice that the functional feature shown in Figure 2.7 and the code above corresponds to
a bloc handling the events and state performed on a Home, one of the domains included
in the application. Furthermore, this type of feature usually manages the business logic
above other nested features, either visual or functional, or both. Notice it is a standalone
component, and hence, we can reuse or leverage it in other parts of the application,
ensuring that the behavior of this functional feature stays consistent across the application.
Lastly, we provide further insights on functional features in section 2.3, presenting the
reader with a comprehensive overview of state management patterns and components.
52 2| Implementation
Figure 2.8 above, the HomeDetailsBloc, and HomeDetailsPage displayed in code listings
24 and 25, respectively, show a feature’s structure and implementation combining both
visual and functional values. More specifically, this feature provides the end-user with a
Home Details Page, including a nested view and Widgets, which leverages a HomeBloc
and a HomeDetailsBloc to handle user interactions and state changes throughout this
feature. Furthermore, this feature includes both a bloc and view subcomponents, enabling
more complex features. Ultimately, these are the most widely used features within the
application as they allow us to enhance the presentation layer with the state changes
handled by blocs at the business logic layer. Once again, we take a more in-depth look at
these features in section 2.3.
2.2.3. Packaging
In addition to the advantages provided by combining the layered and feature-oriented
architectural patterns, we consider fundamental complementing our architecture with an
effective modularization approach. This approach leverages the full power of dart packages
reviewed in Section 1.2.4, enabling the compartmentalization of features by layer and
feature. We propose a project structure that adheres to the Multimodule Monorepo
[46] [52], an approach that allows maintaining a project as a single repository with multiple
submodules for each of the features included in each layer. Furthermore, the taxonomy
of this enhanced architecture introduces two higher grouping layers: lib and packages.
2| Implementation 53
As shown in Figure 2.9, the lib layer is effectively a directory encompassing the two higher
layers of our architecture, presentation and business logic, and the application features
therein. Moreover, we find another encompassing layer called packages, whose principal
function is to group the repositories and clients within the domain and service layers under
a single directory. Ultimately, we can group all our application features as submodules
under the lib directory while repositories and clients become standalone packages listed
under the packages directory. Notice this approach provides our project with several
benefits, such as:
• Discoverability - Access to all packages and codes from the IDE view
• Separation of concern - Each package has a single purpose and becomes a com-
posing part of a given layer
• Testability and composition: Each layer has distinct rules enabling independent
and isolated testing by layer
• Reusability: Different projects may share the same package keeping functionality
consistent across applications
• Clarity: Explicit understanding of the project’s dependency graph (tools like pub-
54 2| Implementation
In the end, following these architectural patterns and decisions enables engineers
to easily add, modify, remove, and test features and address bugs and fixes without
affecting other project mates’ work, enhancing the overall maintainability and scalability
of a given software application. Refer to Figure 2.10 below for an explicit, though
simplified, visualization of the proposed project’s directory structure.
20
https://pub.dev/packages/pubviz
21
https://pub.dev/packages/very_good_cli
22
https://github.com/VeryGoodOpenSource/very_good_cli/blob/main/doc/very_good_core.
md
2| Implementation 55
Predictability
Engineers often face significant challenges when accurately determining the state of the
developed application at any given point in time. As we reviewed in Section [26], Flutter
introduces the Widget Tree to address this challenging ambiguity, allowing developers
to modify the state of the Widgets, and hence, the Widget Tree’s structure. However,
managing widget states vertically and horizontally across a complex Widget Tree is a
tortuous and intricate endeavor. Therefore, flutter_bloc allows developers to decompose
the application’s state into smaller, well-defined, and deterministic state machines to
address this complexity and ambiguity. Ultimately, these state machines transform events
into zero, one, or multiple predictable states.
56 2| Implementation
Simplicity
Notice Apps are reactive by nature, and thus, developers must program them to be reac-
tive. However, this natural reactiveness is the cause of non-deterministic user interactions
that may occur at any point in time, if at all. Therefore, developers traditionally relied
on powerful yet complex APIs to manage said reactiveness and produce interactive and
engaging applications. On the other hand, flutter_bloc proposes a simplified API that
abstracts the complexity of streams while still honoring the natural reactiveness of ap-
plications. Thereby, developers need not maintain non-trivial stream subscriptions and
lifecycles, allowing them to focus on predictable interactions by handling incoming events
and outputting new states.
High testability
As previously stated numerous times throughout this Thesis work, we consider testability
is a crucial feature of any high-quality software application. Furthermore, we seek and
deliver one hundred percent coverage through unit testing, as shown in section 1.4, to
boost developers’ confidence in delivering reliable and quality products. Moreover, the
flutter_library makes code testing one of its principal values and provides a dedicated
package for such a purpose, bloc_test. This package is a utility library that eliminates
the complexity of testing reactive code while enabling developers to unit test their code
and validate the product behavior at any point in time with barely any setup required.
However, we had to address and clarify some concerns about flutter_bloc being
too complex requiring too much boilerplate. Regarding the package’s complexity, it is
necessary to highlight that Flutter already provides a capable solution to manage state:
setState on StatefulWidgets. However, this simplistic approach can rapidly lead to state
mismanagement, and at scale, it may suffer from known problems such as prop drilling
[29]. As for the required boilerplate, we consider benefits introduced by flutter_bloc
such as truly immutable and independent events and states outweigh having fewer lines
of code. Thus, the package’s complexity and required boilerplate remain justified and
do not suggest that any other analyzed solution should become more suitable for our
purposes.
Code Sample I
This first code sample presents a straightforward Bloc, allowing the introduction of more
complex examples building on top of the concepts learned from this initial use case. These
types of blocs represent a functional feature and favor reusability by other design features.
Hence, we start by analyzing the events shaping the behavior of the proposed bloc.
6 @override
7 List<Object> get props => [];
8 }
9
10 @override
11 List<Object?> get props => [status];
12
13 DeleteDeviceState copyWith({
14 Device? device,
15 DeleteDeviceStatus? status,
16 }) =>
17 DeleteDeviceState(
18 status: status ?? this.status,
19 );
20 }
21
22 enum DeleteDeviceStatus {
23 initial,
24 updated,
25 loading,
26 success,
27 failed,
28 }
The state file includes a single immutable and instantiable class, DeleteDeviceState. It is
worth mentioning that other implementations and even the official flutter_bloc documen-
tation declare the main Bloc state class as an abstract and then include multiple state
classes to represent the different states a given feature may exhibit. However, we intro-
duce a cleaner and leaner approach that provides enhanced flexibility when leveraging this
state within the presentation layer without jeopardizing the immutability principles. This
approach relies on a status Enum encompassing the different states otherwise declared as
immutable classes and a copyWith method that returns a new instance of the state class.
2| Implementation 59
We can employ this approach thanks to the Equatable23 class that allows comparing two
objects, including their properties, and thus states too, to determine whether they are
equal or not. Notice how the overridden props getter includes the status property, which
effectively allows differentiating between states by checking the value of this property.
Ultimately, we broadly use this pattern in the application, keeping bloc state design and
implementation consistent across features. Moreover, we proceed to analyze the proposed
bloc.
Listing 9: Delete Device Bloc
1 part 'delete_device_event.dart';
2 part 'delete_device_state.dart';
3
18 FutureOr<void> _deleteDeviceRequested(
19 DeleteDeviceRequested event,
20 Emitter<DeleteDeviceState> emit,
21 ) async {
22 try {
23 emit(state.copyWith(
24 status: DeleteDeviceStatus.loading,
25 ),
26 );
27 await _deviceRepository.unregisterDevice(
28 deviceId: _deviceId,
29 );
23
https://pub.dev/packages/equatable
60 2| Implementation
30 emit(state.copyWith(
31 status: DeleteDeviceStatus.success,
32 ),
33 );
34 } catch (e) {
35 addError(e);
36 emit(state.copyWith(
37 status: DeleteDeviceStatus.failed,
38 ),
39 );
40 }
41 }
42 }
The bloc described in the code above, DeleteDeviceBloc, extends the base class
Bloc<Event, Class>, having DeleteDeviceEvent and DeleteDeviceState as the Event
and State types, respectively. Notice we must inject all the dependencies leveraged by the
bloc as parameters. Thus, employing this DI approach enables developers to thoroughly
test blocs, as we will see in subsection 2.4.3. In this case, the described bloc requires
an instance of a DeviceRepository and a parameter of type String. Furthermore, the
on<DeleteDeviceRequested > function enables the registration of an event handler for
an event of type DeleteDeviceRequested. Additionally, the flutter_bloc documentation
indicates "there should only ever be one event handler per event type E," ensuring event
handling in a systematic and deterministic manner. Moreover, the described bloc includes
a private method to handle the reaction to the DeleteDeviceRequested event. This private
method leverages an Emitter of the same type as the bloc state, DeleteDeviceState, to
emit state changes exploited by the presentation layer. Notice how we employ the pre-
viously described copyWith method to emit a new state that includes the corresponding
status. Lastly, since the private method includes an asynchronous operation, we must
use the async-await keywords to ensure a correct state sequence and surround it in a
try-catch block to prevent unhandled exceptions, including an addError function that
allows logging caught errors.
Code Sample II
We now review a more involved use case that refers to a feature combining design and
functional value. Thus, we aim to demonstrate how these two parts are integrated and
walk the reader through the thinking process and implications behind the code referenced
in the List of Source Codes . Much like we did in the previous code sample, we will start
2| Implementation 61
by analyzing the proposed bloc and its different parts, to then move onto the feature’s
View and how the state affects the widgets therein.
From the code snippet 26, we observe multiple immutable classes extending the base class
EditDeviceEvent. These immutable classes correspond to a particular event that the bloc
will handle independently. Moreover, this example emphasizes the importance of the
Equatable class, rendering each immutable event class and its properties independent,
thus, enabling value comparison. Let us now analyze the state of the proposed bloc.
The code presented in 27 corresponds to EditDeviceState, the state shaping the behavior
of the proposed bloc. Notice how this state’s strategy and structure mirrors the state
presented in the previous example by using a single instantiable state class that includes
a copyWith method and relies on a particular enum to determine the state’s status. The
main differences from the previous simpler example are the number of properties included
in this state and the use of boolean getters. The latter is a common practice used
throughout the application to simplify the condition statements in the presentation layer.
Moreover, we proceed to analyze the proposed.
Regarding the proposed bloc 28, EditDeviceBloc, we observe that, once again, it features
evident similarities to the bloc presented previously. However, this bloc features more
parameters required by the bloc constructor and events. Regarding the parameters, it
stands out the use of two repositories, DeviceRepository and AreaDepository, which shall
allow the bloc to interact with the domain layer. As for the events, we have omitted
the specific code of each of the listed private methods for brevity and generality shake.
Nonetheless, notice the one-to-one correlation between the number of events extending
the EditDeviceEvent base class and the number of private bloc methods that include the
business logic to handle each event. Ultimately, this correlation indicates the need for a
bloc to manage each event independently and on a case-by-case basis. Let us now review
the presentation components leveraging the analyzed bloc.
17 @override
18 Widget build(BuildContext context) {
19 return BlocProvider(
20 create: (context) => EditDeviceBloc(
21 homeId: context.read<HomeBloc>().homeId,
22 device: device,
23 deviceRepository: context.read<DeviceRepository>(),
24 areaRepository: context.read<AreaRepository>(),
25 )..add(EditDeviceAreaRequested()),
26 child: const EditDeviceView(),
27 );
28 }
29 }
Firstly, we need to address the technique employed to inject the bloc within the presen-
tation layer so that widgets further down the Widget Tree have access to it. We call this
technique the Page-View pattern and it prevents developers from facing a common
issue where the code calls a bloc that is not within the context it is being used. Thus,
the Page-View pattern enables developers to inject a given bloc at the top of a feature’s
Widget Tree allowing the View Widget and its nested Widgets to use said bloc through
the BuildContext. Thereby, we inject the EditDeviceBloc via the BlocProvider and
pass as a child Widget the EditDeviceView. It is worth noting that we can pass the nec-
essary dependencies, such as repository instances or data from other blocs’ states, to the
bloc by leveraging context.read<T>. This data access and DI mechanism are deeply
influenced by how we inject dependencies into the application structure (Widget Tree),
further reviewed in the Testing section. Furthermore, observe how we add an event, Ed-
itDeviceAreaRequested, as soon as we instantiate the bloc, allowing the EditDeviceView
to access the initial necessary data right away.
Let us now take a look at the EditDeviceView Widget presented in source code 29. The
EditDeviceView Widget’s purpose is to provide a Scaffold for rendering nested widgets,
and more importantly, listen to state changes to show notification widgets and perform
navigation actions. Regarding the listening aspect, widgets may employ a BlocLis-
tener<Bloc, State>, which guarantees to invoke the listener in response to a state
2| Implementation 63
4 @override
5 Widget build(BuildContext context) {
6 return BlocProvider(
7 create: (context) => PlugPairingBloc(
8 hubRepository: context.read<HubRepository>(),
9 deviceRepository: context.read<DeviceRepository>(),
10 )..add(const PlugPairingCableChecked()),
11 child: const PlugPairingBodySwitcher(),
12 );
13 }
64 2| Implementation
14 }
The PlugPairingBody Widget presented in code above adheres to the Page-View pat-
tern previously mentioned. Notice this pattern does not enforce a fixed naming con-
vention as the Page and View terms are concepts rather than specific types of widgets.
Therefore, in this case, PlugPairingBody corresponds to the Page concept, and Plug-
PairingBodySwitcher corresponds to the View concept. Thus, we inject the PlugPairing-
Bloc with its required dependencies using the create function parameter exposed by the
BlocProvider and pass PlugPairingBodySwitcher as a child having access to the injected
bloc. Let us now look further into this child widget shown in code snippet 31.
PlugPairingBodySwitcher serves as a general example of a widget that allows developers
to address a typical situation where the contents of a View must dynamically change
based on a given state. More specifically, we are not interested in rerendering some of
the displayed widgets but rather in changing the entire Widget Tree contents from below
a specific point, effectively rendering a new View. Moreover, these kinds of views often
require developers to handle showing notifications in the form of dialogs or snack bars
and navigation control. Thereby, BlocConsumer<Bloc, State> enables developers
to address this situation neatly. This flutter-provided class, analogous to a nested Blo-
cListener and BlocBuilder, exposes a builder and listener to react to new states while
reducing the boilerplate needed. Notice how BlocConsumer<PlugPairingBloc, PlugPair-
ingState> relies on buildWhen for more granular control avoiding unnecessary builder
calls, enhancing the widget’s efficiency.
Code Sample IV
Lastly, we introduce the reader to an example of the only cubit implemented in the
proposed Flutter application. The reason behind using blocs in favor of cubits is that the
latter limit some relevant functionality, like automating logs and analytic events, which
become convenient and valuable as the application grows. Nonetheless, this code sample
exemplifies the usefulness of cubits for particular situations, rendering our review of state
management implementations and use cases complete.
7 if (index >= 0
8 && index <= BottomNavigationPage.values.length) {
9 emit(BottomNavigationPage.values[index]);
10 }
11 }
12 }
13
14 enum BottomNavigationPage {
15 home,
16 areas,
17 stats,
18 routines,
19 settings,
20 }
The code presented above refers to a cubit used to manage the navigation across the main
tabs or pages of the implemented application. Thus, BottomNavigationCubit implements
a single public method that manages the navigation flow, switchTo, and leverages an
enum as the state shaping the cubit’s behavior, BottomNavigationPage. Notice the evident
similarities with its bloc counterpart, but most importantly, the reduction in boilerplate
employed to implement a cubit.
2.4. Testing
This section presents the last and arguably, most critical content related to the proposed
large-scale Flutter application’s implementation. It provides the reader with a preamble
that covers substantial insights into the motivations and criteria behind the proposed
testing practices. Moreover, it introduces an exhaustive list of the most characteristic
tests found in the application’s test suite. Lastly, it culminates with some final remarks
about the considered test cases and software testing in the context of this implementation.
risk, increasing confidence in a given codebase, and keeping current expectations and
assumptions aligned. Ultimately, a well-structured test will always output the same result
for any given input, ensuring the long-term functionality of code regardless of functionality
and features added in the future.
Furthermore, Section 1.4 offered an extensive software testing analysis, touching upon
some fundamental aspects, such as testing criteria and code coverage. Accordingly, we
strive for one hundred percent code coverage for our entire codebase as a standard for
code quality and test adequacy, enforcing the exercise of every line of code at least once.
Additionally, we integrate these standards into a work methodology requiring developers
to build features and corresponding tests as part of the same engineering effort. This
approach encourages code ownership and responsibility while potentially boosting pro-
ductivity and predictable behaviors across an engineering team.
Lastly, a comprehensive test suite requires extra time to write and maintain, which may
hinder the initial progress of pure development tasks. Nonetheless, as a codebase grows,
tests serve to avoid requirements ambiguity, communicate intended behavior, and identify
and fix unwanted functionality or bugs. Thus, investing time in writing tests alongside
feature implementation saves time in the long run by avoiding code rewrites and helps to
deliver more stable and reliable products.
api_client
Let us now consider the api_client package to illustrate the testing practices employed
to analyze its internal functionality. Before diving into the code, it is worth noting that
this package is of utmost importance for our codebase as it interacts directly with the
external GraphQL API, and hence, numerous repository packages rely on its functionality
to access domain data. Thus, selecting this specific package to exemplify testing practices
in client packages is not a coincidence. Instead, we deliberately chose this package to
enable the reader to understand the implications of comprehensively testing this specific
package and its effects on the broader scheme of packages and features included in the
application. Moreover, we present the initial setup for the tests cases addressing one of the
2| Implementation 67
resources exposed by the ApiClient class, the AlarmResource, in the code sample below.
7 void main() {
8 group('AlarmResource', () {
9 late GraphQLCategory graphQLCategory;
10 late AlarmResource alarmResource;
11
12 setUp(() {
13 graphQLCategory = MockGraphQLCategory();
14 alarmResource = AlarmResource(
15 graphQLCategory: graphQLCategory,
16 );
17 });
18
19 setUpAll(() {
20 registerFallbackValue(FakeGraphQLRequest());
21 });
22 }
Firstly, we must declare the mock and fake classes representing external dependencies
leveraged by the ApiClient and the AlarmResource classes. Mocking or faking a depen-
dency enables developers to isolate and focus on the tests instead of on the behavior or
state of external dependencies. As stated in section 1.4.4, fakes are stand-in resources that
provide only the necessary data, while mocks extend this concept with object behavior.
In this case, MockGraphQLCategory allows simulating the behavior of queries, mutations,
and subscriptions without interacting with the external API, while FakeGraphQLRequest
allows to fake the request data included in the mocked object. Furthermore, gathering all
the test setup and cases under a descriptive group under main() is a best practice that
boosts clarity, organization, and maintainability as the test suite grows. Then, observe
how we use the setUp function, a helper function that runs before executing every test,
to instantiate the mocked GraphQLCategory and then inject it as a dependency of the
AlarmResource, effectively allowing developers to control the behavior of queries, muta-
tions, and subscriptions within this class object. Lastly, setUpAll registers a function
68 2| Implementation
to be run once before all tests, in this case, registerFallbackValue. Passing an instance
of FakeGraphQLRequest to registerFallbackValue() allows using any() as a parameter to
mock objects, which is crucial for taking full advantage of mocking practices. Let us now
introduce a simple test that serves as a basis for more complex examples.
their previous setup are general enough to showcase how this approach shaped the rest of
the test suite employed to analyze the remaining functionality exhibited by this resource,
and most importantly, the rest of the resources exposed by the ApiClient class.
alarm_repository
Let us now introduce a series of insightful tests for the alarm_repository, which al-
low the reader to continue building knowledge about unit testing upon the previously
analyzed api_client package. Much like in the example before, the following code and
descriptions do not cover all the behavior exhibited by the AlarmRepository class and
do not represent all the tests included in this package’s test suite, but they provide the
necessary information to comprehend how and why we applied these generic tests across
different repositories within the domain layer. Once again, we start by reviewing the
setup preceding the test cases.
One final comment about the exemplified test cases shown in this subsection is
that implementing the underlying code that powers the helper methods used to create
and manage mock and fake objects would be extremely time-consuming and inefficient.
Therefore, our entire application’s test suite relies on mocktail24 , a Dart package that
focuses on providing a familiar, simple API for creating mocks in Dart (with null-safety)
without the need for manual mocks or code generation.
24
https://pub.dev/packages/mocktail
2| Implementation 71
7 );
8 }
9
10 blocTest<WaterLeakAlarmBloc, WaterLeakAlarmState>(
11 'emits correct state when alarmRepository.alarms '
12 'returns Stream with expected alarms',
13 setUp: () {
14 when(() => alarmRepository.alarms(any()))
15 .thenAnswer((_) => Stream.value(alarms));
16 },
17 build: buildBloc,
18 act: (bloc) => bloc.add(const WaterLeakAlarmDataFetched()),
19 expect: () => <WaterLeakAlarmState>[
20 const WaterLeakAlarmState(
21 fetchedAlarmStatus: FetchedDevicesStatus.loading,
22 ),
23 WaterLeakAlarmState(
24 alarm: alarm,
25 fetchedAlarmStatus: FetchedDevicesStatus.success,
26 ),
27 ],
28 );
29
30 blocTest<WaterLeakAlarmBloc, WaterLeakAlarmState>(
31 'emits error',
32 setUp: () {
33 when(() => alarmRepository.alarms(any()))
34 .thenAnswer((_) => Stream.error('Error'));
35 },
36 build: () => WaterLeakAlarmBloc(
37 homeId: 'homeId',
38 deviceRepository: deviceRepository,
39 alarmRepository: alarmRepository,
40 ),
41 act: (bloc) => bloc.add(const WaterLeakAlarmDataFetched()),
42 expect: () => const <WaterLeakAlarmState>[
43 WaterLeakAlarmState(
44 fetchedAlarmStatus: FetchedDevicesStatus.loading,
45 ),
2| Implementation 73
46 WaterLeakAlarmState(
47 fetchedAlarmStatus: FetchedDevicesStatus.error,
48 ),
49 ],
50 );
51 });
Observe how we gather all the test cases related to the WaterLeakAlarmDataFetched
bloc event under the same group. This grouping approach allows developers to tackle
functionality on an event-by-event basis in an orderly manner. Furthermore, a common
practice is to create custom helper functions, such as buildBloc(), which developers
can reuse across multiple test cases to avoid code redundancy. Moreover, we introduce a
detailed dissection of the underlying functionality of a blocTest to understand the two
examples presented in the code above:
• blocTest - Creates a new bloc-specific test case with the given description. It
handles asserting the orderly emission of the expected bloc after executing the act
function. Additionally, it ensures that no additional states are emitted by closing
the bloc stream before evaluating the expectation.
• setUp - It is an optional parameter used to set up any dependencies before initial-
izing the bloc under test. Ultimately, it sets up the necessary state for a particular
test case. build - Constructs and returns the bloc under test.
• act - It is an optional callback parameter invoked with the bloc under test and used
to interact with the bloc.
• expect - It is an optional Function parameter that allows verifying that the bloc
under test emits the expected returned Matcher after executing the act function.
Notice that said Matcher is often an ordered list of the emitted bloc states, as shown
in the test cases above.
Moreover, we introduce one final bloc test in code snippet 37 to complete the bloc testing
process review. Notice that this code addresses a new bloc event, WaterLeakAlarmOkSta-
tusBannerClosed. This test case features the same structure as the previous bloc tests, but
it presents a subtle yet powerful variation. This variation refers to the seed parameter,
an optional Function that returns a state used to seed the bloc before calling act.
and often unaffordable human efforts. Therefore, automated tests mitigate these efforts
by ensuring a given app performs correctly before publishing it while retaining features
and bug-fix velocity. Furthermore, the Flutter framework supports a variety of automated
tests summarized in the table below 2.1.
Integration An end-to-end experience with mocked dependencies To find business logic breaks in a client
End-to-End An end-to-end experience with a real backend and/or hardware Attempting to validate the full end-user experience
Is a “black box”
Golden The “pixel-by-pixel” spec Once you have widget tests and UI is locked down
This thesis work focuses on widget testing to validate the behavior of the presentation
layer, and hence, this subsection introduces the reader to this new testing approach. A
widget test, also known as component test in other UI frameworks, tests a single wid-
get. Its goal is to verify that a widget’s UI looks and interacts as expected. Furthermore,
widget testing involves multiple classes and requires a test environment that provides the
appropriate widget lifecycle context [24]. It is worth mentioning that a widget test is more
comprehensive than a unit test, though it also requires a simplified test environment to
validate the results. Therefore, it is critical to properly architect the Flutter application,
and most importantly, its Widget Tree to provide a widget test suite with such a test-
ing environment. We consider that the approach illustrated in code snippet 38 provides
the most effective and least error-prone architecture for widget-based feature integration
and testing. Notice how the App widget, placed at the top of the Widget Tree, requires
injecting all the necessary repositories as already-instantiated injectable dependencies.
Furthermore, we use MultiRepositoryProvider to merge multiple RepositoryProvider wid-
gets into a single widget tree, which improves the readability and eliminates the need
to nest multiple RepositoryProviders, while ensuring child widgets have access to the in-
jected repositories. The implications of this DI approach are tremendously relevant as it
allows developers to have access to these repositories anywhere down the Widget Tree via
BuildContext, enabling them to inject any repository into any given bloc that may require
its exposed functionality. Lastly, observe that we, once again, use the Page-View pattern
allowing us to pass the AppView widget as the child funneling down this functionality.
Moreover, this approach facilitates the creation of a helper extension, AppTester, on Wid-
2| Implementation 75
getTester, a class that programmatically interacts with widgets and the test environment.
Thus, the sole purpose of this helper extension is to provide a flexible environment for
a widgetUnderTest. This environment leverages a MaterialApp widget as a wrapper for
widgetUnderTest and defaults blocs and repositories to mock instances unless developers
inject custom mock objects to achieve further controllability. The invaluable benefit of
using this helper will become evident once we review the widget test cases introduced
in the upcoming code samples. Hence, we present the reader with our proposed setup
for widget testing in code snippet 39 and three different examples addressing the most
common use cases for testing widgets in a large-scale Flutter application.
Setup
Firstly, we declare the mock objects leveraged across the different test cases, as we did in
previous testing examples. In this case, we are testing the presentation layer, and hence,
the external dependencies or components producing side effects within test cases are typi-
cally blocs. Therefore, bloc_test provides a helper class, MockBloc, enabling developers
to create mock blocs that implement all the necessary fields and methods and allow fur-
ther customization at runtime to define how the bloc may behave. Furthermore, we
employ the same grouping and setUp approach employed in previous test examples by
providing an explicit description of the widget under test, WaterLeakAlarmDetailsView,
and instantiating the mock object, MockWaterLeakAlarmBloc. Notice the setUp func-
tion also defines the behavior of MockWaterLeakAlarmBloc when accessing its state by
returning an instance of its initial state, WaterLeakAlarmState.
5 void main() {
6 group('WaterLeakAlarmDetailsView', () {
7 late WaterLeakAlarmBloc waterLeakAlarmBloc;
8
9 setUp(() {
10 waterLeakAlarmBloc = MockWaterLeakAlarmBloc();
11 when(() => waterLeakAlarmBloc.state).thenReturn(
12 const WaterLeakAlarmState(),
13 );
14 });
76 2| Implementation
15 )};
16 }
Lastly, we introduce a common practice implemented across the application widget-testing
files, which relies on a WidgetTester extension, pumpWaterLeakAlarmDetailsView, that
allows developers to inject a mock bloc with custom behavior into the previously ana-
lyzed helper method, pumpApp. Ultimately, this technique saves time, improves test
readability, and avoids potentially unintended behavior.
renders
Testing whether and when a Flutter application renders a given widget is arguably the
most fundamental widget test. Thus, we provide the reader with two examples addressing
this test case. Notice that both tests adhere to the same structure using the testWidgets
function to access the WidgetTester object. Once inside the test body, we control the
behavior of the mock bloc by returning the desired state, which will determine the ren-
dered widgets. Moreover, we pump the application by leveraging the previously described
extension method, pumpWaterLeakAlarmDetailsView, providing the widget under test,
WaterLeakAlarmDetailsView, access to the mock bloc, MockWaterLeakAlarmBloc. Lastly,
the expect function validates the test’s result by leveraging find.byType (Finder) and
findsOneWidget (Matcher), allowing developers to assert that the Finder locates ex-
actly one widget in the widget tree.
23 testWidgets(
24 'CircularProgressIndicator when FetchedDevicesStatus.loading',
25 (WidgetTester tester) async {
26 when(() => waterLeakAlarmBloc.state).thenReturn(
27 const WaterLeakAlarmState(
28 valve: valve,
29 sensors: sensors,
30 fetchedDevicesStatus: FetchedDevicesStatus.loading,
31 ),
32 );
33 await tester.pumpWaterLeakAlarmDetailsView(
34 waterLeakAlarmBloc: waterLeakAlarmBloc,
35 );
36 expect(
37 find.byType(CircularProgressIndicator),
38 findsOneWidget,
39 );
40 },
41 );
42 });
78 2| Implementation
shows
Another typical use case for widget testing is to check whether the app shows a specific
widget. For this purpose, we use a similar approach to the one presented before, but
we introduce a new helper function provided by bloc_test, whenListen. This function
creates a stub response for the listen method on a given bloc, allowing to return a canned
Stream of states for a bloc instance. Moreover, whenListen also handles stubbing the
bloc’s state, keeping it in sync with the emitted state. Lastly, attempt to find the desired
widget byKey, this time.
adds
This third example covers a test case where developers need to verify whether a specific
function has been called. For this purpose, we introduce another widget, WaterSystemOk-
2| Implementation 79
Banner. Thereby, we find two essential differences when compared to the previous widget
tests. Firstly, we use the instance of WidgetTester, tester, to act on a specific widget,
found byKey, by tapping on it. Secondly, once the action completes successfully, we must
verify that the function under test, waterLeakAlarmBloc.add(const WaterLeakAlarmOk-
StatusBannerClose()), has been called exactly once. Ultimately, the WidgetTester
class exposes numerous methods to interact with widgets and the test environment pro-
grammatically, allowing developers to test all sorts of actions on a given widget.
5 void main() {
6 group('WaterSystemOkBanner', () {
7 late WaterLeakAlarmBloc waterLeakAlarmBloc;
8
9 setUp(() {
10 waterLeakAlarmBloc = MockWaterLeakAlarmBloc();
11 });
12
13 group('adds', () {
14 testWidgets(
15 'tap close button hides banner',
16 (WidgetTester tester) async {
17 when(() => waterLeakAlarmBloc.state).thenReturn(
18 const WaterLeakAlarmState(
19 displayOkStatusBanner: true,
20 ),
21 );
22 await tester.pumpBanner(
23 waterLeakAlarmBloc: waterLeakAlarmBloc,
24 );
25
26 await tester.tap(
27 find.byKey(const Key('waterSystemOkBanner_closeButton')),
28 );
29 verify(
30 () => waterLeakAlarmBloc.add(
80 2| Implementation
31 const WaterLeakAlarmOkStatusBannerClose(),
32 ),
33 ).called(1);
34 },
35 );
36 });
37 });
38 }
39
40 extension on WidgetTester {
41 Future<void> pumpBanner({
42 required WaterLeakAlarmBloc waterLeakAlarmBloc,
43 }) =>
44 pumpApp(
45 BlocProvider.value(
46 value: waterLeakAlarmBloc,
47 child: const WaterSystemOkBanner(),
48 ),
49 );
50 }
One final comment about the exemplified cases about widget testing shown in this sub-
section is that it would be remarkably challenging to test all the widgets in an appli-
cation without following a coherent and unambiguous architectural design, such as the
Page-View pattern, as we could not inject the necessary dependencies to gained the
indispensable controllability over any given widget test case.
2.4.5. Remarks
After reviewing various insightful concepts and examples about testing the different lay-
ers and components of a large-scale Flutter application, we conclude this section with
a few final remarks. Firstly, let us emphasize the importance of employing a standard-
ized, predictable, and systematic approach when implementing and testing features. This
methodological procedure should allow developers to build a reliable, easy-to-navigate,
and sound codebase for any large-scale Flutter application, enhancing its maintenance,
scalability, and testability.
Moreover, tests files and directories should always mirror the structure of the implemen-
tation project. Therefore, whether the tests belong to a domain or data layer package, or
the business logic or presentation layer, there must be a test directory at the same level as
2| Implementation 81
the folder containing the application implementation files, the lib folder, in Dart/Flutter
projects. Additionally, these directories should have the same name both in the test and
implementation directories, while test files should add the suffix _test to the name of its
corresponding implementation file.
Lastly, we encourage using graphical tools that allow developers to track the testing
coverage of any given file, package, or project. Thus, we propose using lcov25 , a graphical
front-end for GCC’s coverage testing tool that collects gcov26 data for multiple source files
and creates HTML pages containing the source code annotated with coverage information.
More importantly, it supports statement, function, and branch coverage measurement and
provides overview pages for easy navigation within the file structure.
25
http://ltp.sourceforge.net/coverage/lcov.php
26
https://gcc.gnu.org/onlinedocs/gcc/Gcov.html
83
3| Results
This chapter navigates the reader through the results derived from the implementation
endeavors presented in the previous chapter 2. Thereby, it provides quantitative and
qualitative data supporting the objectives of this thesis.
packages
The completion of this project required developers to implement 40 Dart packages. Out
of all these packages, 12 correspond to data clients representing the application’s data
layer, 19 correspond to domain repositories representing the application’s data layer, 2
correspond to plugins, and the remaining 7 correspond to utility or helper packages. It is
worth mentioning that the api_client, one of the core data packages, exposed 8 resources
which laid the foundation for another 8 domain repositories. Lastly, Table 3.1 below
display further quantitative information related to client and repository packages. Notice
the lines column does not refer to the number of lines implemented in a given package
but its number of testable lines belonging to statement, branch, and path code.
lib
Let us now focus on the business logic and presentation layers, the upper layers of the
proposed architecture encompassed by the lib directory. To deliver the desired value to
end-users, developers implemented a total of 123 Application features. Most importantly,
the application leveraged 69 distinct blocs that manage the state of the Flutter applica-
tion and required 488 dedicated bloc tests to thoroughly cover all the business logic and
functionality exposed by all the blocs. Lastly, testing the application’s presentation layer
involved 1306 widget tests which included, but were not limited to, all the rendering,
navigation, and function-calling use cases. Lastly, Table 3.2 below displays a summarized
quantitative data analysis related to the contents of the lib directory.
App Features Blocs Bloc Tests Widget Tests Total App Tests Covered lines
123 69 488 1306 2014 13247
Totals
This short subsection aims to provide a concise yet precise view of the previously presented
data. Thus, Table 3.3 below illustrates the data aggregation of the quantitative values
previously collected and reviewed.
• Óscar Martin - Senior Software Engineer I at Very Good Ventures, Project Lead,
and Flutter Spain co-founder. He was directly involved in the project.
• Jaime Blasco - Software Engineer II at Very Good Ventures, Google Developer
Expert for Flutter, and Flutter Spain co-founder. He was directly involved in the
project.
• Jorge Coca - Head of Engineering at Very Good Ventures, Google Developer Expert
for Flutter, and organizer of Chicago Flutter.
• Dominik Roszkowski - Principal Engineer at Very Good Ventures and Google De-
veloper Expert for Flutter.
Let us now present the gathered data.
Code Quality
Both Óscar and Jaime evaluated the quality of the proposed App’s codebase with four
out of five maximum points. Óscar pointed out that the App has some large features
containing nested sub-features, which increased the overall complexity of the application.
However, the overall consensus was that the codebase’s code quality was notably high.
The App features clearly-differentiated domains, and classes and methods are predictable
while having a single responsibility. Moreover, it is worth noting that we "kept the
majority of the features independent and could be modularized in the future" based on
requirements and specific app constraints.
"This is something very subjective, but a great application is read easily, and its
features can be quickly located. From an objective point of view, a well-tested app
(100% is our standard) that runs on CI/CD every code change tends to be a very
important indicator of quality."
— Jorge Coca, on code quality in Flutter Apps.
Architecture
All surveyed engineers raised concerns about having either purely layered or feature-
oriented architectures. Regarding purely layered architectures, they stated that scalabil-
ity was an evident problem as adding or modifying features require developers to adjust
multiple files and directories, creating version control conflicts in a distributed software
environment. On the other hand, purely feature-oriented architectures add a level of com-
plexity that makes feature definition, reusability, directory organization non-trivial. More
specifically, achieving fully independent features that share resources without duplicating
code and poorly affecting maintainability often requires heuristics and a non-standardized
approach. Moreover, Jorge defined modularization as the is simply the art of organizing
86 3| Results
State Management
The survey asked the respondents to state which state management solutions they had
used in the past. See the table below to check the responses.
0
d
r
vide
der
erpo
x
X
It
X
c
u
Mob
Blo
Get
Get
Red
Bin
Pro
Riv
Advantages:
• Easier to modularize and to know the current state of your application.
• It handles the states through a stream of events that modify the current state,
allowing developers to know which state the application is currently in and test it
accordingly.
• Bloc is consistent, predictable, easy to test, hard to break, and has a strong com-
munity that shares tutorials, documentation, plugins, articles, videos...
• It is made for testing.
• It has built-in observability of the events and states.
3| Results 87
• It provides constraints on the app modules’ structure and architecture, making blocs
look similar to each other.
Disadvantages:
• It requires a new development mindset since most developers learn declarative pro-
gramming and not state machine drive architectures.
• Some systems are hard to define as state machines. For instance, bloc can be
counterproductive for background processing e.g. especially when a given process
needs to be repeated for some number of objects.
• Hard to learn for beginners.
• Some UI components or interactions do not fit well within the state/event paradigm
(eg: showing Snack Bars)
Moreover, their level of satisfaction towards the BLoC pattern and the bloc library re-
ceived an average of four-point-five out of five maximum points. Overall, Jorge pointed out
that the Bloc pattern is the most suitable pattern to handle state management in Flutter
applications. Additionally, he emphasized that BLoC acknowledges the reactive nature
of applications’ UIs and that it helps developers think of every possible combination of
states a UI may handle, making it very easy to test.
out any given development sprint and that the overall application architecture facilitates
module and feature testing. However, Jaime pointed out that there is room for improve-
ment by including golden tests that ensure the UI aspect and behavior, and integration
tests that checked the entire application flow.
"At the end of the day, 100% coverage guarantees confidence and reduces the costs of
development by enforcing rules and standards in an automated way."
— Jorge Coca, on why Very Good Ventures emphasizes 100% coverage on all projects.
As far as maintainability, Jaime stated that the implemented codebase has undergone a
continuous evolution allowing developers to adapt to changing requirements. However,
technical debt arose along the way, affecting specific parts or features of the application,
such as inconsistencies in the application theming. Furthermore, they agreed that the
modularization approach implemented throughout the application architecture facilitates
the maintainability of features and modules. Overall, they rated the maintainability of
the application’s codebase with four-point-five out of a maximum of five scores.
Lastly, Óscar and Jaime rated the scalability of the application’s codebase with five out
of a maximum of five points. They stated that the implemented application features a
clear division between layers and domains. Moreover, all dependencies shaping any given
feature are constrained by the defined app requirements, while each feature is scoped, al-
lowing developers to keep growing the codebase with new requirements without negatively
impacting the previous code. However, Jaime emphasized the importance of reviewing
features independently to further improve their modularity.
Flutter
Lastly, the survey prompted the respondents to give feedback and opinions about Flut-
ter. Óscar and Jaime focused on the productivity and efficiency benefits using Flutter
provides to developers. Based on their experience, they confirmed that development is
faster, allowing developers to focus on the product instead of on the platform where it
needs to be deployed. Moreover, they mentioned the numerous benefits of having a sin-
gle team developing one application targetting various platforms, rather than multiple
teams managing various applications depending on the target platform. Thus, Flutter
development presents evident benefits in terms of team performance and costs. Jorge also
pointed out that, although other cross-platform frameworks achieved being able to write
once and deploy in more platforms, there was always a penalty price that someone had to
pay, whether it was performance (final users), developer experience (employees), or lack
of a strong community that elevated the practice (organization). However, the surveyed
engineers also pointed out relevant drawbacks worth considering, such as:
90 3| Results
• Flutter has to fight against the pre-existing stigma that cross-platform development
is slow, bad, or does not scale.
• Dart is not a popular language and makes organizations reluctant about using it for
industry projects.
• Dart is less powerful than Kotilin or Swift
• Flutter has limited access to system APIs (background processing, camera access
and control)
• Flutter is not the official supported approach to developing Android and iOS apps.
"I think we will see Flutter running everywhere: it will start with Toyota and their
vehicles, and I am sure other vehicle manufacturers will follow... but I think of stadium
Jumbotrons, Times Square, smart devices at home... anywhere where there is a screen!"
— Jorge Coca, on the future of Flutter, its ecosystem, and community.
91
4| Related Work
This chapter wraps up the work presented in this thesis by providing the reader with addi-
tional insights into other authors’ academic endeavors related to the knowledge presented
in this document. It reviews a series of research and thesis papers focused on software
architecture, state management solutions, and testability in Flutter applications. Fur-
thermore, the following academic contributions allowed this thesis’ author to assemble
a sizable portion of the content included in the Background and Motivations chapter 1.
Ultimately, they served as a solid theoretical and empirical foundation for this thesis,
inspiring its author throughout the research and implementation phases of the proposed
work.
Sebastian Faust’s thesis documented the crucial steps most development teams may face
using Flutter in a large-scale application [31]. His work included the creation of a large-
scale application used as a reference to introduce the steps taken during its development,
providing a thorough review of the decisions and evaluated options shaping this process.
Furthermore, he shared comprehensive insights into the wide range of explored, com-
pared, and analyzed solutions about state management and software architecture. His
work covered valuable topics such as an extensive review of the Flutter framework, im-
mutability, dependency injection, and modularization, among other content. Moreover,
his thesis shares an interview with Felix Angelov, the current Head of Architecture and
Principal Engineer at Very Good Ventures, to support his decisions and implementation
regarding state management and application architecture. He essentially built an arguably
large-scale Flutter application leveraging a four-tier architecture, the BLoC pattern as the
chosen solution for state management, and Dart packages to achieve enhanced modular-
ization. Nonetheless, the size of his application and his understanding of "a large-scale
application" fade compared to the work presented in this thesis. Moreover, he did not
reference testing as a core pillar of software development based on its effects on software
maintainability and scalability. Hence, his work lacks references to automated testing
covering unit, bloc, and widget tests cases.
Moreover, Michał Szczepanik and Michał Kędziora [64], Ly Hong Hoang [39], Dmitrii
Slepnev [59] contributed to the Flutter literature by furthering the analysis and review
of state management solutions employed in applications leveraging this cross-platform
92 4| Related Work
framework, being the latter author who provided the most comprehensive work about
such solutions. Dimitrii’s thesis focused on categorizing state management approaches and
provided a means to select the most suitable solution for the most common use cases. His
research efforts relied on quantitative analysis about mobile app development, including
its market, underlying operating systems, and available options, a thorough review of
the Flutter framework and Dart, and a comprehensive analysis of state management
approaches. Thereby, he used the learned knowledge to implement a Flutter application
leveraging each of the most representative state management solutions, including setState,
InheritedWidget, Provider, GetX, BLoC, MobX, Redux. Ultimately, he based his analysis
and validation criteria on the following six aspects: Complexity, Boilerplate code, Code
generation, Time travel, Scalability, and Testability
His analysis led him to conclude that BLoC and its Flutter implementation with
flutter_bloc were the most suitable choice to build a highly scalable and testable Flutter
application, aligning with the state management solution proposed in this thesis.
Lastly, it is worth noting that there is a scarce number of academic articles fo-
cused on Flutter. Therefore, this thesis refers to trusted, genuine, and accurate content
elaborated by influential and renowned figures in the Flutter community to complement
the scientific papers composing this work’s bibliography. Accordingly, we cited numerous
official sources to include the utmost rigorous and precise knowledge on subjects like
Flutter, Dart, state management, or bloc, which made up for the lack of academic papers.
93
5| Conclusions
This thesis intended to provide a standardized and almost systematic approach to build-
ing large-scale Flutter applications. Firstly, we proposed a hybrid architecture combining
the strengths and advantages of the layered and feature-oriented architectural design pat-
terns. Furthermore, we enhanced this hybrid architecture by complementing it with a
modularization approach based on Dart packages. Regarding the selected state manage-
ment solution, we provided coherent criteria to support the choice of the BLoC pattern
and its Flutter implementation with flutter_bloc. We also introduced the Page-View
pattern, a straightforward approach to inject blocs into the presentation layer and sim-
plify the access to the state from any given Widget. Moreover, this thesis emphasized
the importance of testing as a core activity of the software development lifecycle while
enforcing one hundred percent code coverage for the entire application’s codebase, demon-
strating its positive effects on the long-term maintainability and scalability of any given
Flutter application. Accordingly, this thesis illustrated all the non-trivial decisions and
implementation details through general, transferable, and comprehensive code examples.
Most importantly, the results derived from this thesis work support the decisions taken
throughout the completion of the Flutter application’s implementation phase. From a
quantitative perspective, the results validated our proposal as we were able to deliver
a large-scale application meeting, or exceeding, the expectations about the quality of
the product delivered to end-users. On the other hand, the qualitative data obtained
through testimonials and observations of Flutter experts, directly and indirectly, involved
in the project’s development also backed the thesis author’s proposed work. Ultimately,
94 5| Conclusions
this thesis contributed to the existing yet limited literature about the Flutter framework
by introducing comprehensive research knowledge, which gathered and aggregated
information from numerous reliable sources. It is also worth mentioning that the size
of the project this thesis builds upon corresponds to an undoubtedly large-scale Flutter
application. Lastly, unlike many other related studies, its development, testing, and
validation were carried out in a professional, industry-oriented environment involving
real-world stakeholders.
Before concluding this thesis, we propose the following relevant topics that could
extend the work presented in this document, its line of study, and further contribute to
the Flutter literature:
• Enhance the test suite of this Flutter application, or any other large-scale Flutter
application, by including golden tests and integration testing. The analysis and
implementation of these two test types should provide valuable insights into their
implications on a given App’s maintainability, scalability, and testability.
• Expand this work to evaluate the multi-platform performance of the proposed ar-
chitecture and state management solution compared to other approaches.
• Build a fully modular Flutter application where any given feature could run inde-
pendently, possibly leveraging a micro-service architecture, allowing total control
over feature addition and deletion, and maximizing its reusability across different
applications.
95
Bibliography
[1] Microsoft Application Architecture Guide: Patterns & Practices. Microsoft, 2nd
edition, 2009.
[10] K. Beck. Smalltalk Best Practice Patterns, volume 1 of Coding. Prentice Hall, 1997.
[11] J. C. Bender and J. McWherter. Professional test driven development with C#:
Developing real world applications with TDD. Wiley, 2011.
[13] D. Boelens. Reactive programming - streams - bloc. aug 2018. URL https://www.
didierboelens.com/2018/08/reactive-programming-streams-bloc/.
96 | Bibliography
[15] J. Bosch and P. Molin. Software architecture design: evaluation and transformation.
pages 4 – 10, 04 1999. ISBN 0-7695-0028-5. doi: 10.1109/ECBS.1999.755855.
[16] T. A. M. Christoph Rieger. Towards the definitive evaluation framework for cross-
platform app development approaches. 153:175–199, Apr. 2019. ISSN 0164-1212.
doi: 10.1016/j.jss.2019.04.001. URL https://www.sciencedirect.com/science/
article/pii/S0164121219300743.
[17] J. Coca. Why we use flutter_bloc for state management, jun 2021. URL https:
//verygood.ventures/blog/why-we-use-flutter-bloc.
[18] G. Developers. Flutter live - flutter announcements and updates (livestream), 2018.
URL https://www.youtube.com/watch?v=NQ5HVyqg1Qc&t=4842s.
[31] S. Faust. Using google´s flutter framework for the development of a large-scale
reference application. 2020.
[32] fireup.pro team. 9 amazing mobile apps built with react native, 2021. URL https:
//fireup.pro/blog/9-amazing-mobile-apps-built-with-react-native.
[36] T.-M. Grønli, J. Hansen, G. Ghinea, and M. Younas. Mobile application platform
heterogeneity: Android vs windows phone vs ios vs firefox os. In 2014 IEEE 28th In-
ternational Conference on Advanced Information Networking and Applications, pages
635–641, 2014. doi: 10.1109/AINA.2014.78.
[39] L. H. Hoang. State management analyses of the flutter application, nov 2019.
[40] P. Hunt. React: Rethinking best practices – jsconf eu, oct 2013. URL https:
//youtu.be/x7cQ3mrcKaY.
[41] IEEE. Ieee standard glossary of software engineering terminology. 1990. doi: 10.
1109/ieeestd.1983.7435207.
[43] W. Leler. What’s revolutionary about flutter, aug 2017. URL https://hackernoon.
com/whats-revolutionary-about-flutter-946915b09514.
[45] S. Liu. Most used libraries and frameworks among developers, worldwide,
as of 2021, 2021. URL https://www.statista.com/statistics/793840/
worldwide-developer-survey-most-used-frameworks/.
[47] T. Mackinnon, S. Freeman, and P. Craig. Endo-Testing: Unit Testing with Mock
Objects, page 287–301. Addison-Wesley Longman Publishing Co., Inc., USA, 2001.
ISBN 0201710404. doi: 10.5555/377517.377534.
[61] P. Soares. Flutter / angulardart – code sharing, better together (dartconf 2018).
Google Developers, jan 2018. URL https://youtu.be/PLHln7wHgPE.
[62] S. Stoll. In plain english: So what the heck is flutter and why is it
a big deal?, may 2018. URL https://medium.com/flutter-community/
in-plain-english-so-what-the-heck-is-flutter-and-why-is-it-a-big-deal-7a6dc926
[64] M. Szczepanik. and M. Kędziora. State management and software architecture ap-
proaches in cross-platform flutter applications. In Proceedings of the 15th Inter-
national Conference on Evaluation of Novel Approaches to Software Engineering -
ENASE,, pages 407–414. INSTICC, SciTePress, 2020. ISBN 978-989-758-421-3. doi:
10.5220/0009411604070414.
100 5| BIBLIOGRAPHY
[69] N. team. What is flutter and why use flutter for app
development, 2020. URL https://nix-united.com/blog/
the-pros-and-cons-of-flutter-in-mobile-application-development/.
[70] V. Team. Top companies using flutter in 2021, 2021. URL https://verygood.
ventures/blog/top-companies-using-flutter-2021.
[73] M. Veng. Dependency Injection and Mock on Software and Testing. PhD thesis,
2014. URL http://urn.kb.se/resolve?urn=urn:nbn:se:uu:diva-226214.
[74] S. Wardrop. Best practices for building scalable flutter applications, dec 2020. URL
https://verygood.ventures/blog/scalable-best-practices.
[75] S. Wardrop. Flutter testing: A very good guide [10 insights], feb 2021. URL https:
//verygood.ventures/blog/guide-to-flutter-testing.
[76] L. Wei, Y. Liu, and S.-C. Cheung. Taming android fragmentation: Characterizing
and detecting compatibility issues for android apps. In 2016 31st IEEE/ACM In-
ternational Conference on Automated Software Engineering (ASE), pages 226–237,
2016.
[77] H. Zhu, P. A. V. Hall, and J. H. R. May. Software unit test coverage and adequacy.
ACM Comput. Surv., 29(4):366–427, dec 1997. ISSN 0360-0300. doi: 10.1145/267580.
267590. URL https://doi.org/10.1145/267580.267590.
101
31 Permission.camera.request();
32
37 /// Request access to look for Bluetooth devices (e.g. BLE peripherals)
38 /// in Android(iOS: Nothing) if access hasn't been previously granted
39 Future<PermissionStatus> requestBluetoothScan() => _platform.isAndroid
40 ? Permission.bluetoothScan.request()
41 : Future.value(PermissionStatus.granted);
42
62 /// Opens to the app settings page, allowing the user to change previously
63 /// denied permissions
64 ///
65 /// Returns true if the settings could be opened, otherwise false
66 Future<bool> openPermissionSettings() =>
67 openAppSettings();
68
69 /// Checks the device's bluetoothScan and location permissions for Android.
A| Appendix - Source Codes 103
70 ///
71 /// It performs a no-op for iOS devices.
72 Future<PermissionStatus>
73 checkPlatformSpecificPermissionsForBluetooth() async {
74 if (_platform.isAndroid) {
75 final bluetoothScan = await requestBluetoothScan();
76 final location = await requestLocation();
77 return (bluetoothScan.isGranted && location.isGranted)
78 ? PermissionStatus.granted
79 : PermissionStatus.denied;
80 } else {
81 return Future.value(PermissionStatus.granted);
82 }
83 }
84
33 BehaviorSubject<T>? __subject;
34
55
94 cancelOnError: cancelOnError,
95 );
96 }
97 }
98
124 completer.complete();
125 }
126 }
127
172 }
173 }
174 }
175
4 class HomeDetailsBloc
5 extends Bloc<HomeDetailsEvent, HomeDetailsState> {
6 HomeDetailsBloc({
A| Appendix - Source Codes 109
21 FutureOr<void> _fetchRequested(
22 HomeDetailsFetchRequested event,
23 Emitter<HomeDetailsState> emit,
24 ) async {
25 _homeSubscription = _homeRepository.homes.listen((homes) {
26 final home = homes.values.firstWhereOrNull(
27 (home) => home.id == _homeId,
28 );
29 if (home != null) {
30 add(HomeDetailsHomeLoaded(home: home));
31 }
32 });
33
34 await emit.forEach<HomeAvatarUpdatedPayload>(
35 _homeRepository.homeAvatarUpdated(homeId: _homeId),
36 onData: (payload) => state.copyWith(
37 home: state.home?.copyWith(avatar: payload.avatar),
38 ),
39 );
40 }
41
42 FutureOr<void> _homeLoaded(
43 HomeDetailsHomeLoaded event,
44 Emitter<HomeDetailsState> emit,
45 ) async {
110 A| Appendix - Source Codes
46 try {
47 final homeInfo = await _homeRepository.getHomeInfo(_homeId);
48 final avatar = await _homeRepository.getAvatarUrls(_homeId);
49 final home = event.home.copyWith(homeInfo: homeInfo, avatar: avatar);
50 emit(state.copyWith(home: home, status: HomeDetailsStatus.loaded));
51 } catch (e) {
52 emit(state.copyWith(status: HomeDetailsStatus.loadingFailed));
53 }
54 }
55
56 @override
57 Future<void> close() {
58 _homeSubscription?.cancel();
59 _avatarSubscription?.cancel();
60 return super.close();
61 }
62 }
30 @override
31 Widget build(BuildContext context) {
32 final l10n = context.l10n;
33 return CustomLoadingOverlay(
34 loadingWhen: (context) {
35 return context.select(
36 (DeleteHomeBloc b) =>
37 b.state.status == DeleteHomeStatus.inProgress,
38 );
39 },
40 child: Scaffold(
41 appBar: CustomAppBar(
42 title: l10n.homeDetailsPageTitle,
43 color: CustomColors.lightWater,
44 actions: [
45 Padding(
46 padding: const EdgeInsets.only(right: 20),
47 child: IconButton(
48 key: const Key(
49 'homeDetails_customAppBar_options_iconButton',
50 ),
51 icon: CustomIcons.icDotsThreeOutline(size: 25),
52 onPressed: () =>
53 openHomeOptionsBottomSheet(context),
54 ),
55 ),
56 ],
57 ),
58 body: const HomeDetailsView(),
59 ),
60 );
112 A| Appendix - Source Codes
61 }
62 }
6 @override
7 List<Object?> get props => [];
8 }
9
17 @override
18 List<Object> get props => [name];
19 }
20
26 @override
27 List<Object?> get props => [area];
28 }
29
5 required this.originalDevice,
6 required this.editedDevice,
7 this.saveStatus = EditDeviceSaveStatus.unknown,
8 this.areas,
9 });
10
21 @override
22 List<Object?> get props => [
23 originalDevice,
24 editedDevice,
25 saveStatus,
26 areas,
27 ];
28
29 EditDeviceState copyWith({
30 EditedDevice? originalDevice,
31 EditedDevice? editedDevice,
32 EditDeviceSaveStatus? saveStatus,
33 List<Area>? areas,
34 }) {
35 return EditDeviceState(
36 originalDevice: originalDevice ?? this.originalDevice,
37 editedDevice: editedDevice ?? this.editedDevice,
38 saveStatus: saveStatus ?? this.saveStatus,
39 areas: areas ?? this.areas,
40 );
41 }
42 }
43
114 A| Appendix - Source Codes
44 enum EditDeviceSaveStatus {
45 unknown,
46 loading,
47 success,
48 failed,
49 }
31 FutureOr<void> _areasRequested(
32 EditDeviceAreaRequested event,
A| Appendix - Source Codes 115
33 Emitter<EditDeviceState> emit,
34 ) async { ... }
35
36 FutureOr<void> _deviceNameUpdated(
37 EditDeviceNameUpdated event,
38 Emitter<EditDeviceState> emit,
39 ) { ... }
40
41 FutureOr<void> _deviceAreaUpdated(
42 EditDeviceAreaUpdated event,
43 Emitter<EditDeviceState> emit,
44 ) { ... }
45
46 Future<void> _saveRequested(
47 EditDeviceSaveRequested event,
48 Emitter<EditDeviceState> emit,
49 ) async { ... }
50 }
4 @override
5 Widget build(BuildContext context) {
6 final l10n = context.l10n;
7
21 }
22 },
23 child: Scaffold(
24 appBar: CustomAppBar( ... ),
25 body: const Padding(
26 padding: EdgeInsets.symmetric(
27 horizontal: CustomSpacing.lg,
28 ),
29 child: ScrollableColumn(
30 children: [
31 _DeviceNameTextField(),
32 SizedBox(height: CustomSpacing.lg),
33 _AreaDropdownButton(),
34 ],
35 ),
36 ),
37 ),
38 );
39 }
40 }
4 @override
5 Widget build(BuildContext context) {
6 final deviceName = context.select(
7 (EditDeviceBloc bloc) =>
8 bloc.state.editedDevice.deviceName,
9 );
10
19 EditDeviceNameUpdated(name),
20 ),
21 keyboardType: TextInputType.text,
22 errorText: deviceName.invalid ?
23 l10n.validationAddDeviceNameError
24 : null,
25 );
26 }
27 }
5 @override
6 Widget build(BuildContext context) {
7 final l10n = context.l10n;
8
17 expect(
18 () => alarmResource.alarms(''),
19 throwsA(isA<AlarmResourceException>()),
20 );
21 });
A| Appendix - Source Codes 119
22
39 expect(
40 () => alarmResource.alarms(''),
41 throwsA(isA<JsonParsingException>()),
42 );
43 });
44 });
45
61 json.decode(alarmsValidResponse)
62 as Map<String, dynamic>;
63 final alarmCollectionPayload = AlarmCollection
64 .fromJson(
65 responseJson['alarms'] as Map<String, dynamic>,
66 );
67
68 expect(
69 result,
70 equals(alarmCollectionPayload),
71 );
72 });
73 });
7 setUp(() {
8 apiClient = MockApiClient();
9 alarmResource = MockAlarmResource();
10 alarmRepository = AlarmRepository(apiClient);
11 when(() => apiClient.alarmResource).thenReturn(
12 alarmResource,
13 );
14 when(
15 () => alarmResource.alarms(any()),
16 ).thenAnswer(
17 (_) async => FakeAlarmCollection(),
18 );
19 when(
20 () => alarmResource.alarmUpdated(
21 homeId: any(named: 'homeId'),
22 ),
23 ).thenAnswer(
24 (_) => Stream.empty(),
25 );
A| Appendix - Source Codes 121
26 });
27 });
28 }
10 expect(
11 alarmRepository.alarms('homeId'),
12 emitsError(isA<AlarmResourceFailure>()),
13 );
14 },
15 );
16
17 test(
18 'throws AlarmResourceFailure on '
19 'alarmUpdated AlarmResourceFailure',
20 () {
21 when(
22 () => alarmResource.alarmUpdated(
23 homeId: any(named: 'homeId'),
24 ),
25 ).thenAnswer(
26 (_) => Stream.error(AlarmResourceFailure(null)),
27 );
28 expect(
29 alarmRepository.alarms('homeId'),
30 emitsInOrder(<dynamic>[
31 {alarm.id: alarm},
32 emitsError(isA<AlarmResourceFailure>()),
33 ]),
34 );
35 },
122 A| Appendix - Source Codes
36 );
37 });
38
39 test('fetches data', () {
40 expect(
41 alarmRepository.alarms('homeId'),
42 emits({alarm.id: alarm}),
43 );
44 });
45
46 test('updates data', () {
47 when(
48 () => alarmResource.alarmUpdated(
49 homeId: any(named: 'homeId'),
50 ),
51 ).thenAnswer(
52 (_) => Stream.value(
53 FakeAlarmUpdatedPayload(),
54 ),
55 );
56 expect(
57 alarmRepository.alarms('homeId'),
58 emitsInOrder(<dynamic>[
59 {alarm.id: alarm},
60 {alarm.id: updatedAlarm},
61 ]),
62 );
63 });
64 });
10 void main() {
11 group('WaterLeakAlarmAlarmBloc', () {
12 late DeviceRepository deviceRepository;
13 late AlarmRepository alarmRepository;
14
15 setUp(() {
16 deviceRepository = MockDeviceRepository();
17 alarmRepository = MockAlarmRepository();
18 });
19 });
20 }
13 const WaterLeakAlarmOkStatusBannerClosed(),
14 ),
15 expect: () => <WaterLeakAlarmState>[
16 WaterLeakAlarmState(),
17 ],
18 );
19 });
32 _alarmRepository = alarmRepository,
33 _deviceRepositoryResolver = deviceRepositoryResolver,
34 _hubRepositoryResolver = hubRepositoryResolver,
35 _areaRepositoryResolver = areaRepositoryResolver,
36 _homeMemberRepositoryResolver = homeMemberRepositoryResolver,
37 _contactsRepository = contactsRepository,
38 super(key: key);
39
59 @override
60 Widget build(BuildContext context) {
61 return MultiRepositoryProvider(
62 providers: [
63 RepositoryProvider.value(
64 value: _appConfigRepository,
65 ),
66 RepositoryProvider.value(
67 value: _connectivityRepository,
68 ),
69 RepositoryProvider.value(
70 value: _authenticationRepository,
126 A| Appendix - Source Codes
71 ),
72 RepositoryProvider.value(
73 value: _accountRepository,
74 ),
75 RepositoryProvider.value(
76 value: _localPhotosRepository,
77 ),
78 RepositoryProvider.value(
79 value: _bluetoothRepository,
80 ),
81 RepositoryProvider.value(
82 value: _wifiInfoRepository,
83 ),
84 RepositoryProvider.value(
85 value: _homeRepository,
86 ),
87 RepositoryProvider.value(
88 value: _smartHomeRepository,
89 ),
90 RepositoryProvider.value(
91 value: _alarmRepository,
92 ),
93 RepositoryProvider.value(
94 value: _contactsRepository,
95 ),
96 RepositoryProvider.value(
97 value: _deviceRepositoryResolver,
98 ),
99 RepositoryProvider.value(
100 value: _hubRepositoryResolver,
101 ),
102 RepositoryProvider.value(
103 value: _areaRepositoryResolver,
104 ),
105 RepositoryProvider.value(
106 value: _homeMemberRepositoryResolver,
107 ),
108 ],
109 child: MultiBlocProvider(
A| Appendix - Source Codes 127
110 providers: [
111 BlocProvider(
112 create: (_) => AppBloc(
113 appConfigRepository: _appConfigRepository,
114 authenticationRepository: _authenticationRepository,
115 isAuthenticated: _isUserAuthenticated,
116 ),
117 ),
118 BlocProvider(
119 create: (_) => AppMonitoringBloc(
120 authenticationRepository: _authenticationRepository,
121 accountRepository: _accountRepository,
122 appMonitoringRepository: _appMonitoringRepository,
123 isAuthenticated: _isUserAuthenticated,
124 shouldTrackData: kReleaseMode,
125 ),
126 lazy: false,
127 ),
128 BlocProvider(
129 create: (_) =>
130 AppStoreReviewBloc(isAppStoreReviewAvailable),
131 ),
132 BlocProvider(create: (_) => ThemeModeBloc()),
133 BlocProvider(
134 create: (_) => HomesBloc(_homeRepository)
135 ..add(
136 HomesFetchRequested(),
137 ),
138 ),
139 BlocProvider(create: (_) => HomeSelectionBloc())
140 ],
141 child: const AppView(),
142 ),
143 );
144 }
145 }
3 Widget widgetUnderTest, {
4 AppConfigRepository? appConfigRepository,
5 AppBloc? appBloc,
6 AppStoreReviewBloc? appStoreReviewBloc,
7 ConnectivityRepository? connectivityRepository,
8 TargetPlatform? platform,
9 ThemeModeBloc? themeModeBloc,
10 AuthenticationRepository? authenticationRepository,
11 AccountRepository? accountRepository,
12 HubRepository? hubRepository,
13 BluetoothRepository? bluetoothRepository,
14 HomesBloc? homesBloc,
15 HomeSelectionBloc? homeSelectionBloc,
16 HomeBloc? homeBloc,
17 DeleteHomeMemberBloc? deleteHomeMemberBloc,
18 WifiInfoRepository? wifiInfoRepository,
19 HomeRepository? homeRepository,
20 DeviceRepository? deviceRepository,
21 SmartHomeRepository? smartHomeRepository,
22 LocalPhotosRepository? localPhotosRepository,
23 AreaRepository? areaRepository,
24 AreaRepositoryResolver? areaRepositoryResolver,
25 AlarmRepository? alarmRepository,
26 DeviceRepositoryResolver? deviceRepositoryResolver,
27 HubRepositoryResolver? hubRepositoryResolver,
28 HomeMemberRepository? homeMemberRepository,
29 HomeMemberRepositoryResolver? homeMemberRepositoryResolver,
30 ContactsRepository? contactsRepository,
31 }) async {
32 registerFallbackValues();
33 await pumpWidget(
34 MultiRepositoryProvider(
35 providers: [
36 RepositoryProvider.value(
37 value: appConfigRepository
38 ?? MockAppConfigRepository(),
39 ),
40 RepositoryProvider.value(
41 value: connectivityRepository
A| Appendix - Source Codes 129
42 ?? MockConnectivityRepository(),
43 ),
44 RepositoryProvider.value(
45 value: authenticationRepository
46 ?? MockAuthenticationRepository(),
47 ),
48 RepositoryProvider.value(
49 value: accountRepository
50 ?? MockAccountRepository(),
51 ),
52 RepositoryProvider.value(
53 value: bluetoothRepository
54 ?? MockBluetoothRepository(),
55 ),
56 RepositoryProvider.value(
57 value: wifiInfoRepository
58 ?? MockWifiInfoRepository(),
59 ),
60 RepositoryProvider.value(
61 value: homeRepository
62 ?? MockHomeRepository(),
63 ),
64 RepositoryProvider.value(
65 value: smartHomeRepository
66 ?? MockSmartHomeRepository(),
67 ),
68 RepositoryProvider.value(
69 value: localPhotosRepository
70 ?? MockLocalPhotosRepository(),
71 ),
72 RepositoryProvider.value(
73 value: areaRepository
74 ?? MockAreaRepository(),
75 ),
76 RepositoryProvider.value(
77 value: alarmRepository
78 ?? MockAlarmRepository(),
79 ),
80 RepositoryProvider.value(
130 A| Appendix - Source Codes
81 value: deviceRepositoryResolver
82 ?? MockDeviceRepositoryResolver(),
83 ),
84 RepositoryProvider.value(
85 value: deviceRepository
86 ?? MockDeviceRepository(),
87 ),
88 RepositoryProvider.value(
89 value: hubRepositoryResolver
90 ?? MockHubRepositoryResolver(),
91 ),
92 RepositoryProvider.value(
93 value: areaRepositoryResolver
94 ?? MockAreaRepositoryResolver(),
95 ),
96 RepositoryProvider.value(
97 value: homeMemberRepositoryResolver ??
98 MockHomeMemberRepositoryResolver(),
99 ),
100 RepositoryProvider.value(
101 value: hubRepository ?? MockHubRepository(),
102 ),
103 RepositoryProvider.value(
104 value: homeMemberRepositoryResolver ??
105 MockHomeMemberRepositoryResolver(),
106 ),
107 RepositoryProvider.value(
108 value: homeMemberRepository
109 ?? MockHomeMemberRepository(),
110 ),
111 RepositoryProvider.value(
112 value: contactsRepository
113 ?? MockContactsRepository(),
114 ),
115 ],
116 child: MultiBlocProvider(
117 providers: [
118 BlocProvider.value(
119 value: appBloc ?? MockAppBloc(),
A| Appendix - Source Codes 131
120 ),
121 BlocProvider.value(
122 value: themeModeBloc
123 ?? MockThemeModeBloc(),
124 ),
125 BlocProvider.value(
126 value: appStoreReviewBloc
127 ?? MockAppStoreReviewBloc(),
128 ),
129 BlocProvider.value(
130 value: homesBloc
131 ?? MockHomesBloc(),
132 ),
133 BlocProvider.value(
134 value: homeSelectionBloc
135 ?? MockHomeSelectionBloc(),
136 ),
137 BlocProvider.value(
138 value: homeBloc ?? _MockHomeBloc(),
139 ),
140 ],
141 child: MaterialApp(
142 title: 'App',
143 localizationsDelegates: const [
144 AppLocalizations.delegate,
145 GlobalMaterialLocalizations.delegate,
146 GlobalWidgetsLocalizations.delegate,
147 GlobalCupertinoLocalizations.delegate,
148 ],
149 home: Theme(
150 data: ThemeData(platform: platform),
151 child: Scaffold(body: widgetUnderTest),
152 ),
153 ),
154 ),
155 ),
156 );
157 await pump();
158 }
132 A| Appendix - Source Codes
159 }
133
List of Figures
1 Cross-Platform Categorization . . . . . . . . . . . . . . . . . . . . . . . . . 2
2 Most used cross-platform mobile frameworks . . . . . . . . . . . . . . . . . 3
List of Tables
2.1 Flutter Test Classification . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
C| Acknowledgements
First and foremost, I would like to thank my family. My parents and brother are the
common factors behind all my successes and the main reason I avoided many failures.
They have always supported me in every decision I made along the way while keeping me
motivated to achieve my goals. They encouraged me to move to the U.S. to pursue my
dream of studying and playing sports at a university level for four years. They allowed
me to stay one extra year to work in Pittsburgh after graduating. And then again,
they supported me when I decided to move back to Europe to get a Master’s degree at
Politecnico di Milano. These selfless actions, among other countless ones, speak volumes
of the family I have and how far they are willing to go to give me a better life. It is an
understatement that I would not be where I am or be who I am without my family, and
graduating from Politecnico di Milano with a Master’s degree in Computer Science and
Engineering is yet another achievement that goes to my record, THANKS TO THEM.
There are no words in any language to describe how grateful and lucky I am to have
these people in my life.
I would also like to thank Very Good Ventures (VGV) and all the people that
work at this amazing company. I have nothing but words of gratitude towards VGV for
everything they have done for me ever since I started my internship with them. They
never fell short of arranging valuable guidance and assistance when required or providing
me with all the necessary resources to work on my thesis. Aside from all the technical
knowledge I learned through my internship, they instilled in me so many priceless values
and principles that I will forever carry with me and be grateful for. A special shout-out
goes to Jorge and Óscar for all the support they showed me from the very first interview
to this day.
How could I forget about il mio fratello italiano, Simone Staffa? I still think that
missing class that day was probably the luckiest thing that ever happened to me during
my days in Milano. I am positive that my experience at PoliMi would have not been
the same without you. It was a pleasure and an honor to learn from you, be in the
same class with you, and work together on so many successful projects. All these ad-
ventures will go down in history, and we now both have a degree to prove it... Grazie mille!
Last but not least, had it not been for my classmates and friends Gianluca and
Benjamino, I would probably still be stuck in that Algebra Logic class. I owe you some
of the happiest moments I can remember while studying or attending class and during
the toughest moments we endured during the pandemic. Thank you for the memories
and that delicious pasta!