Lesson 1 · Flutter UI

Everything is a Widget

Now the exciting part begins: putting things on the screen. Flutter is built on one beautifully simple idea that, once it clicks, makes everything else fall into place — in Flutter, absolutely everything you see is a Widget.

A piece of text? A Text widget. A button? A button widget. A photo? An Image widget. Even invisible things like spacing and alignment are widgets. Your entire app is a tree of widgets, nested inside one another like Russian dolls.

The Lego Analogy: Think of widgets as Lego bricks. Each brick is small and simple on its own. But you snap small bricks into bigger structures, and those into bigger ones still, until you have built an entire castle. A complex app screen is just many small widgets, thoughtfully combined.

Your First Widget

Here is how you create a simple piece of styled text in Flutter:

Text( 'Hello, World!', style: TextStyle( fontSize: 24, color: Colors.blue, fontWeight: FontWeight.bold, ), )

Read it as plain instructions: "Give me a Text widget that says Hello World, and style it with a font size of 24, coloured blue, and bold." Notice how declarative it is — you describe what you want, and Flutter draws it. You never write step-by-step drawing commands.

Two Kinds of Widget

There is one distinction worth learning early, because you will meet it constantly:

The Widget Tree

Because widgets nest inside widgets, your whole screen forms a shape called the widget tree. At the top sits your app; branching beneath it are screens, then sections, then individual buttons and texts at the tips. When you build an app, you are really growing this tree — and learning to read it is one of the most important Flutter skills.

Reassuring truth: There are hundreds of built-in widgets, and you do not need to memorise them. You will learn the dozen or so you use daily, and look up the rest as needed. Every professional keeps the documentation open in a browser tab.

Deep Dive: The Three Trees

While we talk about the "Widget Tree", Flutter actually maintains three trees under the hood to achieve its incredible performance (often 60 or 120 FPS):

  • Widget Tree: The blueprint. It describes what the UI should look like. It rebuilds often and is very lightweight.
  • Element Tree: The manager. It represents the actual active instances on screen. It compares the new Widget Tree with the old one to find what changed.
  • RenderObject Tree: The painter. This is the heavy lifting layer that actually calculates sizes, layout, and draws pixels to the screen.

When you call setState, Flutter rebuilds the Widget Tree, but the Element Tree smartly ensures only the necessary RenderObjects are updated. This is the secret to Flutter's speed!

Animated Example

A screen is a tree of widgets

Scaffold
AppBar
Body
Text
Button
  1. Every screen starts with a Scaffold — the frame.
  2. It branches into an AppBar (top) and a Body.
  3. Inside the body sit a Text and a Button. That nested shape is the widget tree.

Key Takeaways

  • In Flutter, everything you see is a Widget — text, buttons, images, even spacing.
  • Widgets nest inside widgets to form the widget tree.
  • A StatelessWidget never changes; a StatefulWidget can change over time.
  • You describe what you want (declarative) and Flutter draws it for you.

Your Turn: Practice

Look at any single screen in an app on your phone and mentally draw boxes around every piece: each text, icon, button and image. Count how many separate widgets you think it took to build. This "seeing the widget tree" skill is central to Flutter.

Lesson 2 · Flutter UI

The Skeleton of Every Flutter App

Before we place buttons and text, every Flutter app needs a basic structure to sit inside — a skeleton that provides the app bar at the top, the main body area, background colours, and so on. Flutter gives us ready-made widgets for exactly this, and almost every screen you ever build starts from the same handful.

MaterialApp — The Wrapper

At the very root of your app sits a MaterialApp. It is a special widget that sets up the essentials: the app's theme (colours and fonts), the title, and the navigation system that lets you move between screens. You wrap your entire app inside it once and rarely think about it again.

Scaffold — The Page Layout

Inside that, each screen typically uses a Scaffold. The name is perfect: a scaffold is the frame of a building. It gives you clearly-defined regions to drop your content into — an app bar at the top, a main body in the middle, and optional extras like a floating button or bottom navigation.

Scaffold( appBar: AppBar( title: Text('My First App'), backgroundColor: Colors.blue, ), body: Center( child: Text('Hello from the body!'), ), floatingActionButton: FloatingActionButton( onPressed: () {}, child: Icon(Icons.add), ), )

A complete, standard screen structure.

Let us name each part you can already recognise:

The parent-child pattern

Notice a repeating shape: widgets take other widgets as their contents, using words like child (one widget inside) or children (a list of widgets inside). The Scaffold holds an AppBar and a body; the body holds a Center; the Center holds a Text. This nesting is the widget tree from the last lesson, written in code.

Don't memorise, recognise. You will type Scaffold and AppBar so often that they will become second nature within a week. For now, just understand the roles: MaterialApp wraps the whole app, Scaffold frames a single screen.
Animated Example

The Scaffold frames every screen

MaterialApp
Scaffold
AppBar
body
FAB
  1. At the root sits MaterialApp, wrapping the whole app (theme, navigation).
  2. Each screen uses a Scaffold — the frame of the page.
  3. It gives you an AppBar on top and a body for your content.
  4. Plus optional extras like a floating action button. A standard, complete screen.

Key Takeaways

  • MaterialApp wraps your whole app and sets up theme and navigation.
  • Scaffold frames a single screen with an app bar, body and optional buttons.
  • Widgets hold other widgets via child (one) or children (a list).
  • This nesting is the widget tree written in code.

Your Turn: Practice

On paper, sketch a simple screen (say a profile page) and label which part would be the AppBar, which is the body, and whether you would use a floatingActionButton. Matching real layouts to Scaffold regions builds fluency.

Lesson 3 · Flutter UI

The Widgets You'll Use Every Day

You could build almost any interface with a small set of core widgets. Let us meet the ones you will reach for constantly, so they become familiar friends.

Text — Displaying Words

The workhorse for showing any text, with full control over size, colour and weight through its style property (which takes a TextStyle).

Text('Welcome!', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold))

Container — The Versatile Box

