Lecture 09 · Mobile App Development

Stateful & Stateless
Widgets + Navigation

Make your app remember things and move between screens.

MIEng. Monther Ibrahim MMuhammad, M.Sc. Fall 2026

→ next  ·  F fullscreen  ·  N notes  ·  T theme

Today

Learning objectives

By the end of today you can

  • Tell stateless and stateful widgets apart, and pick the right one
  • Write a StatefulWidget and change the screen with setState()
  • Know why the screen does not change without setState()
  • Move between screens with Navigator.push and Navigator.pop
  • Send data to the next screen through its constructor

New words today

statethe data a widget remembers right now
setState()"I changed something, draw me again"
routeone screen of the app
Navigatorkeeps the stack of screens
Goal: Login opens a Home page that shows the email you typed.
Mobile App Development · Lecture 09

Recap · Lecture 08

Where we left off

Welcome back
Log in to continue
ali@email.ly
••••••••
Forgot password?
Login
Logging in…
"Tap Login → only a SnackBar."
"I forget the email as soon as you type it."
Today's plan
1

Two kinds

of widgets: stateless and stateful

2

setState()

change what is on the screen

3

Navigation

move between screens

4

Build

Login → Home, with the email

Mobile App Development · Lecture 09

Two kinds

Two kinds of widgets

BOOK FAIRFriday · 6 PM · printed once

StatelessWidget

a printed poster

Shows the data it is given. It never changes by itself.

TextIconAppButton
vs
HOME2 : 1AWAY

StatefulWidget

a scoreboard

Has data that can change while the app is running.

TextFieldSwitcha counter
New word, state: the data a widget remembers right now (a number, true/false, the typed email).
Mobile App Development · Lecture 09

Compare

The difference, and when to use which

StatelessWidgetStatefulWidget
Its datafinal, set oncecan change
Classes you write12 (the widget + its State)
build() runs again when…its parent rebuildssetState() is called (or its parent rebuilds)
ExamplesText, Icon, AppButton, HomePageTextField, Switch, counter, LoginPage
Ask one question: "Does something on this widget change while the user is looking at it?" No → StatelessWidget (the default). Yes → StatefulWidget.
Start stateless. Switch later if you need to (VS Code can convert it for you, slide 15).
Mobile App Development · Lecture 09

Stateless

How a StatelessWidget works

lib/warmup/greeting.dart
class Greeting extends StatelessWidget {
  const Greeting({super.key, required this.name});

  final String name;

  @override
  Widget build(BuildContext context) {
    return Text('Hello, $name!', style: const TextStyle(fontSize: 24));
  }
}

// Using it:
const Greeting(name: 'Ali'),
const Greeting(name: 'محمد'),
1Data comes inthrough the constructor; the fields are final.
2build() runswhen the parent rebuilds it, maybe with new data.
3Each use, its own data'Ali' and 'محمد': same class, different name.
It can never change its own name.
Mobile App Development · Lecture 09

Stateful

How a StatefulWidget works

lib/warmup/counter_page.dart
class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int count = 0;
  // build() lives here, in the State
}
CounterPagethe widget

The settings card: small and final. Flutter can throw it away and make a new one.

createState()
_CounterPageStatethe State

The memory box: keeps count and has build().

_name_ at the start of a name = private: only this file can use it.
=>A short way to write { return …; }
Mobile App Development · Lecture 09

setState()

setState(): the counter

lib/warmup/counter_page.dart
class _CounterPageState extends State<CounterPage> {
  int count = 0;
inside build() · Scaffold( … )
body: Center(child: Text('$count', style: const TextStyle(fontSize: 64))),
floatingActionButton: FloatingActionButton(
  onPressed: () {
    setState(() {
      count++;
    });
  },
  child: const Icon(Icons.add),
),
  1. count lives in the State; count++ adds 1.
  2. setState() means: "I changed something, please draw me again."
  3. Flutter calls build() again, and the new number appears.
FloatingActionButton is the round button that floats in the bottom-right corner.
Counter
3
Mobile App Development · Lecture 09

Break it on purpose

Without setState: the screen doesn't know

With setState
Tap setState() build() runs again screen shows 1
Same button, no setState
lib/warmup/counter_page.dart
onPressed: () {
  count++;
  debugPrint('count is $count');
},
Debug Console
DEBUG CONSOLE
count is 1
count is 2
count is 3
…
The value changed, but nobody told Flutter to redraw. The phone still shows 0.
Rule: change a State field → do it inside setState(() { … }).
Counter
0
Mobile App Development · Lecture 09

true / false

A like button (true / false)

lib/warmup/like_button.dart_LikeButtonState
bool isLiked = false;
@override
Widget build(BuildContext context) {
  return IconButton(
    iconSize: 48,
    icon: Icon(
      isLiked ? Icons.favorite : Icons.favorite_border,
      color: isLiked ? Colors.red : Colors.grey,
    ),
    onPressed: () {
      setState(() {
        isLiked = !isLiked;
      });
    },
  );
boolholds true or false
a ? b : c"if a, use b, otherwise c"
!isLiked"the opposite": true becomes false, false becomes true
On one page: two Greetings that never change, and a heart that does.
Playground
Hello, Ali!
Hello, محمد!

Tap: grey ↔ red

Mobile App Development · Lecture 09

Navigation

Navigation: screens on a stack

LoginPage on screen

1. The app starts

push
HomePage on screen
LoginPagestill there, underneath

2. A new plate on top

pop
HomePagetaken off
LoginPage on screen again

3. The top plate is removed

routeEach screen is called a route.
NavigatorManages the stack of routes.
pushPut a new screen on top.
popTake the top screen off.
The user always sees the top plate.
Mobile App Development · Lecture 09

Navigation · push

Navigator.push: open a new screen

lib/login_page.dartthe Login button
AppButton(
  text: 'Login',
  onPressed: () {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => HomePage(email: email),
      ),
    );
  },
),
contextWhere we are in the widget tree, so Flutter finds the Navigator.
MaterialPageRouteA route with the normal Android / iOS page animation.
builderA function that builds the new screen.
The data travels in the constructor: HomePage(email: email).
The same way Greeting receives name.
Mobile App Development · Lecture 09

