Last active
May 1, 2019 00:11
-
-
Save buntagonalprism/523610a53f4ec66c2f43acb9a7849a9b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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