A Container is one of the most useful widgets in all of Flutter. It is a rectangular box you can size, colour, pad, add rounded corners to, and place another widget inside. If you have ever used a div in web design, this is its cousin.

Container( width: 200, height: 100, padding: EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(12), ), child: Text('Inside a box'), )

A blue, rounded box with padding and text inside it.

Image — Showing Pictures

Display a picture from the internet with Image.network(...), or one bundled inside your app with Image.asset(...).

Image.network('https://example.com/photo.jpg')

Icon — Little Symbols

Flutter ships with thousands of ready-made icons through the Icons collection — a heart, a home, a settings gear — no image files needed.

Icon(Icons.favorite, color: Colors.red, size: 32)

Buttons — Making Things Tappable

The most common button is ElevatedButton. Its most important part is onPressed: the function that runs when the user taps it. This is where your app comes alive — and notice the arrow function from the Dart course making an appearance.

ElevatedButton( onPressed: () { print('The button was tapped!'); }, child: Text('Tap Me'), )
Play and explore. The best way to learn widgets is to change a number and see what happens. Make the font size 50, the colour purple, the border radius 40. Curiosity and experimentation teach far faster than memorisation.
Animated Example

The everyday widgets, placed one by one

Text — words
Container — a styled box
Image + Icon
ElevatedButton — tap me
  1. Text shows words on screen.
  2. Container is a versatile styled box — size, colour, padding, rounded corners.
  3. Image shows pictures; Icon gives thousands of built-in symbols.
  4. ElevatedButton makes things tappable via its onPressed function.

Key Takeaways

  • Text shows words; Container is a versatile styled box.
  • Image shows pictures; Icon shows built-in symbols with no image files.
  • ElevatedButton makes things tappable via its onPressed function.
  • The best way to learn a widget is to change one property and watch what happens.

Your Turn: Practice

Describe (in code or on paper) a Container that is 150 wide, 150 tall, coloured green, with rounded corners of 20, holding centred white Text that says "Hello". Getting comfortable combining widgets is the goal.

Lesson 4 · Flutter UI

Arranging the Screen: Rows & Columns

If you place a Text widget and a Button widget on a screen, how does Flutter know where to put them? Should they sit side by side, or stack on top of each other? To arrange our Lego bricks, we use two invisible but essential layout widgets: Column and Row. Master these two and you can build almost any layout.

Column — Stacking Vertically

A Column takes a list of widgets (its children) and stacks them top to bottom, like a stack of pancakes.

Column( children: [ Text('Top item'), Text('Middle item'), ElevatedButton( onPressed: () {}, child: Text('Bottom button'), ), ], )

Row — Arranging Horizontally

A Row does the same thing but left to right, like books on a shelf. Want an icon sitting next to a label? Put them both inside a Row.

Row( children: [ Icon(Icons.star, color: Colors.amber), Text('4.8 rating'), ], )

Controlling the Spacing

Rows and Columns give you two powerful properties to position their children along each axis:

Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Total'), Text('\$50.00'), // pushed to the far right ], )

spaceBetween pushes the first child to the start and the last to the end — perfect for price rows.

Two More Helpers: Expanded and SizedBox

Deep Dive: Stack and Positioned

Rows and Columns push items next to each other. But what if you want widgets to overlap? Like a notification badge sitting on top of a bell icon, or text floating over an image?

You use a Stack widget. Children in a Stack are drawn back-to-front. You can then wrap children in a Positioned widget to pin them exactly where you want them relative to the edges of the Stack.

Stack( children: [ Image.network('profile.jpg'), Positioned( bottom: 10, right: 10, child: Icon(Icons.verified, color: Colors.blue), ), ], )
Common beginner error: Putting too many items in a Column so they run off the bottom of the screen produces an "overflow" warning (a yellow-and-black striped bar). The fix is usually to wrap your Column in a SingleChildScrollView, which makes the content scrollable. Now you will recognise that warning instead of fearing it.
Animated Example

Column stacks; Row lines up

Column (vertical)
1
2
3
  1. A Column stacks its children vertically, top to bottom.
  2. Each child sits below the previous — like a stack of pancakes.
  3. Switch to a Row and the very same children line up horizontally.

Key Takeaways

  • Column stacks widgets vertically; Row arranges them horizontally.
  • mainAxisAlignment and crossAxisAlignment control spacing and alignment.
  • Expanded makes a child fill remaining space; SizedBox adds fixed gaps.
  • An overflow warning usually means you need a SingleChildScrollView.

Your Turn: Practice

Plan the layout for a price row that shows the word "Total" on the far left and "$50" on the far right. Which widget (Row or Column) and which alignment value would you use? Write it out.

Lesson 5 · Flutter UI

Making It Beautiful

A working app is good; a beautiful app is what people love. Flutter is famous for making gorgeous design achievable, and the tools are simpler than you might expect. This lesson covers spacing, colour and depth — the three ingredients that turn a plain screen into a polished one.

Spacing: Padding vs. Margin

Two kinds of empty space shape every layout, and beginners often confuse them:

Container( margin: EdgeInsets.all(12), // space OUTSIDE the box padding: EdgeInsets.all(20), // space INSIDE the box child: Text('Comfortable text'), )

EdgeInsets.all(20) applies equal spacing on every side. You can also target sides individually with EdgeInsets.symmetric(horizontal: 16, vertical: 8).

Colour and Rounded Corners

The BoxDecoration widget (used in a Container's decoration) is where visual polish lives: background colour, rounded corners, borders and shadows all live here.

Container( padding: EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.deepPurple, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black26, blurRadius: 10, offset: Offset(0, 4), ), ], ), child: Text('A polished card', style: TextStyle(color: Colors.white)), )

Colour + rounded corners + a soft shadow = a professional-looking card.

The Power of Shadows

That boxShadow is small but transformative. A soft shadow lifts an element off the background and gives it a sense of depth — it is the single most effective trick for making a flat design feel real and tactile. Notice how much more "designed" the card above feels because of it.

Deep Dive: Gradients and Borders

