The time for putting all of our previous work into practice is finally here! This part will be all about building the UI and splitting it into multiple readable and maintainable Widget
s.
If you need a refresher on how the UI looks like...
Creating a Page
The number trivia feature (and in turn the whole app) will have a single page, which will be a StatelessWidget
called NumberTriviaPage
. Let's just create a blank version of it for now.
.../presentation/pages/number_trivia_page.dart
class NumberTriviaPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Trivia'),
),
);
}
}
All this time the main.dart file contained the example counter app. Let's change that! We're going to delete everything and create pristine MaterialApp
with some basic green (Reso Coder style ?) theming. Of course, the home
widget will be the NumberTriviaPage
.
main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Number Trivia',
theme: ThemeData(
primaryColor: Colors.green.shade800,
accentColor: Colors.green.shade600,
),
home: NumberTriviaPage(),
);
}
}
Getting the Presentation Logic Holder
In Clean Architecture, the only communication pathway between UI widgets and the rest of the app is the presentation logic holder. Number Trivia App uses Bloc, but as I'm saying probably for the 100th time, you can use anything from Change Notifier to MobX.
Regardless of your preferred state management method, you still need to provide your presentation logic holder throughout the widget tree. For that, you can obviously use the provider package! Since we're using Bloc which is integrated with provider, we're going to use a special BlocProvider
which has some nice Bloc-specific features.
The NumberTriviaBloc
has to be available to the whole body
of the page's Scaffold
. This is where we will get the registered NumberTriviaBloc
instance from the service locator, which will in turn kick off all of the lazy singletons registered in the previous part.
number_trivia_page.dart
...
import '../../../../injection_container.dart';
import '../bloc/bloc.dart';
class NumberTriviaPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Trivia'),
),
body: BlocProvider(
builder: (_) => sl<NumberTriviaBloc>(),
child: Container(),
),
);
}
}
Building Out the Content
Before reacting to State
s emitted by the NumberTriviaBloc
, let's first build out a basic UI outline using Placeholder
widgets.
The root of the UI will be a padded and centered Column
comprised of two basic parts:
- The top half will be concerned with output. There will be a message containing either the trivia itself, some sort of an error or even the initial call to "Start searching!". The
CircularLoadingIndicator
will also be displayed in the top part. This part of the UI will be rebuilt whenever the state changes. - The bottom part will deal with input. It will hold a
TextField
and twoRaisedButton
s.
Outlining with Placeholders
Designing the UI layout with Placeholder
s is a perfect way to appropriately set the spacing and sizes of the widgets without having to think about their implementation. At this point, it will also be the best to extract the body of the Scaffold
into its own helper buildBody
method.
number_trivia_page.dart
class NumberTriviaPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Trivia'),
),
body: buildBody(context),
);
}
BlocProvider<NumberTriviaBloc> buildBody(BuildContext context) {
return BlocProvider(
builder: (_) => sl<NumberTriviaBloc>(),
child: Center(
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: <Widget>[
SizedBox(height: 10),
// Top half
Container(
// Third of the size of the screen
height: MediaQuery.of(context).size.height / 3,
// Message Text widgets / CircularLoadingIndicator
child: Placeholder(),
),
SizedBox(height: 20),
// Bottom half
Column(
children: <Widget>[
// TextField
Placeholder(fallbackHeight: 40),
SizedBox(height: 10),
Row(
children: <Widget>[
Expanded(
// Search concrete button
child: Placeholder(fallbackHeight: 30),
),
SizedBox(width: 10),
Expanded(
// Random button
child: Placeholder(fallbackHeight: 30),
)
],
)
],
)
],
),
),
),
);
}
}
Top Half - Displaying Data
The top half of the screen has to display different widgets depending on the State
outputted from the NumberTriviaBloc
. Loading
will display a progress indicator, Error
will, of course, display an error message etc. Building out different widgets according to the current state of the Bloc
is possible with a BlocBuilder
.
The initialState
of NumberTriviaBloc
is Empty
, so let's first handle that one by returning a simple Text
wraped inside a Container
to make sure it takes up a third of the screen height.
number_trivia_page.dart
...
// Top half
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return Container(
// Third of the size of the screen
height: MediaQuery.of(context).size.height / 3,
child: Center(
child: Text('Start searching!'),
),
);
}
// We're going to also check for the other states
},
),
...
MessageDisplay
Apart from the text being small, the Empty
state is handled. However, let's first clean up the mess we created by extracting the Container
into its own MessageDisplay
widget. We will use this widget to also display the error messages when the Error
state is emitted, so we have to make sure a String message
can be passed through the constructor.
In addition, we're going to add a bit of styling to make the text bigger and also scrollable using a SingleChildScrollView
. Otherwise, long messages would get cut off because of the Container
which has a limited height.
number_trivia_page.dart
...
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return MessageDisplay(
message: 'Start searching!',
);
}
},
),
...
class MessageDisplay extends StatelessWidget {
final String message;
const MessageDisplay({
Key key,
@required this.message,
}) : assert(message != null),
super(key: key);
@override
Widget build(BuildContext context) {
return Container(
// Third of the size of the screen
height: MediaQuery.of(context).size.height / 3,
child: Center(
child: SingleChildScrollView(
child: Text(
message,
style: TextStyle(fontSize: 25),
textAlign: TextAlign.center,
),
),
),
);
}
}
It's only natural to reuse this MessageDisplay
for messages coming from the Error
state as well. We'll expand the BlocBuilder
with an additional else if
clause:
number_trivia_page.dart
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return MessageDisplay(
message: 'Start searching!',
);
} else if (state is Error) {
return MessageDisplay(
message: state.message,
);
}
},
),
MessageDisplay
for the Loaded
state to display the actual NumberTrivia
, because we want to have the number displayed nicely on the top.LoadingWidget
Getting data from the remote API takes some time. That's why the Bloc emits a Loading
state and it's the responsibility of the UI to display a loading indicator. We already know that putting long-winded widgets directly into the BlocBuilder
hinders readability, so we're immediately going to extract the Container
into a LoadingWidget
.
number_trivia_page.dart
...
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return MessageDisplay(
message: 'Start searching!',
);
} else if (state is Loading) {
return LoadingWidget();
} else if (state is Error) {
return MessageDisplay(
message: state.message,
);
}
},
),
...
class LoadingWidget extends StatelessWidget {
const LoadingWidget({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height / 3,
child: Center(
child: CircularProgressIndicator(),
),
);
}
}
MediaQuery
. Fixating the height is useful to prevent the bottom half from "jumping around" when the length of the displayed message / trivia changes.TriviaDisplay
We're missing one "state-reaction" implementation for the most important state of them all - the Loaded
state which contains the NumberTrivia
entity which the user is interested in.
The TriviaDisplay
widget will be extremely similar to MessageDisplay
, but of course, it will take in a NumberTrivia
object throught the constructor. Also, TriviaDisplay
will be made of a Column
displaying two Text
widgets. One will display the number in a big font, the other one will display the actual trivia.
number_trivia_page.dart
...
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return MessageDisplay(
message: 'Start searching!',
);
} else if (state is Loading) {
return LoadingWidget();
} else if (state is Loaded) {
return TriviaDisplay(
numberTrivia: state.trivia,
);
} else if (state is Error) {
return MessageDisplay(
message: state.message,
);
}
},
),
...
class TriviaDisplay extends StatelessWidget {
final NumberTrivia numberTrivia;
const TriviaDisplay({
Key key,
this.numberTrivia,
}) : assert(numberTrivia != null),
super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height / 3,
child: Column(
children: <Widget>[
// Fixed size, doesn't scroll
Text(
numberTrivia.number.toString(),
style: TextStyle(
fontSize: 50,
fontWeight: FontWeight.bold,
),
),
// Expanded makes it fill in all the remaining space
Expanded(
child: Center(
// Only the trivia "message" part will be scrollable
child: SingleChildScrollView(
child: Text(
numberTrivia.text,
style: TextStyle(fontSize: 25),
textAlign: TextAlign.center,
),
),
),
)
],
),
);
}
}
Minor Refactoring
We've already done a lot to keep the code maintainable by creating custom widgets. Currently though, they are all inside the number_trivia_page.dart file, so let's just simply move them into their own files under the widgets folder.
The widgets.dart file is something called a barrel file. Since Dart doesn't support "package imports" like Kotlin or Java do, we have to help ourselves to get rid of a lot of individual imports with barrel files. It simply exports all of the other files present inside the folder.
widgets.dart
export 'loading_widget.dart';
export 'message_display.dart';
export 'trivia_display.dart';
And then, inside number_trivia_page.dart it's enough to import just the barrel file:
import '../widgets/widgets.dart';
Bottom Half - Receiving Input
All of the widgets we've made up to this point would be of no use unless the user could initiate fetching of random or concrete NumberTrivia
. Because we're using Bloc, this will happen by dispatching events.
Currently, the bottom half of the UI is full of Placeholder
s but even before replacing them with real widgets, let's separate out the whole bottom half Column
into a custom TriviaControls
stateful widget.
number_trivia_page.dart
class TriviaControls extends StatefulWidget {
const TriviaControls({
Key key,
}) : super(key: key);
@override
_TriviaControlsState createState() => _TriviaControlsState();
}
class _TriviaControlsState extends State<TriviaControls> {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
// Placeholders here...
],
);
}
}
Why stateful? TriviaControls
will have a TextField
and in order to deliver the inputted String
to the Bloc whenever a button is pressed, this widget will need to hold that String
as local state.
In addition to dispatch
ing the GetTriviaForConcreteNumber
event when the concrete button is pressed, this event will get dispatched also when the TextField
is submitted by pressing a button on the keyboard.
number_trivia_page.dart
class _TriviaControlsState extends State<TriviaControls> {
final controller = TextEditingController();
String inputStr;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TextField(
controller: controller,
keyboardType: TextInputType.number,
decoration: InputDecoration(
border: OutlineInputBorder(),
hintText: 'Input a number',
),
onChanged: (value) {
inputStr = value;
},
onSubmitted: (_) {
dispatchConcrete();
},
),
SizedBox(height: 10),
Row(
children: <Widget>[
Expanded(
child: RaisedButton(
child: Text('Search'),
color: Theme.of(context).accentColor,
textTheme: ButtonTextTheme.primary,
onPressed: dispatchConcrete,
),
),
SizedBox(width: 10),
Expanded(
child: RaisedButton(
child: Text('Get random trivia'),
onPressed: dispatchRandom,
),
),
],
)
],
);
}
void dispatchConcrete() {
// Clearing the TextField to prepare it for the next inputted number
controller.clear();
BlocProvider.of<NumberTriviaBloc>(context)
.dispatch(GetTriviaForConcreteNumber(inputStr));
}
void dispatchRandom() {
controller.clear();
BlocProvider.of<NumberTriviaBloc>(context)
.dispatch(GetTriviaForRandomNumber());
}
}
Of course, let's again put this widget into its own trivia_controls.dart file and then add that as an export
into the widgets.dart barrel file.
Last Fixes
It works! ? It works!? It works! ? Hooray! The user can finally input a number and get a concrete trivia or he can just press the random button and get some random trivia. There's one UI bug though which becomes immediately apparent when we open a keyboard:
This can be fixed very easily with a SingleChildScrollView
wrapping the whole body
of the Scaffold
. With that, whenever the keyboard appears and the body
shrinks in height, it will become scrollable and no overflow will happen.
number_trivia_page.dart
class NumberTriviaPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Trivia'),
),
body: SingleChildScrollView(
child: buildBody(context),
),
);
}
...
}
Are we done?
In these 14 parts of the Clean Architecture course, we've gone from an idea to a working app. You learned how to architect your apps into independent layers, how to do TDD, discovered the power of contracts, learned dependency injection and much more.
The Number Trivia App we've built may be on the simpler side when it comes to its functionality, what's important though are the principles. What you learned in this course is applicable to all kinds of circumstances and I believe you are now an overall better developer. Practice the principles you learned here and build something awesome!
Awesome series Matej !
I’ll redirect everyone that wants to learn Flutter over here.
Hello! First I would like to thank you for the excellent course! I followed all classes and did exactly the same, but in my “mapEventToState” class NumberTriviaBloc, is always returning the state Empty
Thank you Matej!
This type of course I was looking for Flutter development.
Keep up your good job done here!
How Should I navigate the user to new Widget ?
I meant when I got the response instead of showing the result as you are doing, I want to go somewhere else;
Thank you Matt for all your efforts in teaching us Clean Architecture with TDD and all your other Tutorials!
I am a newbie with flutter, and after learning the basics, will surely come back here for using clean architecture and TDD when developing the app to develop.
Thank you very much.
Your tutorial is really awesome and very helpful. I have one query, Is there any tutorial to learn how to test widgets with your bloc pattern?
Very helpful, but I have this error when trying to dispatch the event from BlocProvider:
BlocProvider.of() called with a context that does not contain a Bloc of type NumberTriviaBloc.
No ancestor could be found starting from the context that was passed to
BlocProvider.of().
Maybe something changed in the BLoC package?
Thanks
I’m not sure why that’s happening but it looks like you’re not providing the NumberTriviaBloc using a BlocProvider. I know this is probably not helpful but I don’t have any more info.
Thank you for this awesome tutorial, i have one question.
If we are using provider with chopper, de we need to pass the BuildContext every time through all layers to the data to calls Provider.of ?
Hi Matt, and thank you for your tutorial I have one problem, the loading state of the Bloc is not changing even the data is completely gotten.
Great tutorials, I really enjoyed your tutorial series. a lot of essential information in chunked concise format. Keep going
Hi, As soon as I replace the container with BlocBuilder, I get an error.
The _ScaffoldLayout custom multichild layout delegate forgot to lay out the following child:
_ScaffoldSlot.body: RenderPositionedBox#19002 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
parentData: offset=Offset(0.0, 0.0); id=_ScaffoldSlot.body
constraints: MISSING
size: MISSING
How did you solve this?
Just finished Clean Architecture TDD Series.
Thanks, very nice content.
Just one question if Entity/Model can be used across multiple Features, how should be the Folder structure inside Lib?
Just finished Clean Architecture TDD Series.
Thanks, Nice content. Thumbs up
One doubt if there is same entity/model which can be used in other Features, how should be the Folder Structure or Where it (Entity/Model) should be placed?
Man thanks for sharing your knowledge in this way. Indeed I am more confident to write a code! Thanx
what about null textString when nothing is given in textField it throws exception.
– In the TextField onSubmitted: (_) => (inputString.isNotEmpty) ? dispatchConcrete() : null,
– In the Search RaisedButton onPressed: (inputString.isNotEmpty) ? dispatchConcrete : null,
Thank you Matt for all your efforts in these tutorials!
Would you please tell us some open source projects which is following these principles?
I think it could be help to getting more experience on Clean Architecture design on flutter.
I love you and this architecture
<3
Thanks for your helpful content. But I get this error
error: The method ‘sl’ isn’t defined for the type ‘NumberTriviaPage’. (undefined_method at [clean_architecture] lib/features/number_trivia/presentation/pages/number_trivia_page.dart:23)
sl is not being recognised by the BlocProvider. Can you help me find a way around it. Thanks in advance.
I Got the same error, and just adding ‘()’ to the http.Client i solved
Like this——->> sl.registerLazySingleton(() => http.Client());
this line is on the injection_container.dart File.
If you already have it, check to see if there is any class that is not instantiated.
Hi everyone !
Firstly, I’d like to thank you for this amazing tutorials and your 7 hours video.
It was awesome, I learn so much ! Thank you very much, it’s a huge work !
I’d like to help others who have a problem loading data.
I’ve found that data_connection_checker library always return false when you are in debug mode (apparently it works on release mode).
To prevent this issue, I replace the code in the network_info.dart by:
import ‘dart:io’;
abstract class NetworkInfo {
Future get isConnected;
}
@override
class NetworkInfoImplementation implements NetworkInfo {
@override
Future get isConnected async {
try {
final list = await InternetAddress.lookup(‘google.com’);
return list.isNotEmpty && list[0].rawAddress.isNotEmpty;
} on SocketException catch (_) {
return false;
}
}
}
This way, you can prevent this issue and retrieve correctly your trivia number 🙂
Don’t forget to remove the test in the core folder, now useless.
You can remove also the data_connection_checker library in your pubspec.
And replace in the injection_container.dart:
sl.registerLazySingleton(() => NetworkInfoImplementation(sl()));
by:
sl.registerLazySingleton(() => NetworkInfoImplementation());
Moreover, don’t forget to add the
key, in your AndroidManifest.xml 😉
And that’s it, the project should work now !
Hope it will help you !
Thanks again for your contents, it’s really amazing!
Have a nice day!
I think that for security reason, the key doesn’t appear in my previous comment, you can find it here:
https://github.com/flutter/flutter/issues/20789
“Don’t forget to remove the test in the core folder, now useless.”
I mean, the the network_info_test.dart file in the core/network folder 🙂
Now useless because we remove the data_connection_checker library
I found errors when querying with the empty TextField, to fix it just add this:
– In the TextField onSubmitted: (_) => (inputString.isNotEmpty) ? dispatchConcrete() : null,
– In the Search RaisedButton onPressed: (inputString.isNotEmpty) ? dispatchConcrete : null,
Finally, I completed this course. (Alhamdulillah)
I did this course in August 2021. About 2 years later. So, I fetched many issues, bugs.
I think, it is difficult for beginners to do this course.
Whatever, I learned many things from this course. (I am intermediate Flutter developer. I had a problem, I couldn’t finish project. After finishing about half of project, I made noise, I became depressed.) But now, I am feeling confidence?. I understand how to do!!
Thanks Matt??
Thanks Matt for this awesome course!! It was very helpful for a beginner like me!
As well as @Prantik I finished it on 2021 and most of the libraries had some updates such as Dart 2.0, BLoC 7.3, get_it 7.2 and so on. It was challenging to make the project work but in the end I made it :). I also combined it with the injectable tutorial that you made and it works amazingly well ?
I leave you the link to my repository in case you all want to take a look on how to make this project work with new dependencies versions (until the day of this post).
Let me know what you think 🙂
https://github.com/jmalovera10/flutter-clean-architecture
This course is great! Thanks for this course!
Thanks for this course!
Would you recommend this structure and all these packages in 2022? Or would you change anything?
Thanks for your tutorial! You taught me a lot about tdd and clean architecture. Since this material was implemented in 2019, some things changed since then: null safety, libraries deprecated and etc.
I’m gonna clone your repository, make the necessary changes and open a pull request, but since this is old and you would have to edit the video description/blog posts, I don’t know if you’re going to change it.
In the meanwhile, if the readers are looking for a version of the app working in 2022 (after null safety update), check my github.
https://github.com/farvic/tdd-clean-number-trivia-app
Credits for the tutorial and original material belong to the author.
Thanks for your tutorial! Matej, i learnt a lot about tdd and clean architecture.
2023 many dependencies has been deprecated and all.
if anyone needs the updated version of the code as of 2023, you can check the repository below, all credits to the original owner.
https://github.com/ribak100/NumberTrivia
Thanks for sharing. I read many of your blog posts, cool, your blog is very good.