Bird
Raised Fist0
Fluttermobile~20 mins

BLoC pattern basics in Flutter - Mini App: Build & Ship

Choose your learning style10 modes available
Build: Counter with BLoC
A simple counter app that uses the BLoC pattern to manage state. The user can increase or decrease the counter value by pressing buttons.
Target UI
---------------------
|     Counter App    |
|                   |
|       0           |
|                   |
|  [-]       [+]    |
---------------------
Use BLoC pattern to manage counter state
Display current counter value in the center
Two buttons: '+' to increment, '-' to decrement
UI updates automatically when state changes
Starter Code
Flutter
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CounterScreen(),
    );
  }
}

class CounterScreen extends StatelessWidget {
  const CounterScreen({super.key});

  @override
  Widget build(BuildContext context) {
    // TODO: Implement UI and connect to CounterBloc
    return Scaffold(
      appBar: AppBar(title: const Text('Counter App')),
      body: const Center(
        child: Text('0', style: TextStyle(fontSize: 48)),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () {},
            child: const Icon(Icons.remove),
          ),
          const SizedBox(width: 16),
          FloatingActionButton(
            onPressed: () {},
            child: const Icon(Icons.add),
          ),
        ],
      ),
    );
  }
}

// TODO: Create CounterBloc class here
Task 1
Task 2
Task 3
Task 4
Solution
Flutter
import 'dart:async';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CounterScreen(),
    );
  }
}

class CounterBloc {
  int _counter = 0;

  final _counterController = StreamController<int>.broadcast();
  Stream<int> get counterStream => _counterController.stream;

  void increment() {
    _counter++;
    _counterController.sink.add(_counter);
  }

  void decrement() {
    _counter--;
    _counterController.sink.add(_counter);
  }

  void dispose() {
    _counterController.close();
  }
}

class CounterScreen extends StatefulWidget {
  const CounterScreen({super.key});

  @override
  State<CounterScreen> createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  final CounterBloc _bloc = CounterBloc();

  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter App')),
      body: Center(
        child: StreamBuilder<int>(
          stream: _bloc.counterStream,
          initialData: 0,
          builder: (context, snapshot) {
            return Text(
              '${snapshot.data}',
              style: const TextStyle(fontSize: 48),
            );
          },
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: _bloc.decrement,
            tooltip: 'Decrement',
            child: const Icon(Icons.remove),
          ),
          const SizedBox(width: 16),
          FloatingActionButton(
            onPressed: _bloc.increment,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        ],
      ),
    );
  }
}

This app uses the BLoC pattern to separate business logic from UI.

The CounterBloc class manages the counter state internally and exposes a Stream<int> to notify listeners about changes.

The UI listens to this stream using StreamBuilder and rebuilds the counter text whenever the value changes.

Increment and decrement buttons call methods on the BLoC to update the counter, which then emits the new value through the stream.

This keeps the UI simple and reactive, while the BLoC handles all state management.

Final Result
Completed Screen
---------------------
|     Counter App    |
|                   |
|       0           |
|                   |
|  [-]       [+]    |
---------------------
Tapping [+] increases the number in the center by 1
Tapping [-] decreases the number in the center by 1
The number updates immediately when buttons are pressed
Stretch Goal
Add a reset button that sets the counter back to zero using the BLoC
💡 Hint
Add a new method in CounterBloc to reset the counter and add a new FloatingActionButton for reset

Practice

(1/5)
1. What is the main purpose of the BLoC pattern in Flutter?
BLoC stands for Business Logic Component.
easy
A. To separate app logic from the user interface using streams
B. To create animations easily
C. To manage database connections
D. To style widgets with themes

Solution

  1. Step 1: Understand BLoC purpose

    BLoC is designed to separate business logic from UI to keep code clean and testable.
  2. Step 2: Identify correct function

    Among options, only separation of logic and UI using streams matches BLoC's goal.
  3. Final Answer:

    To separate app logic from the user interface using streams -> Option A
  4. Quick Check:

    BLoC = Separate logic and UI [OK]
Hint: Remember BLoC means separating logic from UI with streams [OK]
Common Mistakes:
  • Confusing BLoC with UI styling or animations
  • Thinking BLoC manages databases directly
  • Assuming BLoC is only for UI widgets
