Bloc - wurzelsand/flutter-memos GitHub Wiki
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
-
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(); }
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
-
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 überon<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 einCounterIncrementPressed
-Event abschicken.
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
:
-
Der Bloc muss zunächst mit einem Anfangs-State initialisiert werden:
elapsedTime
(Instanz-Variable allerCounterStates
) fängt bei0
an. -
Der Counter-Stream wird über
listen
gestartet. Allerdings sendet der Stream erst nach einer Sekunde seinen ersten Wert. -
Zuvor wird ein
CounterPaused
-Event verschickt. -
CounterPaused
ist mit der Funktion_onPaused
verbunden. -
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). -
Sobald ein Nutzer auf den Play-Button drückt ...
-
... läuft der Counter wieder weiter ...
-
... und nach einer Sekunde kann der Counter sein
CounterIncremented
-Event mitelapsedTime: 1
an das Bloc verschicken, ... -
... 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'));
}
});
}
}
-
BlocProvider
ist ein Widget, das einBloc
initialisiert und dieses damit an seinchild
und dessenchildren
usw. weitergibt. -
Mit
select
lässt sich das Stream direkt abhören: Sobald ein Element empfangen wird, wirdbuild
aufgerufen. Aber eigentlich zu oft: Hier wird z. B. jeder Counter-Wert mindestens zweimal im Text-Widget aufgebaut. -
BlocBuilder
ist ebenfalls ein Widget, ... -
... das aber zusätzlich zum Context auch das aktuelle State-Objekt erhält. Hierbei werden doppelte
States
(deshalb müssen StatesEquatable
implementieren) aussortiert. -
Hier wird das
CounterBloc
eingelesen, welches ein neues Event (CounterResumed
) erhält.
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.
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)));
}
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.
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.
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.
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()),
],
);
}
}