Navigation · pop

Navigator.pop and the back arrow

lib/home_page.dart
AppButton(
  text: 'Logout',
  onPressed: () {
    Navigator.pop(context);
  },
),
Never pop the very first screen: there is nothing below it, so you get a black screen.

pop

Removes the top screen; the screen below appears again.

AppBar ←

Added automatically when there is a screen below. Tapping it also pops.

Back button

The Android back button / back gesture pops too.

Home
Welcome!
You are logged in as ali@email.ly
Logout

← added for you

Mobile App Development · Lecture 09

Build it · 1 of 3

AppTextField tells us what you type

lib/widgets/app_text_field.dartthree new lines
const AppTextField({
  super.key,
  required this.label,
  required this.icon,
  this.isPassword = false,
  this.onChanged, // new
});

final ValueChanged<String>? onChanged; // new

// inside build():
return TextField(
  obscureText: isPassword,
  onChanged: onChanged, // new: hand it to the TextField
ValueChanged<String>The type of a function like (value) { } that receives a String.
?Optional, it can be empty (null). The Password field does not need it.
Like VoidCallback from Lecture 8, but with an input.
Not required, so every place that already uses AppTextField keeps working.
Mobile App Development · Lecture 09

Build it · 2 of 3

LoginPage remembers the email

lib/login_page.dart
class _LoginPageState extends State<LoginPage> {
  String email = '';

  @override
  Widget build(BuildContext context) {
    // … same Scaffold as Lecture 8 …
          AppTextField(
            label: 'Email',
            icon: Icons.email,
            onChanged: (value) {
              setState(() {
                email = value;
              });
            },
          ),
  1. Move LoginPage to lib/login_page.dart, click its name, press Ctrl + . (Mac: ⌘ + .) → Convert to StatefulWidget.
  2. Add the field String email = ''; to the State.
  3. Give the Email field an onChanged, and remove its const.
  4. The Login button gets the Navigator.push code from slide 12.
The email lives in the State, so it is still there when Login is tapped.
Welcome back
Log in to continue
Emailali@email.ly
Password
Forgot password?
Login
Don't have an account?Sign up

email = 'ali@email.ly'

Mobile App Development · Lecture 09

Build it · 3 of 3

The Home page

lib/home_page.dart
class HomePage extends StatelessWidget {
  const HomePage({super.key, required this.email});

  final String email;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      // … icon and 'Welcome!' title …
            Text('You are logged in as $email', textAlign: TextAlign.center),
      // … the Logout AppButton from slide 13 …
HomePage is stateless: it only shows the email it receives.
Welcome back
Log in to continue
Emailali@email.ly
Password
Forgot password?
Login

1 Type the email

Welcome back
Log in to continue
ali@email.ly
Password
Forgot password?
Login

2 Tap Login

Home
Welcome!
You are logged in as ali@email.ly
Logout

3 Home, with ←

Mobile App Development · Lecture 09

Watch out

Common mistakes

The error or symptomThe fix
The method 'setState' isn't defined for the type 'CounterPage'.It is a StatelessWidget. Convert it to a StatefulWidget.
Console shows the new value, the screen does notWrap the change in setState(() { … });
The named parameter 'email' is required, but there's no corresponding argument.Pass the data: HomePage(email: email)
The argument type 'HomePage' can't be assigned to the parameter type 'Route<Object?>'.Wrap the page: MaterialPageRoute(builder: (context) => HomePage(email: email))
Coming later: Navigator.pushReplacement (go to Home with no way back to Login) and named routes.
Mobile App Development · Lecture 09

Meme break

Why didn't the number change?

Anakin and Padme meme about forgetting setState I changed the counter variable. And you called setState, right? …You called setState, right?

Ask the class: why didn't the number on screen change?

The variable changed, but Flutter was never told to rebuild. Put the change inside setState(() { … }).
Mobile App Development · Lecture 09

Wrap-up

Summary + Questions

Remember

  • Stateless = shows what it is given. Stateful = remembers data that can change.
  • A StatefulWidget = 2 classes; the State keeps the data.
  • Change a State field inside setState(), or the screen will not update.
  • Screens sit on a stack: Navigator.push adds, Navigator.pop removes.
  • Send data to a screen through its constructor.
Questions?
Back to the learning objectives on slide 2: which ones can you tick off now?
Mobile App Development · Lecture 09

Practice at home

Homework

1 · Counter reset

A refresh button in the AppBar sets the count back to 0.

2 · Like counter

Show the number of likes next to the heart.

3 · Empty email

Show a SnackBar instead of opening Home.

4 · Sign up

"Sign up" opens your Sign up page; the back arrow returns.

5 · Show password

Bonus

An eye icon that shows / hides the password.

Exercise 3 reuses the SnackBar from Lecture 8: ScaffoldMessenger.of(context).showSnackBar(…)
Mobile App Development · Lecture 09