Sometimes a solid color is too flat. BoxDecoration makes it incredibly easy to add beautiful gradients and borders:

Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.blue, Colors.purple], begin: Alignment.topLeft, end: Alignment.bottomRight, ), border: Border.all(color: Colors.white, width: 2), ), )

Gradients immediately elevate a design from simple to modern, especially when combined with a subtle shadow.

The design mindset

Good design usually means generous spacing, a small, consistent colour palette (one main colour, one accent, plus neutrals), rounded corners, and gentle shadows. You do not need artistic talent to apply these — they are learnable rules. Follow them and your apps will look far more professional than the effort suggests.

Themes save time: Instead of styling every widget individually, Flutter lets you define a ThemeData once in your MaterialApp — your colours and fonts — and the whole app follows it automatically. Change the theme, and your entire app re-styles at once.
Animated Example

A plain box becomes a polished card

Pro Plan
  1. Start with a plain grey box.
  2. borderRadius softens the corners.
  3. A colour and white text make it pop.
  4. Generous padding gives it room.
  5. A soft boxShadow lifts it off the page. Now it looks designed.

Key Takeaways

  • Padding is space inside a box; margin is space outside it.
  • BoxDecoration adds colour, rounded corners, borders and shadows.
  • A soft boxShadow gives depth and is the fastest way to look professional.
  • Good design = generous spacing, a small colour palette, rounded corners, gentle shadows.

Your Turn: Practice

Take the plain "Hello" container from the last exercise and improve it: add 24 of padding, a soft shadow, and 16 margin around it. Notice how a few small style choices transform a plain box into a polished card.

Lesson 6 · Flutter UI

Bringing the App to Life with State

So far our screens are like beautiful paintings — lovely to look at, but if you tap them, nothing happens. To make the screen change in response to the user (a growing like-count, a flipped switch, a typed message), we need the single most important concept in Flutter: State.

What exactly is "State"?

State is simply the current data a screen is remembering right now. If you have a counter showing 0, the state is the number 0. When the user taps the button, the state becomes 1. State is the app's short-term memory for a screen — the values that can change while the user is looking at it.

The Magic Word: setState

Here is the beautiful part. In Flutter you do not manually redraw the screen. Instead, you change your data inside a special function called setState, and Flutter automatically rebuilds the screen to match — instantly and efficiently, faster than the eye can see.

int counter = 0; // our state: the current count ElevatedButton( onPressed: () { setState(() { counter = counter + 1; // change the data... }); // ...and Flutter redraws the screen with the new number! }, child: Text('Add +1'), )

The flow is always the same two-step dance: (1) change the data, (2) call setState so Flutter refreshes the screen. Forget step two and your data changes silently but the screen never updates — a classic beginner puzzle you now know how to solve.

Stateless vs. Stateful, Revisited

This is why the two widget types from Lesson 1 exist. A widget that needs to remember changing data must be a StatefulWidget (it can hold state and call setState). A widget that only displays fixed content can stay a lighter StatelessWidget.

Reading User Input

To capture what a user types, you use a TextField widget connected to a TextEditingController. The controller holds whatever the user has typed, and you can read it whenever you need:

final controller = TextEditingController(); TextField( controller: controller, decoration: InputDecoration(hintText: 'Enter your name'), ) // Later, read what they typed: print(controller.text);

The Widget Lifecycle

StatefulWidgets have several important methods besides build that dictate their "lifecycle":

You've built the heart of every app

"Hold some data, react to a tap, update the screen" — that single loop is the beating heart of every interactive app in existence. A to-do list, a game score, a shopping cart, a chat: all of them are just state changing and the UI following along. You now understand the core of Flutter.

Deep Dive: Beyond setState

While setState is perfect for a single screen, what if you have a shopping cart icon on screen A that needs to update when you tap "Add to Cart" on screen B? Passing data back and forth becomes messy. Professional apps use State Management solutions to solve this. The most popular ones are:

  • Provider / Riverpod: These let you put your state outside the widget tree and "listen" to it from anywhere.
  • BLoC (Business Logic Component): Uses streams of events and states, separating logic completely from the UI. Great for large enterprise apps.
  • GetX: A simplified, all-in-one ecosystem for state, routing, and dependencies.

You don't need these yet, but knowing they exist is the first step towards advanced architecture!

Animated Example

setState changes data, the UI follows

0
+1 · setState
  1. Tap +1 → setState changes count to 1, and Flutter redraws the number.
  2. Tap again → count is 2. The screen always tracks the data.
  3. Change the data → the UI updates itself. That loop powers every app.

Key Takeaways

  • State is the changeable data a screen remembers right now.
  • Change data inside setState(...) and Flutter redraws the screen automatically.
  • Widgets that hold changing data must be StatefulWidgets.
  • Capture typed input with a TextField and a TextEditingController.

Your Turn: Practice

On paper, describe the two steps that must happen when a user taps a "+1" button on a counter. (Hint: one step changes data, the other tells Flutter to redraw.) Forgetting the second step is the classic beginner bug — make sure you can name it.

Lesson 7 · Flutter UI

Displaying Lists of Data

Almost every real app is, at its heart, a list: a list of chats, posts, products, songs, messages. So far we have hand-typed each widget, but real apps do not do that — they take a list of data (remember Lists from Dart?) and turn it into widgets automatically. This lesson is a genuine turning point: it is where your apps start to feel real.

The ListView Widget

A ListView is like a Column, but with a superpower: if its contents are taller than the screen, it becomes scrollable automatically. For a small, fixed number of items you can list them directly:

ListView( children: [ ListTile(title: Text('First item')), ListTile(title: Text('Second item')), ListTile(title: Text('Third item')), ], )

Notice ListTile — a ready-made widget that gives you a nicely-styled row, perfect for lists. It can hold a title, a subtitle, a leading icon and a trailing arrow, all for free.

The Real Power: ListView.builder

