Skip to content

Instantly share code, notes, and snippets.

@buntagonalprism
Last active May 1, 2019 00:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save buntagonalprism/523610a53f4ec66c2f43acb9a7849a9b to your computer and use it in GitHub Desktop.
Save buntagonalprism/523610a53f4ec66c2f43acb9a7849a9b to your computer and use it in GitHub Desktop.
// my_feature.dart
class MyFeatureLauncher {
final _factory = di<MyFeatureBlocFactory>();
void show(BuildContext context) {
Navigator.of(context).push(MaterialPageRoute(
settings: RouteSettings(name: '/my-feature'),
builder: (context) => BlocProvider(
block: _factory.init(diCon<Strings>(context)),
child: MyFeatureScreen()
),
));
}
}
class MyFeatureScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("My Feature"),
),
body: MyFeatureView(),
);
}
}
class MyFeatureView extends StatefulWidget {
@override
_MyFeatureViewState createState() => _MyFeatureViewState();
}
class _MyFeatureViewState extends State<MyFeatureView> {
MyFeatureBloc bloc;
@override
Widget build(BuildContext context) {
final bloc = diCon<MyFeatureBloc>(context);
return ValueStreamBuilder(
valueStream: bloc.data
builder: (context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data)
}
return Text(snapshot.data);
}
);
}
}
// my_feature_bloc.dart
class MyFeatureBlocFactory {
MyFeatureBloc init(Strings strings) {
return MyFeatureBloc._(strings);
}
}
class MyFeatureBloc extends BaseBloc {
final ValueStream<String> data;
final Strings strings;
MyFeatureBloc._(this.strings) {
data.add("Hello World")
}
@override
void dispose() {
super.dipose();
// Close any resources that need closing.
}
}
// my_feature_test.dart
// This means we don't need a monstrous bloc
class _BlocMock extends Mock implements MyFeatureBloc {}
class _FactoryMock extends Mock implements MyFeatureBlocFactory {}
class _AnotherFeatureMock extends Mock implements AnotherFeatureLauncher {}
void main() {
MyFeatureBloc bloc;
TestValueStreamController<String> controller;
setup(() {
bloc = _BlockMock();
controller = TestValueStreamController<String>();
applyDiOverride<MyfeatureBloc>();
when(bloc.data).thenAnswer((_) => controller.stream);
});
testWidgets('Launcher creates bloc provider containing screen', (WidgetTester tester) async {
final _factory = _FactoryMock();
applyDiOverride<MyFeatureBlocFactory>(factory);
when(_factory.init(any)).thenReturn(bloc);
await pumpTestLauncher(tester, (context) {
MyFeatureLauncher.show(context);
});
// Validate bloc was created. Check any arguments that should be passed to the bloc here
verify(_factory.init(any)).called(1);
Finder blocFinder = find.byType(blocProviderType<MyFeatureBloc>(bloc));
expect(blocFinder, findsOneWidget);
BlocProvider blocProvider = tester.widget(blocFinder);
expect(blocProvider.bloc, bloc);
Finder screenFinder = find.descendant(of: blocFinder, matching: find.byType(ChatScreen));
expect(screenFinder, findsOneWidget);
// Validate any arguments which should have been passed down to the screen here
MyFeatureScreen screen = tester.widget(screenFinder);
});
testWidgets('Screen displays loading and data', (WidgetTester tester) {
await pumpTestApp(tester, MyFeatureScreen());
// Expect initially loading
Finder loading = find.byType(CircularProgressIndicator);
expect(loading, findsOneWidget);
String testData = "Some data";
Finder text = find.text(testData);
expect(text, findsNothing);
// Expect loading gone when data displayed
await controller.add(tester, "Some data");
expect(loading, findsNothing);
expect(text, findsOneWidget);
});
testWidgets('Screen navigates to another screen', (WidgetTester tester) {
final _launcher = _AnotherFeatureMock();
applyDiOverride<AnotherFeatureLauncher>(_launcher);
await pumpTestApp(tester, MyFeatureScreen());
verifyNever(_launcher.show(any)).never()
// Tap button should call launcher
Finder goToScreenBtn = find.byType(RaisedButton);
await tester.tap(goToScreenBtn);
verify(_launcher.show(any)).called(1);
});
}
// bloc test
Type blocProviderType<T extends BaseBloc>(T mockBloc) {
return BlocProvider(
bloc: mockBloc,
child: Container(),
).runtimeType;
}
class TestValueStreamController<T> {
final _controller = StreamController<T>.broadcast();
final _valueStream = ValueStream(_controller.stream);
Future add(WidgetTester tester, T data) {
_controller.add(data);
return _doublePump(tester);
}
Future addError(WidgetTester tester, Object error) {
_controller.addError(error);
return _doublePump(tester);
}
Future _doublePump(WidgetTester tester) async {
await tester.pump();
await tester.pump();
}
ValueStream<T> get broadcastStream => _valueStream;
}
/// A simple stream wrapper that makes available the last value output by the stream
/// When combined with ValueStreamBuilder, this avoids the single-frame flicker of
/// native StreamBuilder by always supplying initial data for the first build pass.
class ValueStream<T> {
Stream<T> _stream;
Stream<T> get stream => _stream;
T _value;
T get value => _value;
ValueStream(Stream<T> sourceStream, [T initialData]) {
_value = initialData;
_stream = sourceStream.map((data) {
_value = data;
return data;
});
}
}
/// A simple wrapper around the flutter native StreamBuilder that accepts a
/// ValueStream, and automatically uses the value from the ValueStream as the
/// initial data for the first pass build.
///
/// Stream builders by design do a build using the supplied initial data, before
/// any events from the data stream are received.
class ValueStreamBuilder<T> extends StatelessWidget {
final ValueStream<T> valueStream;
final AsyncWidgetBuilder<T> builder;
ValueStreamBuilder({@required this.valueStream, @required this.builder});
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: valueStream.stream,
initialData: valueStream.value,
builder: builder,
);
}
}
/// Widget used to make a bloc instance easily accessible to child widgets
/// The bloc instance first passed to the stateful element is kept within the widget state,
/// it is not replaced duirng rebuild.
/// An inherited widget is created internally to provide efficient lookup using
/// BlocProvider.of<BlocType>(context)
class BlocProvider<T extends BaseBloc> extends StatefulWidget {
final T bloc;
final Widget child;
BlocProvider({@required this.bloc, @required this.child});
/// Use this method to obtain a view model of a given type.
static T of<T extends BaseBloc>(BuildContext context) {
_BlocInherited<T> inherited = context.inheritFromWidgetOfExactType(_BlocInherited<T>().runtimeType);
return inherited.bloc;
}
@override
_BlocProviderState<T> createState() => new _BlocProviderState<T>();
}
class _BlocProviderState<T extends BaseBloc> extends State<BlocProvider> {
T bloc;
@override
void initState() {
bloc = widget.bloc;
super.initState();
}
@override
Widget build(BuildContext context) {
return new _BlocInherited<T>(
bloc: bloc,
child: widget.child,
);
}
@override
void dispose() {
super.dispose();
bloc.dispose();
}
}
class _BlocInherited<T extends BaseBloc> extends InheritedWidget {
final T bloc;
_BlocInherited({Key key, this.bloc, Widget child})
: super(key: key, child: child);
@override
bool updateShouldNotify(_BlocInherited<T> old) {
return true;
}
}
abstract class BaseBloc {
void dispose();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment