Bloc - wurzelsand/flutter-memos GitHub Wiki

Bloc

Cubit

import 'package:bloc/bloc.dart';

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
}

void main() {
  final cubit = CounterCubit();
  print(cubit.state);
  cubit.increment();
  print(cubit.state);
  cubit.close();
}

Ausgabe:

0
1

Anmerkungen

  • Ein Cubit besitzt auch einen Stream, den man abhören kann:

    Future<void> main() async {
      final cubit = CounterCubit();
      final subscription = cubit.stream.listen((event) {
        print(event); // >> 1
      });
      cubit.increment();
      await Future.delayed(Duration.zero);
      await subscription.cancel();
      await cubit.close();
    }

BLoC

import 'package:bloc/bloc.dart';

abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
  }
}

Future<void> main() async {
  final bloc = CounterBloc();
  print(bloc.state);
  bloc.add(CounterIncrementPressed());
  await Future.delayed(Duration.zero);
  print(bloc.state);
  await bloc.close();
}

Ausgabe:

0
1

Anmerkungen

  • Ein BLoC besitzt auch einen Stream, den man abhören kann:

    Future<void> main() async {
      final bloc = CounterBloc();
      final subscription = bloc.stream.listen((event) {
        print(event); // >> 1
      });
      bloc.add(CounterIncrementPressed());
      await Future.delayed(Duration.zero);
      await subscription.cancel();
      await bloc.close();
    }

Ein BLoC besteht aus drei Komponenten, z. B. Counter:

  • Event: abstract class CounterEvent extends Equatable und dessen Ableitungen, z. B. CounterIncrementPressed

  • State: abstract class CounterState extends Equatable und dessen Ableitungen, z. B. CounterInitial

  • BLoC: class TimerBloc extends Bloc<CounterEvent, CounterState> und darin Methoden die über on<Event>(...) regeln, was passieren soll, wenn ein bestimmtes Ereignis eintritt. Hier kann ich auch noch einen Stream einbauen, den der BLoC über eine Subscription abhört und sich darüber selbst Events zuschickt. So könnte beispielsweise ein Stream jede Sekunde ein CounterIncrementPressed-Event abschicken.

flutter_bloc

Der Counter soll starten, sobald ich Play drücke. Jede Sekunde soll der Counter um 1 erhöht werden. Währenddessen soll der Button Pause anzeigen und den Counter pausieren, sobald er gedrückt wird.

counter_bloc.dart:

import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';

part 'counter_event.dart';

part 'counter_state.dart';

class Counter {
  const Counter();

  Stream<int> count({required int start}) {
    return Stream.periodic(const Duration(seconds: 1), (count) => count + start + 1);
  }
}

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc({required Counter counter})
      : _counter = counter,
        super(const CounterInitial()) { // #1
    on<CounterIncremented>(_onCounterIncremented);
    on<CounterPaused>(_onPaused); // #4
    on<CounterResumed>(_onResumed); // #6

    _counterSubscription = _counter.count(start: state.elapsedTime).listen((count) { // #2
      add(CounterIncremented(elapsedTime: count)); // #8
    });
    add(const CounterPaused()); // #3
  }

  final Counter _counter;
  StreamSubscription<int>? _counterSubscription;

  @override
  Future<void> close() {
    _counterSubscription?.cancel();
    return super.close();
  }

  void _onCounterIncremented(CounterIncremented event, Emitter emit) {
    emit(CounterRun(elapsedTime: event.elapsedTime)); // #9
  }

  void _onPaused(CounterPaused event, Emitter emit) { // #5
    _counterSubscription?.pause();
    emit(CounterPause(elapsedTime: state.elapsedTime));
  }

  void _onResumed(CounterResumed event, Emitter emit) { // #7
    _counterSubscription?.resume();
    emit(CounterRun(elapsedTime: state.elapsedTime));
  }
}

CounterBloc nutzt 3 Event-Klassen und 3 State-Klassen:

Events:

  • CounterIncremented

  • CounterResumed

  • CounterPaused

States:

  • CounterInitial

  • CounterRun

  • CounterPause

Möglicher Ablauf im CounterBloc:

  1. Der Bloc muss zunächst mit einem Anfangs-State initialisiert werden: elapsedTime (Instanz-Variable aller CounterStates) fängt bei 0 an.

  2. Der Counter-Stream wird über listen gestartet. Allerdings sendet der Stream erst nach einer Sekunde seinen ersten Wert.

  3. Zuvor wird ein CounterPaused-Event verschickt.

  4. CounterPaused ist mit der Funktion _onPaused verbunden.

  5. Der Counter wird pausiert und zum zweiten Mal wird ein State-Objekt an interessierte Widgets verschickt (das CounterInitial-Objekt war das erste, das verschickt wurde).

  6. Sobald ein Nutzer auf den Play-Button drückt ...

  7. ... läuft der Counter wieder weiter ...

  8. ... und nach einer Sekunde kann der Counter sein CounterIncremented-Event mit elapsedTime: 1 an das Bloc verschicken, ...

  9. ... das diesen Wert in ein State verpackt und an interessierte Widgets verschickt.

