This is a series of Flutter tutorials and we will learn and build on each step something new. We are going to start it at very basic level and will create a single view Flutter app in this tutorial.
Flutter Setup
If you are using a Mac, you can follow my tutorial: Setting-up Futter on a Mac. If you are using Windows or Linux, please follow official guidelines to setup Flutter.
Create Project
In VS Code, open the Command Palette (From menu View -> Command Palette or press ⌘⇪P).

- Type ‘flutter’ and select ‘Flutter: New Project’ command.
- Enter project name ‘single_view_app’
- Select the parent folder for your project.
- Wait for the project creation until the file
main.dart
appears.
If you not using VS Code, you can create project using following command in Terminal.
> flutter create single_view_app
Remove main.dart file created with project
This boilerplate project indeed creates a single view app however we want to create a cleaner structure which can be used as foundation for our upcoming multi-view apps. Also by doing this we’ll be able to focus on some of the basic terminologies and concepts.
Therefore please go ahead and remove the existing main.dart.
Entrypoint
For Flutter apps, the entrypoint is main
function defined in main.dart
. Right after deleting the existing one, let us create a new main.dart
file under lib/
folder. Add following code in that file.
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(SingleViewApp());
}
We are creating a material app so we import the material
package on the first line. SingleViewApp
is the name of our app class that we’ll create shortly. As we run the app, this main
functions gets executed which is doing only one thing; instantiating and running SingleViewApp
.
Add SingleViewApp
class in main.dart
class SingleViewApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Single View app',
theme: ThemeData(
primarySwatch: Colors.indigo,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
);
}
}
We are extending StatelessWidget
to create SingleViewApp
because we don’t want to save any state at app level. We are overriding build
function and returning MaterialApp
instance.
Add routes
For easy navigation among different app views, I recommend using routes. As our app is single view app, we need only one route /home
that we describe with following code.
routes: <String, WidgetBuilder> {
'/home': (context) => HomeScreen(),
},
HomeScreen
is the name the class that represents our only view in this app. We’ll create this class in steps ahead.
Flutter needs to know which view it should render at the start. For that we must set initialRoute
which can be any route from the defined routes
. So we will add…
initialRoute: '/home',
Now, our main.dart
should look like:
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(SingleViewApp());
}
class SingleViewApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Single View app',
theme: ThemeData(
primarySwatch: Colors.indigo,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: '/home',
routes: <String, WidgetBuilder> {
'/home': (context) => HomeScreen(),
},
);
}
}
How will the home screen look like!
Before we start coding the home screen, it will be helpful if we see what we are going to build. For simplicity we call our single view ‘Home Screen’. It is a simple view consisting of a text field, two buttons and a label.
Two states
This view has two states, error state or normal state. In error state, it will display a message in Red whereas in normal state it will show a message in Green. Both states can be seen in images below.
Add Widget Tests
We already have test/widget_test.dart
file in our project but that is now irrelevant because that was created with project for default hello world app. For our home screen, let us add our own tests so we’ll replace the existing widget_test.dart
with following.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_single_view/main.dart';
void main() {
testWidgets('State message test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(SingleViewApp());
// 1. Verify the initial UI rendered correctly.
expect(find.widgetWithText(TextField, 'User name'), findsOneWidget);
expect(find.widgetWithText(RaisedButton, 'Done'), findsOneWidget);
expect(find.widgetWithText(RaisedButton, 'Reset'), findsOneWidget);
// 2. Verify the ERROR STATE
// Tap the 'Done' button and trigger a frame.
await tester.tap(find.widgetWithText(RaisedButton, 'Done'));
await tester.pump();
expect(find.text('Please enter user name'), findsOneWidget);
// 3. Verify the NORMAL STATE
// Input username, tap the 'Done' button and trigger a frame.
await tester.enterText(find.widgetWithText(TextField, 'User name'), 'Alex');
await tester.tap(find.widgetWithText(RaisedButton, 'Done'));
await tester.pump();
expect(find.text('Hello Alex'), findsOneWidget);
// 4. Verify the RESET working fine
// Tap the 'Reset' button and trigger a frame.
await tester.tap(find.widgetWithText(RaisedButton, 'Reset'));
await tester.pump();
expect(find.text('Hello Alex'), findsNothing);
expect(find.text('Please enter user name'), findsNothing);
});
}
Right now, all test cases will fail but by the time we complete this tutorial, all tests should and will pass.
How to run the tests?
In case you are struggling with running the test cases, it’s easy. In VS Code, open widget_test.dart
and Run the project. Or otherwise you can also use the menu that appears just at the start of main
function in test file.

