Home TutorialsFlutter Create a single view stateful Flutter app

Create a single view stateful Flutter app

by Atif Azad

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).

VS Code create new Flutter project
VS Code Command Palette
  • 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.

Flutter single view app - normal state
Normal state (on pressing Done button with a user name)
Flutter single view app - error state
Error state (on pressing Done button with empty user name field

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.

Run menu in VS Code (`widget_test.dart)

Add Home Screen

  • Under lib/folder, create folder screens.
  • Create home_screen.dart under screens 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 of body.
  • 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 has Column widget as its child.
  • Column widget arranges the elements vertically and it has multiple children; usernameField, SizedBox, Row, SizedBox and messageLable.
  • We have enclosed doneButton and resetButton in a Row widget because those two needed to be horizontally aligned with each other.
  • Please note that we are using mainAxisAlignment for both Column and Row. However, as you might probably have guessed!, the main axis for a Column is the vertical axis whereas for Row it is the horizontal axis. To align elements on the other axis for both widgets, we can use crossAxisAlignment property.

Now run your project to see that the UI has been laid out correct.

Flutter single view app - UI layout
UI layout

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. 🙂

0 0 vote
Article Rating

You may also like

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More

Privacy & Cookies Policy
0
Would love your thoughts, please comment.x
()
x