2. Which of the following is the correct way to create a StreamController in Dart for BLoC?
easy
A. var controller = Stream();
B. var controller = StreamController<int>();
C. var controller = ControllerStream();
D. var controller = StreamBuilder();

Solution

  1. Step 1: Recall StreamController syntax

    In Dart, StreamController is created by calling its constructor: StreamController<int>().
  2. Step 2: Check options for correct syntax

    Only var controller = StreamController<int>(); uses correct class and constructor syntax.
  3. Final Answer:

    var controller = StreamController<int>(); -> Option B
  4. Quick Check:

    StreamController() constructor = var controller = StreamController(); [OK]
Hint: StreamController is a class, so use its constructor with parentheses [OK]
Common Mistakes:
  • Using Stream() instead of StreamController()
  • Confusing StreamBuilder with StreamController
  • Misspelling class names
3. Given this BLoC code snippet:
final _controller = StreamController<int>();
void addNumber(int n) {
  _controller.sink.add(n);
}

Stream<int> get stream => _controller.stream;

// In UI:
StreamBuilder<int>(
  stream: bloc.stream,
  builder: (context, snapshot) {
    return Text(snapshot.data?.toString() ?? 'No data');
  },
);

What will the UI show after calling addNumber(5)?
medium
A. 5
B. No data
C. null
D. Error

Solution

  1. Step 1: Understand data flow

    Calling addNumber(5) adds 5 to the stream sink, which emits 5 to listeners.
  2. Step 2: StreamBuilder updates UI

    StreamBuilder listens to stream and rebuilds Text widget with snapshot.data = 5.
  3. Final Answer:

    5 -> Option A
  4. Quick Check:

    Stream emits 5, UI shows 5 [OK]
Hint: StreamBuilder shows latest stream data in UI [OK]
Common Mistakes:
  • Thinking UI shows 'No data' after adding a number
  • Confusing snapshot.data with snapshot.hasData
  • Assuming null is shown instead of actual data
4. Identify the error in this BLoC snippet:
class CounterBloc {
  final _controller = StreamController<int>();

  void increment() {
    _controller.sink.add(_counter + 1);
  }

  Stream<int> get stream => _controller.stream;
}

Assuming _counter is not declared anywhere, what is the issue?
medium
A. increment() should return a value
B. StreamController is not closed properly
C. Stream getter should be a method, not a property
D. _counter is not defined, causing a compile error

Solution

  1. Step 1: Check variable usage

    _counter is used but never declared or initialized, causing an error.
  2. Step 2: Review other options

    While closing StreamController is good practice, missing _counter causes immediate compile error.
  3. Final Answer:

    _counter is not defined, causing a compile error -> Option D
  4. Quick Check:

    Undefined variable = compile error [OK]
Hint: Undefined variables cause compile errors immediately [OK]
Common Mistakes:
  • Ignoring missing variable declarations
  • Confusing Stream getter syntax
  • Thinking StreamController must always be closed immediately
5. You want to create a BLoC that filters out even numbers and only emits odd numbers to the UI. Which code snippet correctly implements this behavior?
hard
A. final _controller = StreamController<int>(); Stream<int> get stream => _controller.stream.skip(1);
B. final _controller = StreamController<int>(); Stream<int> get stream => _controller.stream.map((n) => n * 2);
C. final _controller = StreamController<int>(); Stream<int> get stream => _controller.stream.where((n) => n % 2 != 0);
D. final _controller = StreamController<int>(); Stream<int> get stream => _controller.stream.where((n) => n % 2 == 0);

Solution

  1. Step 1: Understand filtering logic

    We want to emit only odd numbers, so filter with condition n % 2 != 0.
  2. Step 2: Check each option's stream transformation

    Only the transformation using where((n) => n % 2 != 0) correctly filters odd numbers. The others either skip the first element, double the numbers with map, or filter even numbers with where((n) => n % 2 == 0).
  3. Final Answer:

    final _controller = StreamController<int>();\nStream<int> get stream => _controller.stream.where((n) => n % 2 != 0); -> Option C
  4. Quick Check:

    Filter odd numbers with where(n % 2 != 0) [OK]
Hint: Use where() with condition to filter stream data [OK]
Common Mistakes:
  • Using map() instead of where() for filtering
  • Filtering even numbers instead of odd
  • Using skip() which removes elements by count
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard