diff --git a/README.md b/README.md index 691d645..25d607d 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ [![pub package](https://img.shields.io/pub/v/flutter_offline.svg)](https://pub.dartlang.org/packages/flutter_offline) -Handle offline/online connectivity in Flutter like a Boss. Ample support for both iOS and Android platforms. +A tidy utility to handle offline/online connectivity like a Boss. It provides support for both iOS and Android platforms (offcourse). ## 🎖 Installing ```yaml dependencies: - flutter_offline: "^0.1.0" + flutter_offline: "^0.2.0" ``` ### ⚡️ Import @@ -23,38 +23,7 @@ import 'package:flutter_offline/flutter_offline.dart'; import 'package:flutter/material.dart'; import 'package:flutter_offline/flutter_offline.dart'; -class OfflineDelegate extends OfflineBuilderDelegate { - @override - Widget builder(BuildContext context, bool state) { - return new Stack( - fit: StackFit.expand, - children: [ - Positioned( - height: 24.0, - left: 0.0, - right: 0.0, - child: Container( - color: state ? Color(0xFF00EE44) : Color(0xFFEE4400), - child: Center( - child: Text("${state ? 'ONLINE' : 'OFFLINE'}"), - ), - ), - ), - Center( - child: new Text( - 'Yay!', - ), - ), - ], - ); - } -} - class DemoPage extends StatelessWidget { - const DemoPage({ - Key key, - }) : super(key: key); - @override Widget build(BuildContext context) { return new Scaffold( @@ -62,7 +31,45 @@ class DemoPage extends StatelessWidget { title: new Text("Offline Demo"), ), body: OfflineBuilder( - delegate: new OfflineDelegate(), + connectivityBuilder: ( + BuildContext context, + ConnectivityResult connectivity, + Widget child, + ) { + final bool connected = connectivity != ConnectivityResult.none; + return new Stack( + fit: StackFit.expand, + children: [ + Positioned( + height: 24.0, + left: 0.0, + right: 0.0, + child: Container( + color: connected ? Color(0xFF00EE44) : Color(0xFFEE4400), + child: Center( + child: Text("${connected ? 'ONLINE' : 'OFFLINE'}"), + ), + ), + ), + Center( + child: new Text( + 'Yay!', + ), + ), + ], + ); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Text( + 'There are no bottons to push :)', + ), + new Text( + 'Just turn off your internet.', + ), + ], + ), ), ); } @@ -76,13 +83,13 @@ For more info, please, refer to the `main.dart` in the example.
- + - + - +
@@ -100,6 +107,10 @@ For help getting started with Flutter, view our online For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). +### 🤓 Mentions + +Simon Lightfoot ([@slightfoot](https://github.com/slightfoot)) is just awesome 👍. + ## ⭐️ License MIT License diff --git a/analysis_options.yaml b/analysis_options.yaml index f38e330..ceb3e86 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -28,7 +28,7 @@ analyzer: implicit-dynamic: false errors: # treat missing required parameters as a warning (not a hint) - missing_required_param: warning + # missing_required_param: warning # treat missing returns as a warning (not a hint) missing_return: warning # allow having TODOs in the code diff --git a/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java deleted file mode 100644 index 4c118a2..0000000 --- a/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.flutter.plugins; - -import io.flutter.plugin.common.PluginRegistry; -import io.flutter.plugins.connectivity.ConnectivityPlugin; - -/** - * Generated file. Do not edit. - */ -public final class GeneratedPluginRegistrant { - public static void registerWith(PluginRegistry registry) { - if (alreadyRegisteredWith(registry)) { - return; - } - ConnectivityPlugin.registerWith(registry.registrarFor("io.flutter.plugins.connectivity.ConnectivityPlugin")); - } - - private static boolean alreadyRegisteredWith(PluginRegistry registry) { - final String key = GeneratedPluginRegistrant.class.getCanonicalName(); - if (registry.hasPlugin(key)) { - return true; - } - registry.registrarFor(key); - return false; - } -} diff --git a/android/local.properties b/android/local.properties deleted file mode 100644 index 87e390a..0000000 --- a/android/local.properties +++ /dev/null @@ -1,3 +0,0 @@ -sdk.dir=/Users/apple/Library/Android/sdk -flutter.sdk=/Users/apple/Desktop/flutter -flutter.versionName=0.0.1 \ No newline at end of file diff --git a/example/lib/delegates/demo_1.dart b/example/lib/delegates/demo_1.dart deleted file mode 100644 index 78f91c2..0000000 --- a/example/lib/delegates/demo_1.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_offline/flutter_offline.dart'; - -class Demo1OfflineDelegate extends OfflineBuilderDelegate { - @override - Widget builder(BuildContext context, bool state) { - return new Stack( - fit: StackFit.expand, - children: [ - Positioned( - height: 24.0, - left: 0.0, - right: 0.0, - child: Container( - color: state ? Color(0xFF00EE44) : Color(0xFFEE4400), - child: Center( - child: Text("${state ? 'ONLINE' : 'OFFLINE'}"), - ), - ), - ), - new Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - new Text( - 'There are no bottons to push :)', - ), - new Text( - 'Just turn off your internet.', - ), - ], - ), - ], - ); - } -} diff --git a/example/lib/delegates/demo_2.dart b/example/lib/delegates/demo_2.dart deleted file mode 100644 index 180d2c3..0000000 --- a/example/lib/delegates/demo_2.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_offline/flutter_offline.dart'; - -class Demo2OfflineDelegate extends OfflineBuilderDelegate { - @override - Widget offlineBuilder(BuildContext context, bool state) { - return Container( - color: Colors.white, - child: Center( - child: Text( - "Oops, \n\nNow we are Offline!", - style: TextStyle(color: Colors.black), - ), - ), - ); - } - - @override - Widget builder(BuildContext context, bool state) { - return Center( - child: new Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - new Text( - 'There are no bottons to push :)', - ), - new Text( - 'Just turn off your internet.', - ), - ], - ), - ); - } -} diff --git a/example/lib/delegates/demo_3.dart b/example/lib/delegates/demo_3.dart deleted file mode 100644 index 86e6a98..0000000 --- a/example/lib/delegates/demo_3.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_offline/flutter_offline.dart'; - -class Demo3OfflineDelegate extends OfflineBuilderDelegate { - @override - Duration delay = Duration(seconds: 3); - - @override - Widget offlineBuilder(BuildContext context, bool state) { - return Container( - color: Colors.white70, - child: Center( - child: Text( - "Oops, \n\nWe experienced a Delayed Offline!", - style: TextStyle(color: Colors.black), - ), - ), - ); - } - - @override - Widget builder(BuildContext context, bool state) { - return Center( - child: new Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - new Text( - 'There are no bottons to push :)', - ), - new Text( - 'Just turn off your internet.', - ), - new Text( - 'This one has a bit of a delay.', - ), - ], - ), - ); - } -} diff --git a/example/lib/demo_page.dart b/example/lib/demo_page.dart index 25a99ee..abde482 100644 --- a/example/lib/demo_page.dart +++ b/example/lib/demo_page.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter_offline/flutter_offline.dart'; class DemoPage extends StatelessWidget { const DemoPage({ Key key, - @required this.delegate, + @required this.child, }) : super(key: key); - final OfflineBuilderDelegate delegate; + final Widget child; @override Widget build(BuildContext context) { @@ -15,9 +14,7 @@ class DemoPage extends StatelessWidget { appBar: new AppBar( title: new Text("Offline Demo"), ), - body: OfflineBuilder( - delegate: delegate, - ), + body: child, ); } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 4b23241..95beec0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,23 +1,11 @@ -import 'package:example/delegates/demo_1.dart'; -import 'package:example/delegates/demo_2.dart'; -import 'package:example/delegates/demo_3.dart'; -import 'package:example/demo_page.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_offline/flutter_offline.dart'; -void main() => runApp(new MyApp()); +import './demo_page.dart'; +import './widgets/demo_1.dart'; +import './widgets/demo_2.dart'; +import './widgets/demo_3.dart'; -void navigate(BuildContext context, OfflineBuilderDelegate delegate) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return new DemoPage( - delegate: delegate, - ); - }, - ), - ); -} +void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override @@ -33,19 +21,19 @@ class MyApp extends StatelessWidget { RaisedButton( child: Text("Demo 1"), onPressed: () { - navigate(context, Demo1OfflineDelegate()); + navigate(context, Demo1()); }, ), RaisedButton( child: Text("Demo 2"), onPressed: () { - navigate(context, Demo2OfflineDelegate()); + navigate(context, Demo2()); }, ), RaisedButton( child: Text("Demo 3"), onPressed: () { - navigate(context, Demo3OfflineDelegate()); + navigate(context, Demo3()); }, ), ], @@ -54,4 +42,12 @@ class MyApp extends StatelessWidget { ), ); } + + void navigate(BuildContext context, Widget widget) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => DemoPage(child: widget), + ), + ); + } } diff --git a/example/lib/widgets/demo_1.dart b/example/lib/widgets/demo_1.dart new file mode 100644 index 0000000..7809967 --- /dev/null +++ b/example/lib/widgets/demo_1.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_offline/flutter_offline.dart'; + +class Demo1 extends StatelessWidget { + @override + Widget build(BuildContext context) { + return OfflineBuilder( + connectivityBuilder: ( + BuildContext context, + ConnectivityResult connectivity, + Widget child, + ) { + final bool connected = connectivity != ConnectivityResult.none; + return Stack( + fit: StackFit.expand, + children: [ + child, + Positioned( + height: 32.0, + left: 0.0, + right: 0.0, + child: AnimatedContainer( + duration: const Duration(milliseconds: 350), + color: connected ? Color(0xFF00EE44) : Color(0xFFEE4400), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + child: connected + ? Text('ONLINE') + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('OFFLINE'), + SizedBox(width: 8.0), + SizedBox( + width: 12.0, + height: 12.0, + child: CircularProgressIndicator( + strokeWidth: 2.0, + valueColor: + AlwaysStoppedAnimation(Colors.white), + ), + ), + ], + ), + ), + ), + ), + ], + ); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Text( + 'There are no bottons to push :)', + ), + new Text( + 'Just turn off your internet.', + ), + ], + ), + ); + } +} diff --git a/example/lib/widgets/demo_2.dart b/example/lib/widgets/demo_2.dart new file mode 100644 index 0000000..5028cb1 --- /dev/null +++ b/example/lib/widgets/demo_2.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_offline/flutter_offline.dart'; + +class Demo2 extends StatelessWidget { + @override + Widget build(BuildContext context) { + return OfflineBuilder( + connectivityBuilder: ( + BuildContext context, + ConnectivityResult connectivity, + Widget child, + ) { + if (connectivity == ConnectivityResult.none) { + return Container( + color: Colors.white, + child: Center( + child: Text( + "Oops, \n\nNow we are Offline!", + style: TextStyle(color: Colors.black), + ), + ), + ); + } else { + return child; + } + }, + builder: (BuildContext context) { + return Center( + child: new Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Text( + 'There are no bottons to push :)', + ), + new Text( + 'Just turn off your internet.', + ), + ], + ), + ); + }, + ); + } +} diff --git a/example/lib/widgets/demo_3.dart b/example/lib/widgets/demo_3.dart new file mode 100644 index 0000000..6ca8033 --- /dev/null +++ b/example/lib/widgets/demo_3.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_offline/flutter_offline.dart'; + +class Demo3 extends StatelessWidget { + @override + Widget build(BuildContext context) { + return OfflineBuilder( + debounceDuration: Duration.zero, + connectivityBuilder: ( + BuildContext context, + ConnectivityResult connectivity, + Widget child, + ) { + if (connectivity == ConnectivityResult.none) { + return Container( + color: Colors.white70, + child: Center( + child: Text( + "Oops, \n\nWe experienced a Delayed Offline!", + style: TextStyle(color: Colors.black), + ), + ), + ); + } + return child; + }, + child: Center( + child: new Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Text( + 'There are no bottons to push :)', + ), + new Text( + 'Just turn off your internet.', + ), + new Text( + 'This one has a bit of a delay.', + ), + ], + ), + ), + ); + } +} diff --git a/lib/flutter_offline.dart b/lib/flutter_offline.dart index 6204ec8..ba26015 100644 --- a/lib/flutter_offline.dart +++ b/lib/flutter_offline.dart @@ -1,9 +1,4 @@ library flutter_offline; -import 'dart:async'; - -import 'package:connectivity/connectivity.dart'; -import 'package:flutter/material.dart'; - -part 'src/delegate.dart'; -part 'src/main.dart'; +export 'package:connectivity/connectivity.dart' show ConnectivityResult; +export 'src/main.dart'; diff --git a/lib/src/delegate.dart b/lib/src/delegate.dart deleted file mode 100644 index c301696..0000000 --- a/lib/src/delegate.dart +++ /dev/null @@ -1,15 +0,0 @@ -part of flutter_offline; - -abstract class OfflineBuilderDelegate { - Duration get delay => const Duration(milliseconds: 350); - - Widget waitBuilder(BuildContext context) { - return Center(child: CircularProgressIndicator()); - } - - Widget offlineBuilder(BuildContext context, bool state) { - return null; - } - - Widget builder(BuildContext context, bool state); -} diff --git a/lib/src/main.dart b/lib/src/main.dart index d97eefb..4bbd570 100644 --- a/lib/src/main.dart +++ b/lib/src/main.dart @@ -1,48 +1,107 @@ -part of flutter_offline; +import 'dart:async'; -class OfflineBuilder extends StatefulWidget { - final OfflineBuilderDelegate delegate; +import 'package:connectivity/connectivity.dart'; +import 'package:flutter/widgets.dart'; + +const kOfflineDebounceDuration = const Duration(seconds: 3); + +typedef Widget ConnectivityBuilder( + BuildContext context, ConnectivityResult connectivity, Widget child); +abstract class ConnectivityService { + Stream get onConnectivityChanged; +} + +class OfflineBuilder extends StatefulWidget { const OfflineBuilder({ Key key, - @required this.delegate, - }) : super(key: key); + @required this.connectivityBuilder, + this.connectivityService, + this.debounceDuration = kOfflineDebounceDuration, + this.builder, + this.errorBuilder, + this.child, + }) : assert(builder == null || child == null, + 'builder and child, cannot both be null'), + // TODO + // assert(builder != null && child != null, 'You can only specify builder or child, not both'), + super(key: key); + + /// Override connectivity service used for testing + final ConnectivityService connectivityService; + + /// Debounce duration from epileptic network situations + final Duration debounceDuration; + + final ConnectivityBuilder connectivityBuilder; + final WidgetBuilder builder; + final Widget child; + final WidgetBuilder errorBuilder; @override - OfflineBuilderState createState() => new OfflineBuilderState(); + OfflineBuilderState createState() => OfflineBuilderState(); } class OfflineBuilderState extends State { - final _connectivity = new Connectivity(); + Stream _connectivityStream; + bool _seenFirstData = false; + Timer _debounceTimer; @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _connectivity.checkConnectivity(), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return widget.delegate.waitBuilder(context); - } - return new StreamBuilder( - initialData: snapshot.data, - stream: _connectivity.onConnectivityChanged.distinct().asyncMap( - (event) => Future.delayed(widget.delegate.delay, () => event), - ), - builder: (context, snapshot) { - final _state = snapshot.data != ConnectivityResult.none; - - final _offlineView = _state == false - ? widget.delegate.offlineBuilder(context, _state) - : null; - - if (_offlineView != null) { - return _offlineView; + void initState() { + super.initState(); + Stream stream; + if (widget.connectivityService != null) { + stream = widget.connectivityService.onConnectivityChanged; + } else { + stream = Connectivity().onConnectivityChanged; + } + _connectivityStream = stream.distinct().transform(StreamTransformer + .fromHandlers( + handleData: + (ConnectivityResult data, EventSink sink) { + if (_seenFirstData) { + _debounceTimer?.cancel(); + _debounceTimer = + Timer(widget.debounceDuration, () => sink.add(data)); + } else { + sink.add(data); + _seenFirstData = true; } - - return widget.delegate.builder(context, _state); }, - ); + handleDone: (EventSink sink) => + _debounceTimer?.cancel(), + )); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: _connectivityStream, + builder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + final child = widget.child ?? widget.builder(context); + if (!snapshot.hasData && !snapshot.hasError) { + return SizedBox(); + } else if (snapshot.hasError) { + if (widget.errorBuilder != null) { + return widget.errorBuilder(context); + } + throw new OfflineBuilderError(snapshot.error); + } + return widget.connectivityBuilder(context, snapshot.data, child); }, ); } } + +class OfflineBuilderError extends Error { + final Object error; + + OfflineBuilderError(this.error); + + @override + String toString() => error.toString(); +} diff --git a/pubspec.yaml b/pubspec.yaml index 5e99cb6..423e85b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_offline -description: Handle offline/online connectivity in Flutter like a Boss. -version: 0.1.0 +description: A tidy utility to handle offline/online connectivity like a Boss. +version: 0.2.0 author: Jeremiah Ogbomo homepage: https://github.com/jogboms/flutter_offline diff --git a/screenshots/demo_1.gif b/screenshots/demo_1.gif index 541f870..2551ea3 100644 Binary files a/screenshots/demo_1.gif and b/screenshots/demo_1.gif differ diff --git a/test/flutter_offline_test.dart b/test/flutter_offline_test.dart index ab73b3a..c0f32da 100644 --- a/test/flutter_offline_test.dart +++ b/test/flutter_offline_test.dart @@ -1 +1,221 @@ -void main() {} +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_offline/flutter_offline.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Test builder runs builder param', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: OfflineBuilder( + connectivityService: TestConnectivityService(ConnectivityResult.none), + connectivityBuilder: (_, __, Widget child) => child, + builder: (BuildContext context) => Text('builder_result'), + ), + )); + await tester.pump(kOfflineDebounceDuration); + expect(find.text('builder_result'), findsOneWidget); + }); + + testWidgets('Test builder passes back child param', + (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: OfflineBuilder( + connectivityService: TestConnectivityService(ConnectivityResult.none), + connectivityBuilder: (_, __, Widget child) => child, + child: Text('child_result'), + ), + )); + await tester.pump(kOfflineDebounceDuration); + expect(find.text('child_result'), findsOneWidget); + }); + + group("Test Status", () { + testWidgets('Test builder offline', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: OfflineBuilder( + connectivityService: TestConnectivityService(ConnectivityResult.none), + connectivityBuilder: (_, ConnectivityResult connectivity, __) => + Text('$connectivity'), + child: SizedBox(), + ), + )); + await tester.pump(kOfflineDebounceDuration); + expect(find.text('ConnectivityResult.none'), findsOneWidget); + }); + + testWidgets('Test builder online', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: OfflineBuilder( + connectivityService: + TestConnectivityService(ConnectivityResult.mobile), + connectivityBuilder: (_, ConnectivityResult connectivity, __) => + Text('$connectivity'), + child: SizedBox(), + ), + )); + await tester.pump(kOfflineDebounceDuration); + expect(find.text('ConnectivityResult.mobile'), findsOneWidget); + }); + }); + + group("Test Flipper", () { + testWidgets('Test builder flips online to offline', + (WidgetTester tester) async { + final service = TestConnectivityService(ConnectivityResult.mobile); + await tester.pumpWidget(MaterialApp( + home: OfflineBuilder( + connectivityService: service, + connectivityBuilder: (_, ConnectivityResult connectivity, __) => + Text('$connectivity'), + child: SizedBox(), + ), + )); + + await tester.pump(kOfflineDebounceDuration); + expect(find.text('ConnectivityResult.mobile'), findsOneWidget); + + service.result = ConnectivityResult.none; + await tester.pump(kOfflineDebounceDuration); + expect(find.text('ConnectivityResult.none'), findsOneWidget); + }); + + testWidgets('Test builder flips offline to online', + (WidgetTester tester) async { + final service = TestConnectivityService(ConnectivityResult.none); + await tester.pumpWidget(MaterialApp( + home: OfflineBuilder( + connectivityService: service, + connectivityBuilder: (_, ConnectivityResult connectivity, __) => + Text('$connectivity'), + child: SizedBox(), + ), + )); + + await tester.pump(kOfflineDebounceDuration); + expect(find.text('ConnectivityResult.none'), findsOneWidget); + + service.result = ConnectivityResult.wifi; + await tester.pump(kOfflineDebounceDuration); + expect(find.text('ConnectivityResult.wifi'), findsOneWidget); + }); + }); + + group("Test Debounce", () { + testWidgets('Test for Debounce: Zero', (WidgetTester tester) async { + final service = TestConnectivityService(ConnectivityResult.none); + const debounceDuration = Duration.zero; + await tester.pumpWidget(MaterialApp( + home: OfflineBuilder( + connectivityService: service, + debounceDuration: debounceDuration, + connectivityBuilder: (_, ConnectivityResult connectivity, __) => + Text('$connectivity'), + child: SizedBox(), + ), + )); + + service.result = ConnectivityResult.wifi; + await tester.pump(debounceDuration); + expect(find.text('ConnectivityResult.wifi'), findsOneWidget); + service.result = ConnectivityResult.mobile; + await tester.pump(debounceDuration); + expect(find.text('ConnectivityResult.mobile'), findsOneWidget); + service.result = ConnectivityResult.none; + await tester.pump(debounceDuration); + expect(find.text('ConnectivityResult.none'), findsOneWidget); + service.result = ConnectivityResult.wifi; + await tester.pump(debounceDuration); + expect(find.text('ConnectivityResult.wifi'), findsOneWidget); + }); + + testWidgets('Test for Debounce: 5 seconds', (WidgetTester tester) async { + final service = TestConnectivityService(ConnectivityResult.none); + const debounceDuration = const Duration(seconds: 5); + await tester.pumpWidget(MaterialApp( + home: OfflineBuilder( + connectivityService: service, + debounceDuration: debounceDuration, + connectivityBuilder: (_, ConnectivityResult connectivity, __) => + Text('$connectivity'), + child: SizedBox(), + ), + )); + + service.result = ConnectivityResult.wifi; + await tester.pump(Duration.zero); + expect(find.text('ConnectivityResult.none'), findsOneWidget); + service.result = ConnectivityResult.mobile; + await tester.pump(Duration.zero); + expect(find.text('ConnectivityResult.none'), findsOneWidget); + service.result = ConnectivityResult.none; + await tester.pump(Duration.zero); + expect(find.text('ConnectivityResult.none'), findsOneWidget); + service.result = ConnectivityResult.wifi; + await tester.pump(debounceDuration); + expect(find.text('ConnectivityResult.wifi'), findsOneWidget); + }); + }); + + group("Test Platform Errors", () { + testWidgets('Test w/o errorBuilder', (WidgetTester tester) async { + final service = TestConnectivityService(ConnectivityResult.none); + + await tester.pumpWidget(MaterialApp( + home: OfflineBuilder( + connectivityService: service, + connectivityBuilder: (_, ConnectivityResult connectivity, __) => + Text('$connectivity'), + child: SizedBox(), + ), + )); + + service.addError(); + await tester.pump(kOfflineDebounceDuration); + expect(tester.takeException(), isInstanceOf()); + }); + + testWidgets('Test w/ errorBuilder', (WidgetTester tester) async { + final service = TestConnectivityService(ConnectivityResult.none); + + await tester.pumpWidget(MaterialApp( + home: OfflineBuilder( + connectivityService: service, + connectivityBuilder: (_, ConnectivityResult connectivity, __) => + Text('$connectivity'), + child: SizedBox(), + errorBuilder: (context) => Text('Error'), + ), + )); + + service.addError(); + await tester.pump(kOfflineDebounceDuration); + expect(find.text("Error"), findsOneWidget); + }); + }); +} + +class TestConnectivityService extends ConnectivityService { + StreamController _controller; + ConnectivityResult _result = ConnectivityResult.none; + + TestConnectivityService([ConnectivityResult result]) { + _result = result; + _controller = StreamController.broadcast( + onListen: () => _controller.add(_result), + ); + } + + set result(ConnectivityResult result) { + _result = result; + _controller.add(result); + } + + void addError() { + _controller.addError('Error'); + } + + @override + Stream get onConnectivityChanged => _controller.stream; +}