Here is the professional way, and it is important. Imagine you have a list of 1,000 messages. Building all 1,000 rows at once would be slow and waste memory. Instead, ListView.builder is lazy: it only builds the few items currently visible on screen, and creates more as you scroll. This is exactly how Instagram scrolls smoothly through endless posts.

// Our data — a simple List of Strings final tasks = ['Buy milk', 'Walk dog', 'Study Flutter']; ListView.builder( itemCount: tasks.length, // how many rows in total itemBuilder: (context, index) { // Called once per visible row; 'index' is 0, 1, 2... return ListTile( leading: Icon(Icons.check_circle), title: Text(tasks[index]), // pull item by its index ); }, )

itemCount says how many; itemBuilder builds each row from the data.

The lazy waiter: A smart waiter does not cook all 1,000 meals the moment the restaurant opens. They cook each meal only as it is ordered. ListView.builder is that smart waiter — it builds each row only when you actually scroll to it, keeping the app fast no matter how much data there is.

Connecting Data to the Screen

Study the pattern, because you will use it constantly: you have a List of data, you tell the builder how many items there are (itemCount), and for each index you return a widget built from data[index]. Change the data (using setState) and the list on screen updates itself. This is how every dynamic, data-driven screen in Flutter is built.

Pull to Refresh: Want to let users pull down to load new data? Just wrap your ListView in a RefreshIndicator and give it an onRefresh function that fetches the new data. Flutter handles the entire dragging animation and loading spinner for free.

Why this lesson matters so much

The moment you can turn a list of data into a scrolling list of widgets, you can build the core of almost any app you can imagine — a to-do list, a contacts book, a chat, a shop, a news feed. They are all, underneath, a ListView.builder over some data. This is one of the most valuable skills in all of Flutter.

Deep Dive: Grids and Separators

ListView is just the beginning. Flutter gives you variants for almost any data presentation:

  • ListView.separated: Just like builder, but it takes a separatorBuilder. Perfect for adding a subtle dividing line (like Divider()) between chats without making it part of the item itself.
  • GridView.builder: Need a photo gallery instead of a vertical list? GridView arranges items in columns and rows. You give it a gridDelegate to tell it how many columns you want (e.g., SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2)).
  • ReorderableListView: Want users to drag and drop items to reorder them? This built-in widget handles the drag animations and gives you the new indices automatically.
Animated Example

ListView.builder builds rows from data

tasks = [ "Buy milk", "Walk dog", "Study" ]
Buy milk
Walk dog
Study
  1. We start with a data list of three task names.
  2. ListView.builder builds a row for the item at index 0.
  3. Then index 1 — one ListTile per item, automatically.
  4. Then index 2. With 1,000 items it builds only what's on screen, staying fast.

Key Takeaways

  • Real apps display dynamic lists built from data, not hand-typed widgets.
  • ListView shows a scrollable column of items.
  • ListView.builder efficiently builds only the items currently on screen — essential for long lists.
  • You turn a data list into widgets by mapping each item to a widget (often a ListTile).

Your Turn: Practice

You have a List of ten task names. Describe how you would display them as a scrollable list. Which widget would you use for performance with a long list, and what widget makes a nice pre-styled row for each task? Write your plan.

Lesson 8 · Flutter UI

Collecting Input with Forms

Every login, sign-up, search bar and comment box is a form. Forms are how your app listens to the user instead of only talking to them. And a crucial professional skill comes with them: validation — checking that what the user typed actually makes sense before you act on it.

Reading What the User Types

We met the TextField and its TextEditingController in the state lesson. The controller is a small object that always holds the current text in the field, ready for you to read whenever you need it:

final emailController = TextEditingController(); TextField( controller: emailController, decoration: InputDecoration( labelText: 'Email', border: OutlineInputBorder(), ), ) // Later, when a button is pressed, read the value: print(emailController.text);

Why Validation Is Non-Negotiable

Users will type anything: an email with no "@", an empty password, letters in a phone-number field. If you blindly trust that input, your app will break or store rubbish. Validation is the gate that checks input before you accept it, and shows a clear, friendly error when something is wrong.

Forms with Built-in Validation

Flutter gives you a proper Form widget together with TextFormField, which has a validator — a small function that inspects the input and returns an error message (or null if all is well). Notice how naturally the null-safety idea from Dart returns here: no error means return null.