counter_event.dart:

part of 'counter_bloc.dart';

abstract class CounterEvent extends Equatable {
  const CounterEvent();

  @override
  List<Object> get props => [];
}

class CounterIncremented extends CounterEvent {
  const CounterIncremented({required this.elapsedTime});

  final int elapsedTime;

  @override
  List<Object> get props => [elapsedTime];
}

class CounterResumed extends CounterEvent {
  const CounterResumed();
}

class CounterPaused extends CounterEvent {
  const CounterPaused();
}

counter_state.dart:

part of 'counter_bloc.dart';

abstract class CounterState extends Equatable {
  const CounterState({required this.elapsedTime});

  final int elapsedTime;

  @override
  List<Object> get props => [elapsedTime];
}

class CounterInitial extends CounterState {
  const CounterInitial({super.elapsedTime = 0});
}

class CounterRun extends CounterState {
  const CounterRun({required super.elapsedTime});
}

class CounterPause extends CounterState {
  const CounterPause({required super.elapsedTime});
}

main.dart:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'counter_bloc.dart';

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

class App extends StatelessWidget {
  const App({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Counter',
      home: CounterPage(),
    );
  }
}

class CounterPage extends StatelessWidget {
  const CounterPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider( // #1
      create: (_) => CounterBloc(counter: const Counter()),
      child: const CounterView(),
    );
  }
}

class CounterView extends StatelessWidget {
  const CounterView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: const [CounterText(), CounterButton()],
    );
  }
}

class CounterText extends StatelessWidget {
  const CounterText({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final elapsedTime =
        context.select((CounterBloc bloc) => bloc.state.elapsedTime); // #2
    return Text(
      '$elapsedTime',
      style: Theme.of(context).textTheme.headlineLarge,
    );
  }
}

class CounterButton extends StatelessWidget {
  const CounterButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<CounterBloc, CounterState>( // #3
        buildWhen: (previousState, currentState) =>
            previousState.runtimeType != currentState.runtimeType,
        builder: (context, state) { // #4
          if (state is CounterPause) {
            return TextButton(
                onPressed: () =>
                    context.read<CounterBloc>().add(const CounterResumed()), // #5
                child: const Text('Play'));
          } else {
            return TextButton(
                onPressed: () =>
                    context.read<CounterBloc>().add(const CounterPaused()),
                child: const Text('Pause'));
          }
        });
  }
}

Anmerkungen

  1. BlocProvider ist ein Widget, das ein Bloc initialisiert und dieses damit an sein child und dessen children usw. weitergibt.

  2. Mit select lässt sich das Stream direkt abhören: Sobald ein Element empfangen wird, wird build aufgerufen. Aber eigentlich zu oft: Hier wird z. B. jeder Counter-Wert mindestens zweimal im Text-Widget aufgebaut.

  3. BlocBuilder ist ebenfalls ein Widget, ...

  4. ... das aber zusätzlich zum Context auch das aktuelle State-Objekt erhält. Hierbei werden doppelte States (deshalb müssen States Equatable implementieren) aussortiert.

  5. Hier wird das CounterBloc eingelesen, welches ein neues Event (CounterResumed) erhält.

Transform Bloc-Events

Ich probiere aus, wie ich Events, die an ein Bloc geschickt werden, transformieren kann. Ein Stream soll dabei nach jeder halben Sekunde aufwärts zählen. Die Zahlen daraus sollen per Event an das Bloc geschickt werden. Das Bloc soll aber mehr als eine Sekunde benötigen, das Event in ein State zu verarbeiten. Daher wird also bereits ein neues Event verschickt, bevor das erste Event verarbeitet wurde.

Ausführung

ticker_bloc.dart:

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:stream_transform/stream_transform.dart';

/// Number of TickerEvents triggered by the counter:
const iterations = 6;

/// Duration between events triggered by the counter:
const period = Duration(milliseconds: 500);

/// Minimal duration between two on<TickerEvent>:
const throttle = Duration(milliseconds: 1100);

/// Duration of on<TickerEvent>:
const blocked = Duration(milliseconds: 600);

/// Counter to trigger events:
Stream<int> count() =>
    Stream.periodic(period, (count) => count).take(iterations);

class TickerEvent {
  const TickerEvent({required this.value});

  final int value;
}

class TickerState {
  const TickerState({required this.value});

  final int value;
}

class TickerBloc extends Bloc<TickerEvent, TickerState> {
  TickerBloc() : super(TickerState(value: 0)) {
    on<TickerEvent>(_onTickerEvent, transformer: null);
  }

  final DateTime startTime = DateTime.now();

  String get secondsSinceStart {
    final milliseconds = DateTime.now().difference(startTime).inMilliseconds;
    final seconds = milliseconds / 1000;
    return seconds.toStringAsFixed(1);
  }

  Future<void> _onTickerEvent(
      TickerEvent event, Emitter<TickerState> emit) async {
    print('$secondsSinceStart seconds: Start sending ${event.value}');
    await Future.delayed(blocked);
    emit(TickerState(value: event.value));
    print('$secondsSinceStart seconds: Finished sending ${event.value}');
  }

