flutterbloc_kit
flutter_bloc for DartNative: the widgets that make the BLoC pattern usable from a widget tree, with the names, type parameters and lifecycle Flutter developers already use, on top of the pure-Dart bloc package.
Why
bloc is pure Dart and runs on DartNative untouched. flutter_bloc is not:
it depends on Flutter's widget layer and on provider, which reaches into the
Flutter element tree. This package is that layer rebuilt on the two
BuildContext primitives DartNative offers, so a screen written against
flutter_bloc ports by changing its imports.
// Before
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// After
import 'package:dartnative/dartnative.dart';
import 'package:flutterbloc_kit/flutterbloc_kit.dart';
package:bloc/bloc.dart is re-exported: Bloc, Cubit, Emitter,
BlocObserver, Transition and the transformers need no second import.
What is here
| flutter_bloc | flutterbloc_kit |
|---|---|
BlocProvider, BlocProvider.value, .of |
same |
MultiBlocProvider |
same |
RepositoryProvider, .value, .of |
same |
MultiRepositoryProvider |
same |
BlocBuilder with buildWhen |
same |
BlocListener with listenWhen |
same |
MultiBlocListener |
same |
BlocConsumer |
same |
BlocSelector |
same |
context.read<T>() |
same |
context.watch<T>() |
same |
context.select<T, R>() |
same |
ProviderNotFoundException |
same |
lazy creation, lazy: false, automatic close |
same |
Every widget file in lib/src/ is a port of the flutter_bloc file of the
same name (MIT, notice in LICENSE). The provider half is this package's
own, see "How it works" below.
Usage
import 'package:dartnative/dartnative.dart';
import 'package:flutterbloc_kit/flutterbloc_kit.dart';
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterCubit(),
child: Scaffold(
body: Center(
child: BlocBuilder<CounterCubit, int>(
builder: (context, count) => Text('$count'),
),
),
floatingActionButton: Builder(
builder: (context) => FloatingActionButton(
onPressed: () => context.read<CounterCubit>().increment(),
child: const Icon(CupertinoIcons.plus),
),
),
),
);
}
}
Providers compose the way they do in Flutter:
MultiRepositoryProvider(
providers: [
RepositoryProvider<TodosRepository>(create: (_) => TodosRepository()),
],
child: MultiBlocProvider(
providers: [
BlocProvider<SettingsCubit>(create: (_) => SettingsCubit()),
BlocProvider<TodosBloc>(
create: (context) =>
TodosBloc(context.read<TodosRepository>())..add(const TodosLoaded()),
),
],
child: const App(home: HomeScreen()),
),
)
Listen, select and consume:
BlocListener<CounterCubit, int>(
listenWhen: (previous, current) => current % 5 == 0,
listener: (context, count) => ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Reached $count'))),
child: ...,
)
BlocSelector<TodosBloc, TodosState, int>(
selector: (state) => state.doneCount,
builder: (context, done) => Text('$done done'),
)
// Inside build: rebuilds when the accent changes, on nothing else.
final accent = context.select<SettingsCubit, Accent>((c) => c.state.accent);
// Inside build: rebuilds on every state the cubit emits.
final settings = context.watch<SettingsCubit>().state;
Share an existing bloc with a pushed route:
Navigator.push(
context,
PageRoute(
builder: (_) => BlocProvider.value(
value: context.read<CounterCubit>(),
child: const DetailScreen(),
),
),
);
The example/ app walks through all of it: a counter cubit with a toast on
every fifth count, three rebuild counters that make watch, select and
BlocSelector visibly different, a todos bloc fed by a repository, a
settings cubit and a shared-cubit detail route.
How it works
DartNative's BuildContext has getInheritedWidgetOfExactType (no
dependency) and dependOnInheritedWidgetOfExactType (rebuild when the
inherited widget's updateShouldNotify says so), and nothing else from the
element tree. provider needs more than that, so:
- Every provider is a
StatefulWidget(InheritedProvider) whose state owns the value: created on first read whenlazy, closed or disposed when the provider leaves the tree. Below it sits oneInheritedWidgetcarrying that holder.context.readis the non-depending lookup;.valueswaps rebuild dependents throughupdateShouldNotify. context.watchandcontext.selecton a bloc use the framework'sListenable.watch(context)bridge, which schedules the calling element's rebuild when a listenable notifies. Each (element, bloc) pair gets oneListenableadapting the bloc's state stream:watchnotifies on every state,selectonly when the selected value changes. Registrations are collected per build and committed in a microtask, so nothing leaks across rebuilds.MultiBlocProvider,MultiBlocListenerandMultiRepositoryProviderfold their list into nested widgets throughSingleChildWidget.withChild, in place ofpackage:nested, which builds Flutter elements directly. A custom entry in one of those lists implementsSingleChildWidget.BlocBuilder,BlocListener,BlocConsumerandBlocSelectorregister a plain dependency on their provider instead of flutter_bloc'scontext.select<B, bool>((bloc) => identical(_bloc, bloc)); the effect is the same: aBlocProvider.valueswap re-subscribes them.
Deviations from flutter_bloc
BlocProvider.ofandRepositoryProvider.ofthrowStateErrorwhere flutter_bloc throwsFlutterError; the message is the same.context.selecton a non-bloc value rebuilds when the provided instance is replaced, not on selector changes (provider tracks those through element aspects). For blocs, selection works as in provider.debugFillPropertiesis not ported; DartNative has no diagnostics tree.SingleChildWidgetis this package's interface, notpackage:nested's.
Testing
Blocs and cubits test with bloc_test as usual. For code that reads them
from a BuildContext, package:flutterbloc_kit/testing.dart mounts
providers outside a widget tree, through the framework's public state hooks,
and hands out a FakeBuildContext:
import 'package:flutterbloc_kit/testing.dart';
test('submit increments through context.read', () {
final cubit = CounterCubit();
final providers = mountProviders([
BlocProvider<CounterCubit>.value(value: cubit),
RepositoryProvider<Api>.value(value: FakeApi()),
]);
addTearDown(providers.dispose);
onSubmitPressed(providers.context);
expect(cubit.state, 1);
});
mountProviders takes the same list a MultiBlocProvider would; create
callbacks run lazily and can read providers listed before them, and
dispose closes what was created. mountProvider mounts one and lets you
update it to test a .value swap. read, watch, BlocProvider.of and
RepositoryProvider.of all resolve; nothing rebuilds, since there is no
element. The package's own tests are written this way and run with
dn test.
Lints
assists_kit 0.1.6 ships three
rules for this package: flutterbloc_watch_outside_build and
flutterbloc_read_state_in_build (on by default) catch context.watch in an
event handler and context.read<T>().state rendered in build, both of which
never rebuild; flutterbloc_read_in_build (opt-in) flags every read in
build.
License
MIT. Contains code from flutter_bloc and provider (MIT), see LICENSE.