TextFormField( decoration: InputDecoration(labelText: 'Email'), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your email'; } if (!value.contains('@')) { return 'That does not look like a valid email'; } return null; // null means: this input is valid! }, )

The validator returns an error string, or null when the input is acceptable.

When the user taps submit, Flutter runs every field's validator at once. If any returns a message, that red error appears beneath the field and submission is blocked until it is fixed — exactly the polished behaviour you see in professional apps.

Controlling the Keyboard: Use the textInputAction property on a TextField to change the return key on the keyboard (e.g., from "Done" to "Next"). Combine it with a FocusNode to automatically jump the cursor from the Email field straight into the Password field when the user taps Next.
Golden rule of input: Never trust the user's input — not because users are bad, but because people make honest mistakes and typos. Validating every field is a hallmark of careful, professional code, and it dramatically reduces bugs and support headaches.

Deep Dive: GlobalKey and Form State

To run validation on all fields at once when the user taps "Submit", you wrap your TextFormFields in a Form widget. But how does the button outside the Form tell the Form to validate? You use a GlobalKey.

final _formKey = GlobalKey<FormState>(); Form( key: _formKey, child: Column( children: [ TextFormField(...), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { // If the form is valid, display a snackbar or submit data } }, ) ] ) )

The GlobalKey acts like a remote control, letting the button trigger the validate() method hidden inside the Form's state.

You can now build interactive screens

With lists to display data and forms to collect it, you have both halves of a real, interactive app: showing information and gathering it. Combined with everything before — widgets, layout, styling, state — you can now build genuinely useful screens. One concept remains to tie multiple screens together: navigation.

Animated Example

A validator blocks bad input

email…
✗ "That does not look like a valid email"
✓ valid → form submits
  1. A TextFormField collects the user's email.
  2. They type something invalid. The validator checks it…
  3. …and returns an error message — submission is blocked until it's fixed.
  4. Now a proper email passes the validator (it returns null) and the form submits.

Key Takeaways

  • Forms collect user input with TextField or TextFormField widgets.
  • A TextEditingController (or the form's own state) holds what the user typed.
  • Validation checks input and shows a helpful error before submitting.
  • Always validate: never trust that the user typed what you expected.

Your Turn: Practice

Design a simple login form on paper: two fields (email and password) and a submit button. Write down one validation rule for each field (for example, "email must contain an @"). Thinking about validation up front is what separates robust apps from fragile ones.

Lesson 10 · Flutter UI

Themes & Material Design

Styling each widget by hand works, but it does not scale — and it is how apps end up looking inconsistent. Professionals define the look once in a theme, and every widget follows it automatically. Flutter's widgets are built on Google's Material Design, a complete design system, so you get a polished, consistent look almost for free.

ThemeData: your app's single style sheet

In your MaterialApp you provide a ThemeData — your colours, fonts and shapes. Every Material widget (buttons, app bars, switches) reads from it, so they all match without you styling each one:

MaterialApp( theme: ThemeData( colorSchemeSeed: Colors.deepPurple, // one seed → a whole palette brightness: Brightness.light, textTheme: TextTheme( ... ), ), home: HomeScreen(), )

Dark mode is nearly free

Because everything reads from the theme, offering a dark mode is mostly a matter of supplying a second ThemeData with Brightness.dark and letting the user switch. You do not restyle a single screen by hand.

One dial controls the whole room. A theme is like a lighting dashboard for a building: turn one dial and every light changes together. Change the theme's seed colour and your buttons, links, highlights and app bar all recolour in sync — instantly and consistently.
Animated Example

Change the theme, restyle the whole app

seed: blue
Button
AppBar
Chip
  1. You set your colours once in a ThemeData seed.
  2. Every Material widget — button, app bar, chip — reads from that theme automatically.
  3. Change the seed colour and the entire app restyles at once. That's exactly how a dark-mode toggle works.

Key Takeaways

  • A theme defines your colours, fonts and shapes once, and every widget follows it.
  • Flutter's widgets are built on Material Design, giving a polished, consistent look for free.
  • Change the theme and the whole app restyles — which makes dark mode almost trivial.
  • Consistent theming is the difference between a hobby app and a professional one.

Your Turn: Practice

Pick two colours for an app idea of yours — a main brand colour and an accent. Sketch (or describe) how a button, an app bar and a card would look in a light theme and a dark theme using those colours. Thinking in a small palette is the heart of good design.

Lesson 11 · Flutter UI

Animations & Motion

The difference between an app that feels cheap and one that feels premium is often motion. When things slide, fade and grow instead of snapping, the app feels alive and responsive. Flutter is famous for making beautiful animation genuinely easy, and the gentle starting point is implicit animations.

Implicit animations: motion for free

Normally, if you change a widget's size or colour it jumps instantly. Swap a Container for an AnimatedContainer, give it a duration, and Flutter tweens smoothly between the old and new values every time a property changes — with zero frame-by-frame code from you:

AnimatedContainer( duration: Duration(milliseconds: 400), curve: Curves.easeOut, width: isBig ? 200 : 100, // change this… color: isBig ? Colors.green : Colors.purple, )

Flip isBig inside setState and the box glides to its new size and colour. You describe the destination; Flutter animates the journey.

Motion has rules. Keep animations short (200–400ms), use easing curves (not linear), and animate to communicate — a new item sliding in, a tap giving feedback — never just for decoration. For complex sequences Flutter also offers explicit animations with AnimationController, but implicit ones cover most everyday needs.
Animated Example

AnimatedContainer tweens between states

  1. Change the box's positionAnimatedContainer glides it there smoothly.
  2. Change its size and shape → it grows and rounds into a circle, tweened automatically.
  3. Change the colour and send it home → Flutter animates every property at once. You set the end state; it did the motion.

Key Takeaways

  • Motion makes an app feel premium, responsive and alive.
  • Implicit animations (AnimatedContainer, AnimatedOpacity…) tween automatically when a property changes.
  • You describe the destination inside setState; Flutter animates the journey — no frame code.
  • Keep animations short, eased, and purposeful — motion should communicate, not decorate.

Your Turn: Practice

Think of one place in a simple app (a like button, a card opening, a menu appearing) where a small animation would improve the feel. Describe what property changes and roughly how long the animation should take. Learning to notice where motion helps is the real skill.

Lesson 12 · Flutter UI

Advanced Scrolling with Slivers

A standard ListView is great, but what if you want a collapsing app bar, a grid that turns into a list halfway down, or complex scrolling effects where headers pin to the top? Enter Slivers. A sliver is simply a portion of a scrollable area. By combining multiple slivers inside a CustomScrollView, you can achieve professional, highly customized scrolling behaviors.

The Scrolling Puzzle: Think of a normal ListView as a single, pre-built conveyor belt. It's easy, but it only does one thing. A CustomScrollView is an empty track, and Slivers are interlocking puzzle pieces you place on it: a header piece that shrinks, a grid piece for photos, a list piece for text. They all scroll together seamlessly on the same track.

The CustomScrollView

Instead of a single widget, a CustomScrollView takes a list of slivers. Everything inside must be a Sliver (e.g., you cannot put a regular Container directly inside; it must be wrapped in a SliverToBoxAdapter).

CustomScrollView( slivers: [ SliverAppBar( expandedHeight: 200.0, floating: false, pinned: true, flexibleSpace: FlexibleSpaceBar( title: Text('SliverAppBar'), background: Image.network('...', fit: BoxFit.cover), ), ), SliverList( delegate: SliverChildBuilderDelegate( (context, index) => ListTile(title: Text('Item $index')), childCount: 50, ), ), ], )

The Magic of SliverAppBar

The SliverAppBar is the secret behind those beautiful profile pages where a large image slowly fades and shrinks into a standard top bar as you scroll down. By setting pinned: true, it stays at the top even when collapsed. Setting floating: true makes it reappear the moment you scroll back up, even if you are deep in the list.

Animated Example

SliverAppBar collapsing on scroll

Profile Header
Content 1
Content 2
Content 3
Content 4
  1. A SliverAppBar starts expanded with a large image or title.
  2. As you scroll down, the sliver list pushes up, and the app bar collapses into a compact header.
  3. Because pinned: true, it stays docked at the top. This is the power of Slivers.

Key Takeaways

  • Slivers allow complex scrolling effects like collapsing app bars and mixed lists/grids.
  • Use CustomScrollView to combine multiple slivers on a single scrolling axis.
  • SliverAppBar gives you that polished, animated header effect seen in many top-tier apps.
  • Regular widgets must be wrapped in a SliverToBoxAdapter to live inside a CustomScrollView.

Your Turn: Practice

Think of a popular app you use (like Twitter or Spotify). How does the top bar behave when you scroll down? Does it hide completely, or shrink and pin to the top? If it shrinks, that app is using the native equivalent of a SliverAppBar!

Lesson 13 · Flutter UI

Explicit Animations

Implicit animations (like AnimatedContainer) are easy, but they are reactive: they only animate when a property changes. What if you want an animation to repeat forever (like a loading spinner), chain multiple animations in sequence, or have fine-grained control to pause and reverse? This is where Explicit Animations and the AnimationController come in.

The Director and the Actor: In implicit animations, Flutter does everything for you. In explicit animations, YOU are the director. You hold a stopwatch (the AnimationController) and shout "Action!", "Pause!", or "Rewind!". The widget is the actor, simply following the stopwatch's time.

The AnimationController

An AnimationController generates a value (usually from 0.0 to 1.0) that changes smoothly over a given duration. You can play, pause, reverse, or loop it.

late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, )..repeat(reverse: true); // Loops back and forth infinitely } @override void dispose() { _controller.dispose(); // Always clean up! super.dispose(); }

Connecting Controller to a Widget

To make the widget move, you use a Transition widget (like RotationTransition, FadeTransition) or an AnimatedBuilder, passing it your controller.

RotationTransition( turns: _controller, child: Icon(Icons.autorenew, size: 50), )
Animated Example

AnimationController repeating infinitely

_controller.stop()
  1. Call forward(): The controller goes from 0.0 to 1.0. The box rotates.
  2. Call reverse(): The controller goes backwards to 0.0. The box rotates back.
  3. Call repeat(reverse: true): It loops infinitely. You are in full control of the timeline.

Key Takeaways

  • Explicit animations give you full control over the animation lifecycle (play, pause, reverse, loop).
  • They require an AnimationController and a vsync (TickerProvider).
  • You MUST call dispose() on your controller to prevent memory leaks.
  • Use explicit animations for continuous effects or complex choreographies.

Your Turn: Practice

If you wanted to build a "Loading..." spinner that spins continuously until data arrives, would you use an Implicit Animation (like AnimatedContainer) or an Explicit Animation with an AnimationController? Why?

Lesson 14 · Flutter UI

Custom Painting & Canvas

When built-in widgets aren't enough, Flutter gives you a blank canvas. CustomPaint allows you to draw arbitrary shapes, paths, lines, and text directly onto the screen. It is how highly custom UI components, charts, and simple games are made.

The Digital Etch A Sketch: If widgets are Lego blocks, CustomPaint is a blank sheet of paper and a pen. You explicitly tell the pen: "Move to x: 50, y: 50. Draw a red line to x: 100, y: 100. Draw a blue circle with radius 20." You have absolute freedom, but you do the math.

Using CustomPainter

You create a class extending CustomPainter and override the paint method. You are given a Canvas (to draw on) and a Size (how big the canvas is).

class MyPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.blue ..strokeWidth = 4.0 ..style = PaintingStyle.stroke; // Draw a line from top-left to bottom-right canvas.drawLine(Offset.zero, Offset(size.width, size.height), paint); // Draw a circle in the center canvas.drawCircle(Offset(size.width / 2, size.height / 2), 40, paint); } @override bool shouldRepaint(CustomPainter oldDelegate) => false; }

Integrating with Widgets

You display your custom masterpiece using the CustomPaint widget, passing an instance of your painter.

CustomPaint( size: Size(200, 200), painter: MyPainter(), )
Animated Example

Drawing instructions on a Canvas

  1. canvas.drawLine(...) paints a diagonal line based on coordinates.
  2. canvas.drawCircle(...) paints a circle at a specific Offset.
  3. Everything is drawn instantly using Flutter's high-performance rendering engine, making it perfect for complex graphics.

Key Takeaways

  • CustomPaint gives you low-level drawing access via a Canvas.
  • You define drawing instructions in a class extending CustomPainter.
  • Great for drawing charts, complex custom shapes, and unique UI elements.
  • It is highly performant because it draws directly to the screen using Flutter's core rendering engine.

Your Turn: Practice

If you were tasked with building a Line Chart showing a stock's price history, would you try to build it out of hundreds of tiny Container widgets, or use a CustomPainter? Why?

Lesson 15 · Flutter UI

Responsive & Adaptive Design

Flutter isn't just for phones; it runs on tablets, desktop, and the web. A professional app respects the device it is running on. Responsive design means changing the layout based on screen size, while Adaptive design means changing the behavior or look to match the platform (e.g., using an iOS-style switch on iOS, and a Material switch on Android).

The Liquid Container: Water takes the shape of its container. A responsive app does the same. If poured into a phone, it flows into a single tall column. If poured into an iPad, it spreads out, putting a menu on the left and content on the right.

LayoutBuilder: Knowing Your Space

Use a LayoutBuilder to make decisions based on the available space. It gives you constraints (like maxWidth) which you can check before deciding what widgets to return.

LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth > 600) { // Screen is wide enough! return Row( children: [ SideMenu(), Expanded(child: MainContent()) ], ); } else { // Screen is narrow (like a phone) return Column( children: [ MainContent() ], ); } }, )

MediaQuery: Knowing the Device

While LayoutBuilder tells you the space of the current widget, MediaQuery.of(context) gives you global device information: the full screen size, whether the device is in dark mode, or the size of the safe area (dodging the iPhone notch).

Animated Example

Layout adapting to screen width

  1. On a narrow phone screen, LayoutBuilder returns a single Column.
  2. When the window resizes (or on a tablet), the width increases...
  3. LayoutBuilder sees maxWidth > 600 and smoothly returns a Row with a sidebar instead.

Key Takeaways

  • Responsive design changes the layout based on available space.
  • Adaptive design uses platform-specific components or behaviors (e.g., Apple vs Material).
  • Use LayoutBuilder to make decisions based on the widget's constraints (width/height).
  • Use MediaQuery for device-level metrics like full screen size, dark mode, and safe areas.

Your Turn: Practice

You are building a chat app. On a phone, the user sees a list of chats, taps one, and navigates to the chat view. On a wide iPad screen, what might be a better responsive layout using a Row?

Lesson 16 · Flutter Architecture

Advanced State Management

While setState is great for local widget state (like opening a dropdown or checking a checkbox), passing state across an entire application via constructors quickly becomes unmanageable. This is called "prop drilling". Professional Flutter apps use State Management solutions to handle global data, separate logic from UI, and keep the codebase clean, testable, and scalable.

The Radio Station Analogy: Think of setState like shouting across a room—it only works for the people (widgets) right next to you. State Management is like setting up a radio station. You broadcast the state (the music), and any widget in your app can tune in (listen) to that exact station, no matter where they are in the widget tree.

Provider & Riverpod: The Modern Standard

Provider is one of the most widely used solutions. It wraps your app and allows any widget down the tree to "listen" to data changes without passing it through constructors.

Riverpod is the modern successor to Provider (built by the same creator, Remi Rousselet). It is compile-time safe, meaning you can never get a "ProviderNotFoundException" at runtime, and it handles asynchronous data beautifully.

// A simple Riverpod Provider final counterProvider = StateProvider<int>((ref) => 0); // Inside a ConsumerWidget (which replaces StatelessWidget): class CounterText extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final count = ref.watch(counterProvider); return Text('$count'); } }

With Riverpod, you can easily combine providers, cache network requests, and manage complex application states with minimal boilerplate.

BLoC (Business Logic Component): The Enterprise Choice

For large enterprise apps, BLoC is extremely popular. It forces a strict separation of concerns by using Dart Streams. The UI is completely dumb—it only sends Events to the BLoC, and the BLoC does the thinking, eventually outputting States for the UI to render. It is more verbose but highly predictable and testable.

Deep Dive: Understanding BLoC Flow

The flow in BLoC is always unidirectional:

  1. Event: User taps a button (e.g., FetchDataEvent).
  2. BLoC: Receives the event, talks to an API, and emits a loading state, followed by a success state.
  3. State: The UI listens to these states using a BlocBuilder and rebuilds automatically (e.g., shows a spinner, then shows a list).

This strict pipeline makes tracking down bugs incredibly easy because every change in the app is represented by a discrete state transition.

Animated Example

Global State Management

Widget A
Add to Cart
Global State
(Cart = 1)
Widget B
Cart Icon
  1. Both widgets exist far apart in the tree.
  2. Widget A updates the Global State (e.g. via Riverpod).
  3. Widget B listens to the state and automatically updates its UI.

Key Takeaways

  • Don't use setState for global data like user authentication or shopping carts.
  • State management tools separate your business logic from your UI widgets.
  • Riverpod and BLoC are two of the most sought-after architecture skills for Flutter developers.
Lesson 17 · Networking

Connecting to APIs

Most apps need to talk to the internet — to fetch news, submit forms, or download images. In Flutter, we use the http package or dio to make network requests, combined with Dart's powerful asynchronous features (Future and async/await).

Making a GET Request & Parsing JSON

When you fetch data from an API, it almost always arrives as a JSON string. The professional way to handle this is not to use raw Maps, but to parse the JSON into a strongly typed Dart Model class (like User or Article).

import 'package:http/http.dart' as http; import 'dart:convert'; // 1. Define your model class User { final String name; User({required this.name}); // A factory constructor to create a User from JSON factory User.fromJson(Map<String, dynamic> json) { return User(name: json['name']); } } // 2. Fetch and parse Future<User> fetchUser() async { final response = await http.get(Uri.parse('https://api.example.com/user/1')); if (response.statusCode == 200) { final data = jsonDecode(response.body); return User.fromJson(data); // Convert to Dart object! } else { throw Exception('Failed to load user'); } }

Deep Dive: http vs Dio

While the standard http package is great for simple requests, large projects often use the Dio package. Dio provides powerful features out-of-the-box that are tedious to write manually with http:

  • Interceptors: Automatically attach authentication tokens to every request globally.
  • FormData: Easily upload files and images.
  • Global configuration: Set a base URL and default timeouts once for your whole app.

FutureBuilder: UI for Async Data

Flutter provides a brilliant widget called FutureBuilder that automatically handles the states of a network request: loading, error, and success. You give it a Future, and it rebuilds the UI as the Future completes.

FutureBuilder<User>( future: fetchUser(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return CircularProgressIndicator(); // Shows while waiting } else if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); // Shows if it failed } // Shows when the Future completes successfully return Text('Welcome, ${snapshot.data!.name}'); } )
Animated Example

FutureBuilder network request

Welcome, Ali!
  1. The app sends an API request to the server.
  2. ConnectionState.waiting: FutureBuilder shows a loading spinner.
  3. Data arrives: The UI rebuilds with the new JSON data.

Key Takeaways

  • Use http or dio for network requests.
  • Always parse JSON data securely using Dart's jsonDecode.
  • FutureBuilder cleanly handles the UI states of loading data from the web.
Lesson 18 · Data Persistence

Local Storage & Databases

Apps need to remember things even when they are closed: user preferences, a dark mode toggle, or cached data so the app works offline. Flutter has several options depending on the complexity of your data.

SharedPreferences

For simple key-value pairs (like "isDarkMode=true" or "username=Ali"), use the shared_preferences package. It is lightweight and easy.

final prefs = await SharedPreferences.getInstance(); // Save data await prefs.setBool('isDarkMode', true); // Read data final isDark = prefs.getBool('isDarkMode') ?? false;

SQLite (sqflite)

If you need structured, relational data (like a complex offline To-Do list with categories and dates), Flutter supports SQLite via the sqflite package. You write standard SQL queries to insert, update, and fetch records directly on the device.

final db = await openDatabase('my_db.db'); // Fetch rows using SQL List<Map> results = await db.rawQuery('SELECT * FROM users WHERE age > ?', [18]);

NoSQL & Objects: Isar / Hive

For lightning-fast, object-oriented databases, Isar and Hive are modern favorites in the Flutter community. Instead of writing SQL tables, you store Dart objects (like your User or Task models) directly. They offer incredible read/write speeds, which is perfect for smooth list scrolling and offline-first apps.

Deep Dive: Why Isar?

Isar is rapidly becoming the standard for Flutter local storage because of its powerful features:

  • Full-text search: Easily search through thousands of text records instantly.
  • Multi-platform: Works natively on iOS, Android, Desktop, and Web.
  • Queries & Links: You can link objects together (e.g., a User has many Posts) and query them using strongly-typed Dart code instead of string-based SQL.
  • Watchers: Similar to Firebase, you can "watch" a query. If the local database changes, your UI updates automatically!
Animated Example

Local Storage Offline Flow

Save Data
Device Storage
(Isar/Hive)
Load Offline
  1. User creates a new To-Do task.
  2. Task is saved directly to local storage instantly.
  3. Even with no internet, the app loads tasks from the device.

Key Takeaways

  • Use SharedPreferences for simple settings and flags.
  • Use sqflite if your app requires complex, relational offline data.
  • Use Isar or Hive for high-performance, object-oriented local storage.
Lesson 19 · Backend Services

Firebase Integration

Building a custom backend from scratch takes time. Firebase (by Google) integrates flawlessly with Flutter, providing a complete suite of cloud services: Authentication, Realtime Databases, Cloud Storage, and Push Notifications.

Firebase Auth

Adding secure login (Email/Password, Google Sign-In, Apple Sign-In) is remarkably simple.

// Sign in with email and password final credential = await FirebaseAuth.instance.signInWithEmailAndPassword( email: 'user@example.com', password: 'SuperSecretPassword123', ); print('Signed in as: ${credential.user?.uid}');

Cloud Firestore

Firestore is a flexible, scalable NoSQL cloud database. What makes it magic in Flutter is its real-time streaming capability. By using a StreamBuilder, your app's UI updates instantly across all devices whenever the data in the cloud changes.

// Listening to a collection in real-time StreamBuilder( // We can also add powerful queries here: stream: FirebaseFirestore.instance .collection('messages') .orderBy('timestamp', descending: true) .limit(50) .snapshots(), builder: (context, snapshot) { if (!snapshot.hasData) return CircularProgressIndicator(); return ListView.builder( itemCount: snapshot.data!.docs.length, itemBuilder: (context, index) { var msg = snapshot.data!.docs[index]; return ListTile( title: Text(msg['text']), subtitle: Text(msg['senderName']), ); } ); } )

Deep Dive: Firestore Security Rules & Offline Mode

Two massive advantages of Firestore for mobile apps:

  • Offline Persistence: By default, Firestore caches the data you fetch. If the user goes into a tunnel and loses signal, your app continues to work and display the cached data. Any writes (like sending a message) are queued and synced automatically when the connection returns!
  • Security Rules: Since your mobile app talks directly to the database without a backend server in the middle, you write declarative rules on the Firebase console to ensure users can only read/write their own data.
Animated Example

Real-time sync with Firestore

Hi!
Hi!
  1. Both devices are listening to the Cloud Firestore.
  2. Device A sends a message.
  3. Device B instantly receives it via StreamBuilder without refreshing.

Key Takeaways

  • Firebase offers "backend-as-a-service" with top-tier Flutter support.
  • Firebase Auth handles complex secure login flows effortlessly.
  • Firestore paired with StreamBuilder enables magical real-time apps like chats.
Lesson 20 · Deployment

Releasing the App

You've built a masterpiece. Now it's time to share it with the world. Flutter makes it possible to compile your codebase into native, release-ready bundles for both iOS and Android.

App Icons and Splash Screens

Before launching, you need branding. Packages like flutter_launcher_icons and flutter_native_splash allow you to generate icons and splash screens for all platforms with a single command, saving hours of manual resizing.

Building for Android

For Android, you generate an App Bundle (.aab) which is the modern format required by the Google Play Store.

flutter build appbundle

You then sign this bundle with an upload key and upload it to the Play Console.

Building for iOS

To release for iOS, you must use a Mac with Xcode installed. You create an archive and upload it to App Store Connect.

flutter build ipa

This command compiles the app and prepares an archive (.xcarchive) that Xcode can validate and submit.

CI/CD: Automating the Process

Professional teams rarely build apps manually on their laptops. They use Continuous Integration/Continuous Deployment (CI/CD) tools like Codemagic, GitHub Actions, or Bitrise. Every time you push code, these services automatically run your tests, build the app, and push the update to testers or the App Store!

Animated Example

The CI/CD Release Pipeline

Push
Build
Release
  1. Developer pushes code to GitHub.
  2. CI/CD server (like Codemagic) automatically runs tests and builds the app.
  3. The final bundle is uploaded to the App Store automatically.

Key Takeaways

  • Use packages to automate tedious tasks like generating app icons and splash screens.
  • Android uses flutter build appbundle for Play Store releases.
  • iOS requires Xcode and uses flutter build ipa.
  • CI/CD tools automate testing and releasing, representing the pinnacle of professional app development.