  @override
  void onEvent(TickerEvent event) {
    print('$secondsSinceStart seconds: Event with ${event.value} entered');
    super.onEvent(event);
  }
}

EventTransformer<E> throttleDroppable<E>(Duration duration) {
  Stream<E> eventThrottler(Stream<E> events, Stream<E> Function(E) mapper) {
    final EventTransformer<E> eventTransformer = droppable<E>();
    final Stream<E> throttledStream = events.throttle(duration);
    return eventTransformer.call(throttledStream, mapper);
  }

  return eventThrottler;
}

main.dart:

import 'ticker_bloc.dart';

void main() {
  final ticker = TickerBloc();
  ticker.stream.listen((event) {
    print('Received: ${event.value}');
  }, onDone: () => ticker.close());
  count().listen((event) => ticker.add(TickerEvent(value: event)));
}

Ausgaben

Ohne Transformer

on<TickerEvent>(_onTickerEvent, transformer: null);
0.5 seconds: Event with 0 entered
0.5 seconds: Start sending 0
1.0 seconds: Event with 1 entered
1.0 seconds: Start sending 1
1.1 seconds: Finished sending 0
Received: 0
1.5 seconds: Event with 2 entered
1.5 seconds: Start sending 2
1.6 seconds: Finished sending 1
Received: 1
2.0 seconds: Event with 3 entered
2.0 seconds: Start sending 3
2.1 seconds: Finished sending 2
Received: 2
2.5 seconds: Event with 4 entered
2.5 seconds: Start sending 4
2.6 seconds: Finished sending 3
Received: 3
3.0 seconds: Event with 5 entered
3.0 seconds: Start sending 5
3.1 seconds: Finished sending 4
Received: 4
3.6 seconds: Finished sending 5
Received: 5
  • Jedes Event wird verarbeitet und umgewandelt in ein State verschickt. Da jedoch neue Events bereits verarbeitet werden, bevor ältere Events abgearbeitet sind, gibt es Probleme wenn die Events unterschiedlich lange verarbeitet werden: Dann ist ein neueres Event bereits vor einem älteren abgefertigt, die Reihenfolge kommt also durcheinander.

Mit droppable

on<TickerEvent>(_onTickerEvent, transformer: droppable());
0.5 seconds: Event with 0 entered
0.5 seconds: Start sending 0
1.0 seconds: Event with 1 entered
1.1 seconds: Finished sending 0
Received: 0
1.5 seconds: Event with 2 entered
1.5 seconds: Start sending 2
2.0 seconds: Event with 3 entered
2.1 seconds: Finished sending 2
Received: 2
2.5 seconds: Event with 4 entered
2.5 seconds: Start sending 4
3.0 seconds: Event with 5 entered
3.1 seconds: Finished sending 4
Received: 4
  • Nach 0.5 Sekunden wird das erste Event sofort verarbeitet. Es ist aber erst nach weiteren 0.6 Sekunden fertig gesendet.

  • Bei 1.0 Sekunden kommt das zweite Event. Da aber das erste Event noch nicht fertig verarbeitet ist, wird das zweite Event ignoriert.

  • Bei 1.1 Sekunden ist das Event fertig verarbeitet und das State wird verschickt.

Mit droppable und throttle

on<TickerEvent>(_onTickerEvent, transformer: throttleDroppable(throttle));
0.5 seconds: Event with 0 entered
0.5 seconds: Start sending 0
1.0 seconds: Event with 1 entered
1.1 seconds: Finished sending 0
Received: 0
1.5 seconds: Event with 2 entered
2.0 seconds: Event with 3 entered
2.0 seconds: Start sending 3
2.5 seconds: Event with 4 entered
2.6 seconds: Finished sending 3
Received: 3
3.0 seconds: Event with 5 entered
  • Das erste Event nach 0.5 Sekunden wird sofort verschickt. Da diesmal gedrosselt wird, kann frühestens bei 1.6 Sekunden (0.5 + 1.1) das nächste Event verarbeitet werden.

  • 2.0 Sekunden ist das erste Event, das nach den 1.6 Sekunden gesendet wird. Das nächste Event kann nun frühestens bei 3.1 Sekunden verarbeitet werden.

RepositoryProvider

A RepositoryProvider can be used to implant any object into a branch of a widget tree:

RepositoryProvider.value(
  value: myObject,
  child: widget,
),

In the entire branch of the widget tree, the object can now be read out again:

MyObject myObject = RepositoryProvider.of<MyObject>(context);

Example:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class MutableInt {
  MutableInt(this.value);
  int value;
}

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  MyApp({super.key}) : mutableInt = MutableInt(100);
  final MutableInt mutableInt;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: RepositoryProvider.value(
          value: mutableInt,
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => mutableInt.value += 1,
                  child: const Text('Increase'),
                ),
                const SizedBox.square(dimension: 8),
                const MyWidget(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

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

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ElevatedButton(
          onPressed: () => setState(() {}),
          child: const Text('Show current value'),
        ),
        Text(RepositoryProvider.of<MutableInt>(context).value.toString()),
      ],
    );
  }
}
⚠️ **GitHub.com Fallback** ⚠️