Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions packages/flutter_bloc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ BlocBuilder(

```dart
BlocProvider(
bloc: BlocA(),
builder: (BuildContext context) => BlocA(),
dispose: (BuildContext context, BlocA bloc) => bloc.dispose(),
child: ChildA(),
);
```
Expand All @@ -67,11 +68,14 @@ By using `BlocProviderTree` we can go from:

```dart
BlocProvider<BlocA>(
bloc: BlocA(),
builder: (BuildContext context) => BlocA(),
dispose: (BuildContext context, BlocA blocA) => blocA.dispose(),
child: BlocProvider<BlocB>(
bloc: BlocB(),
builder: (BuildContext context) => BlocB(),
dispose: (BuildContext context, BlocB blocB) => blocB.dispose(),
child: BlocProvider<BlocC>(
value: BlocC(),
builder: (BuildContext context) => BlocC(),
dispose: (BuildContext context, BlocC blocC) => blocC.dispose(),
child: ChildA(),
)
)
Expand All @@ -83,9 +87,18 @@ to:
```dart
BlocProviderTree(
blocProviders: [
BlocProvider<BlocA>(bloc: BlocA()),
BlocProvider<BlocB>(bloc: BlocB()),
BlocProvider<BlocC>(bloc: BlocC()),
BlocProvider<BlocA>(
builder: (BuildContext context) => BlocA(),
dispose: (BuildContext context, BlocA blocA) => blocA.dispose(),
),
BlocProvider<BlocB>(
builder: (BuildContext context) => BlocB(),
dispose: (BuildContext context, BlocB blocB) => blocB.dispose(),
),
BlocProvider<BlocC>(
builder: (BuildContext context) => BlocC(),
dispose: (BuildContext context, BlocC blocC) => blocC.dispose(),
),
],
child: ChildA(),
)
Expand Down Expand Up @@ -259,7 +272,7 @@ At this point we have successfully separated our presentational layer from our b
- [Github Search](https://felangel.github.io/bloc/#/flutterangulargithubsearch) - an example of how to create a Github Search Application using the `bloc` and `flutter_bloc` packages.
- [Weather](https://felangel.github.io/bloc/#/flutterweathertutorial) - an example of how to create a Weather Application using the `bloc` and `flutter_bloc` packages. The app uses a `RefreshIndicator` to implement "pull-to-refresh" as well as dynamic theming.
- [Todos](https://felangel.github.io/bloc/#/fluttertodostutorial) - an example of how to create a Todos Application using the `bloc` and `flutter_bloc` packages.
- [Timer](https://github.com/felangel/bloc/tree/master/examples/flutter_timer) - an example of how to create a Timer using the `bloc` and `flutter_bloc` packages.
- [Timer](https://felangel.github.io/bloc/#/fluttertimertutorial) - an example of how to create a Timer using the `bloc` and `flutter_bloc` packages.

### Maintainers

Expand Down
10 changes: 8 additions & 2 deletions packages/flutter_bloc/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ void main() {
runApp(
BlocProviderTree(
blocProviders: [
BlocProvider<CounterBloc>(builder: (context) => CounterBloc()),
BlocProvider<ThemeBloc>(builder: (context) => ThemeBloc())
BlocProvider<CounterBloc>(
builder: (context) => CounterBloc(),
dispose: (context, bloc) => bloc.dispose(),
),
BlocProvider<ThemeBloc>(
builder: (context) => ThemeBloc(),
dispose: (context, bloc) => bloc.dispose(),
)
],
child: App(),
),
Expand Down
36 changes: 23 additions & 13 deletions packages/flutter_bloc/lib/src/bloc_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ typedef BlocProviderBuilder<T extends Bloc<dynamic, dynamic>> = T Function(
BuildContext context,
);

/// Signature for the `dispose` function which takes the [BuildContext] and is invoked
/// when the `BlocProvider` is disposed.
typedef BlocProviderDispose<T extends Bloc<dynamic, dynamic>> = void Function(
BuildContext context,
T bloc,
);

/// A Flutter widget which provides a bloc to its children via `BlocProvider.of(context)`.
/// It is used as a DI widget so that a single instance of a bloc can be provided
/// to multiple widgets within a subtree.
///
/// `BlocProvider` automatically calls `dispose` on the [Bloc] therefore
/// `dispose` should not be manually called unless the bloc is initialized outside
/// of `BlocProvider`.
class BlocProvider<T extends Bloc<dynamic, dynamic>> extends StatefulWidget {
/// The [BlocProviderBuilder] which creates the [Bloc]
/// that will be made available throughout the subtree.
Expand All @@ -23,8 +26,18 @@ class BlocProvider<T extends Bloc<dynamic, dynamic>> extends StatefulWidget {
/// The [Widget] and its descendants which will have access to the [Bloc].
final Widget child;

BlocProvider({Key key, @required this.builder, this.child})
: assert(builder != null),
/// The [BlocProviderDispose] which is called when the `BlocProvider` is disposed.
/// In most cases, the provided bloc should be disposed in the `dispose` callback.
/// The main exception to the rule is if a `BlocProvider` is used to provide
/// an existing bloc to a new route.
final BlocProviderDispose<T> dispose;

BlocProvider({
Key key,
@required this.builder,
this.dispose,
this.child,
}) : assert(builder != null),
super(key: key);

@override
Expand All @@ -50,7 +63,7 @@ class BlocProvider<T extends Bloc<dynamic, dynamic>> extends StatefulWidget {
""",
);
}
return provider?.bloc;
return provider.bloc;
}

/// Necessary to obtain generic [Type]
Expand All @@ -75,13 +88,10 @@ class _BlocProviderState<T extends Bloc<dynamic, dynamic>>
@override
void initState() {
super.initState();
_bloc = widget.builder?.call(context);
_bloc = widget.builder(context);
if (_bloc == null) {
throw FlutterError(
"""
BlocProvider builder did not return a Bloc of type $T.
This can happen if the builder is not implemented or does not return a Bloc.
""",
'BlocProvider\'s builder method did not return a Bloc.',
);
}
}
Expand All @@ -96,7 +106,7 @@ class _BlocProviderState<T extends Bloc<dynamic, dynamic>>

@override
void dispose() {
_bloc.dispose();
widget.dispose?.call(context, _bloc);
super.dispose();
}
}
Expand Down
49 changes: 45 additions & 4 deletions packages/flutter_bloc/test/bloc_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import 'package:flutter_bloc/flutter_bloc.dart';

class MyApp extends StatelessWidget {
final CounterBloc Function(BuildContext context) _builder;
final void Function(BuildContext context, CounterBloc bloc) _dispose;
final Widget _child;

const MyApp({
Key key,
@required CounterBloc Function(BuildContext context) builder,
@required void Function(BuildContext context, CounterBloc bloc) dispose,
@required Widget child,
}) : _builder = builder,
_dispose = dispose,
_child = child,
super(key: key);

Expand All @@ -23,6 +26,7 @@ class MyApp extends StatelessWidget {
return MaterialApp(
home: BlocProvider<CounterBloc>(
builder: _builder,
dispose: _dispose,
child: _child,
),
);
Expand Down Expand Up @@ -132,6 +136,21 @@ class CounterPage extends StatelessWidget {
}
}

class RoutePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: RaisedButton(
key: Key('route_button'),
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute<Widget>(builder: (context) => Container()),
);
},
));
}
}

enum CounterEvent { increment, decrement }

class CounterBloc extends Bloc<CounterEvent, int> {
Expand Down Expand Up @@ -177,6 +196,7 @@ void main() {
(WidgetTester tester) async {
await tester.pumpWidget(MyApp(
builder: null,
dispose: null,
child: CounterPage(),
));
expect(tester.takeException(), isInstanceOf<AssertionError>());
Expand All @@ -186,6 +206,7 @@ void main() {
(WidgetTester tester) async {
await tester.pumpWidget(MyApp(
builder: (context) => CounterBloc(),
dispose: null,
child: null,
));
expect(tester.takeException(), isInstanceOf<AssertionError>());
Expand All @@ -195,13 +216,12 @@ void main() {
(WidgetTester tester) async {
await tester.pumpWidget(MyApp(
builder: (context) => null,
dispose: (context, bloc) => bloc.dispose(),
child: CounterPage(),
));
final dynamic exception = tester.takeException();
final String message = """
BlocProvider builder did not return a Bloc of type CounterBloc.
This can happen if the builder is not implemented or does not return a Bloc.
""";
final String message =
'BlocProvider\'s builder method did not return a Bloc.';
expect(exception, isInstanceOf<FlutterError>());
expect((exception as FlutterError).message, message);
});
Expand All @@ -211,6 +231,7 @@ void main() {
final CounterPage _child = CounterPage();
await tester.pumpWidget(MyApp(
builder: _builder,
dispose: (context, bloc) => bloc.dispose(),
child: _child,
));

Expand All @@ -221,6 +242,26 @@ void main() {
expect(_counterText.data, '0');
});

testWidgets('calls dispose on dispose', (WidgetTester tester) async {
bool disposeCalled = false;
final _builder = (BuildContext context) => CounterBloc();
final Widget _child = RoutePage();
await tester.pumpWidget(MyApp(
builder: _builder,
dispose: (context, bloc) => disposeCalled = true,
child: _child,
));

final Finder _routeButtonFinder = find.byKey((Key('route_button')));
expect(_routeButtonFinder, findsOneWidget);
expect(disposeCalled, false);

await tester.tap(_routeButtonFinder);
await tester.pumpAndSettle();

expect(disposeCalled, true);
});

testWidgets(
'should throw FlutterError if BlocProvider is not found in current context',
(WidgetTester tester) async {
Expand Down