Skip to content

Commit

Permalink
Added BlocProvider Widget (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
felangel authored Oct 16, 2018
1 parent 90c52c5 commit 5cc7321
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 58 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ This design pattern helps to separate _presentation_ from _business logic_. Foll

**BlocBuilder** is a Flutter widget which requires a `Bloc` and a `builder` function. `BlocBuilder` handles building the widget in response to new states. `BlocBuilder` is very similar to `StreamBuilder` but has a more simple API to reduce the amount of boilerplate code needed.

**BlocProvider** is 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.

## Usage

For simplicity we can create a Bloc that always returns a stream of static strings in response to any event. That would look something like:
Expand Down
82 changes: 46 additions & 36 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:flutter/material.dart';

import 'package:bloc/bloc.dart';

void main() => runApp(MyApp());
Expand All @@ -15,45 +16,54 @@ class MyAppState extends State<MyApp> {

@override
Widget build(BuildContext context) {
return BlocBuilder(
bloc: _counterBloc,
builder: ((
BuildContext context,
int count,
) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(title: Text('Counter')),
body: Center(
child: Text(
'$count',
style: TextStyle(fontSize: 24.0),
),
return MaterialApp(
title: 'Flutter Demo',
home: BlocProvider(
bloc: _counterBloc,
child: CounterPage(),
),
);
}
}

class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final CounterBloc _counterBloc = BlocProvider.of(context) as CounterBloc;

return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: BlocBuilder<CounterEvent, int>(
bloc: _counterBloc,
builder: (BuildContext context, int count) {
return Center(
child: Text(
'$count',
style: TextStyle(fontSize: 24.0),
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _counterBloc.increment,
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.remove),
onPressed: _counterBloc.decrement,
),
),
],
);
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _counterBloc.increment,
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.remove),
onPressed: _counterBloc.decrement,
),
),
);
}),
],
),
);
}
}
Expand Down
3 changes: 2 additions & 1 deletion lib/bloc.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
library bloc;

export './src/bloc.dart';
export './src/bloc_builder.dart';
export './src/bloc_provider.dart';
export './src/bloc.dart';
14 changes: 7 additions & 7 deletions lib/src/bloc_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,28 @@ import 'package:bloc/bloc.dart';
/// A function that will be run which takes the [BuildContext] and state
/// and is responsible for returning a [Widget] which is to be rendered.
/// This is analagous to the `builder` function in [StreamBuilder].
typedef Widget BlocWidgetBuilder<S>(BuildContext context, S state);
typedef Widget BlocWidgetBuilder<E, S>(BuildContext context, S state);