Add Home Screen
- Under
lib/
folder, create folderscreens
. - Create
home_screen.dart
underscreens
folder. - Add following code
import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
HomeScreenState createState() => HomeScreenState();
}
Again, on top we are importing package material
. Then we are defining HomeScreen
class that we referenced in /home
route declaration in main.dart
. We are extending this class from StatefulWidget
unlike the SingleViewApp
class which we extended from StatelessWidget
.
The HomeScreen
will maintain a state and will display message according to its current state. For that purpose, HomeScreenState
class is being referenced here to create the state for HomeScreen
. We’ll define HomeScreenState
class in the following section.
It is important to note here that all classes that extend StatefulWidget
need to have a state and will always have an associated class to manage its state and that class should extend State
class.
Add HomeScreenState
class
Let’s define HomeScreenState
class.
class HomeScreenState extends State<HomeScreen> {
final TextEditingController _usernameController = TextEditingController();
String _message='';
@override
Widget build(BuildContext context) {
var messageLable = Text(
'$_message'
);
final usernameField = TextField(
controller: _usernameController,
decoration: InputDecoration(
contentPadding: EdgeInsets.fromLTRB(20.0, 15.0, 20.0, 15.0),
hintText: 'User name',
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(8.0))
),
);
final doneButton = RaisedButton(
child: Text('Done'),
onPressed: () async{
// set message
},
);
final resetButton = RaisedButton(
child: Text('Reset'),
onPressed: () async{
_usernameController.text = '';
},
);
return Scaffold(
appBar: AppBar(
title: Text('Single View'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
usernameField,
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
doneButton,
SizedBox(width: 10),
resetButton
]
),
SizedBox(height: 20),
messageLable
]
),
)
);
}
}
UI elements
As needed, we have created a TextField
(usernameField
), two RaisedButton
(doneButton
and resetButton
) and a Text
(messageLable
).
TextEditingController
To access and manipulate the properties of a TextField
we must assign a controller
to it. In our current view we have one TextField
(namely usernameField
) and one TextEditingController
(namely _usernameController
). If we want to have another TextField
, for example for password, we’ll also have another TextEditingController
which will be associated to that new field.
Layout
The build
function returns a Scaffold
object. We are specifying appBar
and body
for that object. appBar
is simple and self-explanatory so let me focus on explaining the body
.
- We are using
Center
widget at the root ofbody
. - As you can see in the result images above, the UI elements are mainly arranged vertically. However the two buttons have a horizontal sub-arrangement.
- For vertical arrangement of UI elements, Flutter provides
Column
widget. - Whereas for horizontal arrangement, Flutter provides
Row
widget. - Therefore the root widget
Center
hasColumn
widget as itschild
. Column
widget arranges the elements vertically and it has multiplechildren
;usernameField
,SizedBox
,Row
,SizedBox
andmessageLable
.- We have enclosed
doneButton
andresetButton
in aRow
widget because those two needed to be horizontally aligned with each other. - Please note that we are using
mainAxisAlignment
for bothColumn
andRow
. However, as you might probably have guessed!, the main axis for aColumn
is the vertical axis whereas forRow
it is the horizontal axis. To align elements on the other axis for both widgets, we can usecrossAxisAlignment
property.
Now run your project to see that the UI has been laid out correct.
Handle States
As we have to display a different message on each of the two states (error state and normal state), we need to add following function just above the build
function in home_screen.dart
.
Color _messageColor = Colors.green;
void setMessage(String message, Color color) {
setState(() {
_message = message;
_messageColor = color;
});
}
@override
void initState() {
super.initState();
setMessage('', null);
}
setMessage
function is taking message
and color
params. With setState
function call, we are passing an anonymous callback function as its param. setState
function call tell the framework that the state of this object has been changed which needs to be reflected on UI. From Flutter official docs:
Calling setState notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a build for this State object.
Also, we need to add style to messageLable
so that it takes the color as set for current state. We’ll replace messageLable
definition with following.
var messageLable = Text(
'$_message',
style: TextStyle(
color: _messageColor,
fontSize: 20
),
);
… and the last thing, we need to call setMessage
with appropriate message
and color
in onPressed
even of doneButton
. Hence we’ll replace doneButton
definition with following.
final doneButton = RaisedButton(
child: Text('Done'),
onPressed: () async{
if(_usernameController.text.trim().length == 0) {
setMessage('Please enter user name', Colors.red);
}
else {
setMessage('Hello ' + _usernameController.text, Colors.green);
}
},
);
The final state of our `home_screen.dart
should be as follows
import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
HomeScreenState createState() => HomeScreenState();
}
class HomeScreenState extends State<HomeScreen> {
final TextEditingController _usernameController = TextEditingController();
String _message;
Color _messageColor = Colors.green;
void setMessage(String message, Color color) {
setState(() {
_message = message;
_messageColor = color;
});
}
@override
void initState() {
super.initState();
setMessage('', null);
}
@override
Widget build(BuildContext context) {
var messageLable = Text(
'$_message',
style: TextStyle(
color: _messageColor,
fontSize: 20
),
);
final usernameField = TextField(
controller: _usernameController,
decoration: InputDecoration(
contentPadding: EdgeInsets.fromLTRB(20.0, 15.0, 20.0, 15.0),
hintText: 'User name',
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(8.0))
),
);
final doneButton = RaisedButton(
child: Text('Done'),
onPressed: () async{
if(_usernameController.text.trim().length == 0) {
setMessage('Please enter user name', Colors.red);
}
else {
setMessage('Hello ' + _usernameController.text, Colors.green);
}
},
);
final resetButton = RaisedButton(
child: Text('Reset'),
onPressed: () async{
_usernameController.text = '';
setMessage('', null);
},
);
return Scaffold(
appBar: AppBar(
title: Text('Single View'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
usernameField,
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
doneButton,
SizedBox(width: 10),
resetButton
]
),
SizedBox(height: 20),
messageLable
]
),
)
);
}
}
That’s all! Run the tests (widget_test.dart
). All tests should pass now 🙂
Run the project (F5 in VS Code) on iOS and/or Android device/simulator. The result should be the same as shown in How will the home screen look like! section above.
Project on Github
The code is available on Github. https://github.com/atifazad/flutter-single-view
What’s ahead
In next tutorial in this series, we will create a multi view Flutter app. Until then I look forward to your feedback on this part. 🙂