/// A Flutter widget which requires a [Bloc] and a [BlocWidgetBuilder] `builder` function.
/// [BlocBuilder] handles building the widget in response to new states.
/// BlocBuilder analagous to [StreamBuilder] but has simplified API
/// to reduce the amount of boilerplate code needed.
class BlocBuilder<S> extends StatefulWidget {
final Bloc<dynamic, S> bloc;
final BlocWidgetBuilder<S> builder;
class BlocBuilder<E, S> extends StatefulWidget {
final Bloc<E, S> bloc;
final BlocWidgetBuilder<E, S> builder;

const BlocBuilder({Key key, @required this.bloc, @required this.builder})
: assert(bloc != null),
assert(builder != null),
super(key: key);

@override
State<StatefulWidget> createState() => _BlocBuilderState(bloc, builder);
State<StatefulWidget> createState() => _BlocBuilderState<E, S>(bloc, builder);
}

class _BlocBuilderState<S> extends State<BlocBuilder<S>> {
class _BlocBuilderState<E, S> extends State<BlocBuilder<E, S>> {
final Bloc<dynamic, S> bloc;
final BlocWidgetBuilder<S> builder;
final BlocWidgetBuilder<E, S> builder;

_BlocBuilderState(this.bloc, this.builder);

Expand Down
29 changes: 29 additions & 0 deletions lib/src/bloc_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';

import 'package:bloc/bloc.dart';

/// 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.
class BlocProvider extends InheritedWidget {
/// The Bloc which is made be made available throughout the subtree
final Bloc bloc;

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

@override
bool updateShouldNotify(BlocProvider oldWidget) => oldWidget.bloc != bloc;

/// Method that allows widgets to access the bloc as long as their `BuildContext`
/// contains a `BlocProvider` instance.
static Bloc of(BuildContext context) =>
(context.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider).bloc;
}
16 changes: 2 additions & 14 deletions test/bloc_builder_test.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import 'dart:async';

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

import 'package:bloc/bloc.dart';

class MyAppNoBloc extends StatelessWidget {
final ThemeBloc _themeBloc;

MyAppNoBloc({Key key, @required ThemeBloc themeBloc})
: _themeBloc = themeBloc,
super(key: key);

@override
Widget build(BuildContext context) {
return BlocBuilder<ThemeData>(
return BlocBuilder<ThemeEvent, ThemeData>(
bloc: null,
builder: null,
);
Expand Down Expand Up @@ -88,12 +80,8 @@ void main() {
group('BlocBuilder', () {
testWidgets('throws if initialized with null bloc and builder',
(WidgetTester tester) async {
final ThemeBloc _themeBloc = ThemeBloc();

await tester.pumpWidget(
MyAppNoBloc(
themeBloc: _themeBloc,
),
MyAppNoBloc(),
);
expect(tester.takeException(), isInstanceOf<AssertionError>());
});
Expand Down
151 changes: 151 additions & 0 deletions test/bloc_provider_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:bloc/bloc.dart';

class MyAppNoBloc extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
bloc: null,
child: Container(),
);
}
}

class MyApp extends StatelessWidget {
final Bloc _bloc;
final Widget _child;

const MyApp({Key key, @required Bloc bloc, @required Widget child})
: _bloc = bloc,
_child = child,
super(key: key);

@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
bloc: _bloc,
child: _child,
),
);
}
}

class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
CounterBloc _counterBloc = BlocProvider.of(context) as CounterBloc;
assert(_counterBloc != null);

return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: BlocBuilder<CounterEvent, int>(
bloc: _counterBloc,
builder: (BuildContext context, int count) {
return Center(
child: Text(
'$count',
key: Key('counter_text'),
style: TextStyle(fontSize: 24.0),
),
);
},
),
);
}
}

abstract class CounterEvent {}

class IncrementCounter extends CounterEvent {}

class DecrementCounter extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
int get initialState => 0;

void increment() {
dispatch(IncrementCounter());
}

void decrement() {
dispatch(DecrementCounter());
}

@override
Stream<int> mapEventToState(int state, CounterEvent event) async* {
if (event is IncrementCounter) {
yield state + 1;
}
if (event is DecrementCounter) {
yield state - 1;
}
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CounterBloc &&
runtimeType == other.runtimeType &&
initialState == other.initialState;

@override
int get hashCode =>
initialState.hashCode ^ mapEventToState.hashCode ^ transform.hashCode;
}

class SimpleBloc extends Bloc<dynamic, String> {
@override
Stream<String> mapEventToState(String state, dynamic event) async* {
yield 'state';
}
}

void main() {
group('BlocProvider', () {
testWidgets('throws if initialized with no bloc',
(WidgetTester tester) async {
await tester.pumpWidget(MyAppNoBloc());
expect(tester.takeException(), isInstanceOf<AssertionError>());
});

testWidgets(
'updateShouldNotify should return false when blocs are the same',
(WidgetTester tester) async {
final _blocProviderA =
BlocProvider(bloc: CounterBloc(), child: Container());
final _blocProviderB =
BlocProvider(bloc: CounterBloc(), child: Container());

expect(_blocProviderB.updateShouldNotify(_blocProviderA), false);
});

testWidgets(
'updateShouldNotify should return true when blocs are different',
(WidgetTester tester) async {
final _blocProviderA =
BlocProvider(bloc: CounterBloc(), child: Container());
final _blocProviderB =
BlocProvider(bloc: SimpleBloc(), child: Container());

expect(_blocProviderB.updateShouldNotify(_blocProviderA), true);
});

testWidgets('passes bloc to children', (WidgetTester tester) async {
final CounterBloc _bloc = CounterBloc();
final CounterPage _child = CounterPage();
await tester.pumpWidget(MyApp(
bloc: _bloc,
child: _child,
));

final Finder _counterFinder = find.byKey((Key('counter_text')));
expect(_counterFinder, findsOneWidget);

final Text _counterText = _counterFinder.evaluate().first.widget;
expect(_counterText.data, '0');
});
});
}
Loading

0 comments on commit 5cc7321

Please sign in to comment.