diff --git a/README.md b/README.md index 672e4bc..27ca79b 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ class DemoPage extends StatelessWidget { title: new Text("Offline Demo"), ), body: OfflineBuilder( + checkHost : true, // check if internet is avaiable by a DNS request , the default value is false + // hostToCheck : "my.domaine.name", // the default value is google.com connectivityBuilder: ( BuildContext context, ConnectivityResult connectivity, diff --git a/example/lib/demo_page.dart b/example/lib/demo_page.dart index 1994d52..3410cc8 100644 --- a/example/lib/demo_page.dart +++ b/example/lib/demo_page.dart @@ -1,19 +1,14 @@ import 'package:flutter/material.dart'; class DemoPage extends StatelessWidget { - const DemoPage({ - Key key, - @required this.child, - }) : super(key: key); + const DemoPage({Key key, @required this.child}) : super(key: key); final Widget child; @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Text("Offline Demo"), - ), + appBar: AppBar(title: Text("Offline Demo")), body: child, ); } diff --git a/example/lib/main.dart b/example/lib/main.dart index f38d52e..c557067 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -13,41 +13,17 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Offline Demo', theme: ThemeData.dark(), - home: Builder( - builder: (BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - RaisedButton( - child: Text("Demo 1"), - onPressed: () { - navigate(context, Demo1()); - }, - ), - RaisedButton( - child: Text("Demo 2"), - onPressed: () { - navigate(context, Demo2()); - }, - ), - RaisedButton( - child: Text("Demo 3"), - onPressed: () { - navigate(context, Demo3()); - }, - ), - ], - ); - }, + home: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RaisedButton(child: Text("Demo 1"), onPressed: () => navigate(context, Demo1())), + RaisedButton(child: Text("Demo 2"), onPressed: () => navigate(context, Demo2())), + RaisedButton(child: Text("Demo 3"), onPressed: () => navigate(context, Demo3())), + ], ), ); } - void navigate(BuildContext context, Widget widget) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => DemoPage(child: widget), - ), - ); - } + void navigate(BuildContext context, Widget widget) => + Navigator.of(context).push(MaterialPageRoute(builder: (_) => DemoPage(child: widget))); } diff --git a/example/lib/widgets/demo_1.dart b/example/lib/widgets/demo_1.dart index 10c8d8f..60551a7 100644 --- a/example/lib/widgets/demo_1.dart +++ b/example/lib/widgets/demo_1.dart @@ -5,12 +5,8 @@ class Demo1 extends StatelessWidget { @override Widget build(BuildContext context) { return OfflineBuilder( - connectivityBuilder: ( - BuildContext context, - ConnectivityResult connectivity, - Widget child, - ) { - final bool connected = connectivity != ConnectivityResult.none; + connectivityBuilder: (BuildContext context, ConnectivityResult connectivity, Widget child) { + final bool isConnected = connectivity != ConnectivityResult.none; return Stack( fit: StackFit.expand, children: [ @@ -21,26 +17,10 @@ class Demo1 extends StatelessWidget { right: 0.0, child: AnimatedContainer( duration: const Duration(milliseconds: 350), - color: connected ? Color(0xFF00EE44) : Color(0xFFEE4400), + color: isConnected ? 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: isConnected ? Text('ONLINE') : _OfflineWidget(), ), ), ), @@ -50,14 +30,30 @@ class Demo1 extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - 'There are no bottons to push :)', - ), - Text( - 'Just turn off your internet.', - ), + Text('There are no bottons to push :)'), + Text('Just turn off your internet.'), ], ), ); } } + +class _OfflineWidget extends StatelessWidget { + const _OfflineWidget({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return 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)), + ), + ], + ); + } +} diff --git a/example/lib/widgets/demo_2.dart b/example/lib/widgets/demo_2.dart index 327d62d..e387d5b 100644 --- a/example/lib/widgets/demo_2.dart +++ b/example/lib/widgets/demo_2.dart @@ -5,36 +5,25 @@ class Demo2 extends StatelessWidget { @override Widget build(BuildContext context) { return OfflineBuilder( - connectivityBuilder: ( - BuildContext context, - ConnectivityResult connectivity, - Widget child, - ) { + 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), - ), + child: Text("Oops, \n\nNow we are Offline!", style: TextStyle(color: Colors.black)), ), ); - } else { - return child; } + + return child; }, builder: (BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - 'There are no bottons to push :)', - ), - Text( - 'Just turn off your internet.', - ), + Text('There are no bottons to push :)'), + Text('Just turn off your internet.'), ], ), ); diff --git a/example/lib/widgets/demo_3.dart b/example/lib/widgets/demo_3.dart index 5d2a9a2..9abbf9b 100644 --- a/example/lib/widgets/demo_3.dart +++ b/example/lib/widgets/demo_3.dart @@ -6,37 +6,25 @@ class Demo3 extends StatelessWidget { Widget build(BuildContext context) { return OfflineBuilder( debounceDuration: Duration.zero, - connectivityBuilder: ( - BuildContext context, - ConnectivityResult connectivity, - Widget child, - ) { + 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), - ), + child: Text("Oops, \n\nWe experienced a Delayed Offline!", style: TextStyle(color: Colors.black)), ), ); } + return child; }, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - 'There are no bottons to push :)', - ), - Text( - 'Just turn off your internet.', - ), - Text( - 'This one has a bit of a delay.', - ), + Text('There are no bottons to push :)'), + Text('Just turn off your internet.'), + Text('This one has a bit of a delay.'), ], ), ), diff --git a/lib/src/main.dart b/lib/src/main.dart index c82cb36..021d678 100644 --- a/lib/src/main.dart +++ b/lib/src/main.dart @@ -5,12 +5,15 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_offline/src/utils.dart'; const kOfflineDebounceDuration = Duration(seconds: 3); +const kHostToCheck = 'google.com'; class OfflineBuilder extends StatefulWidget { factory OfflineBuilder({ Key key, @required ValueWidgetBuilder connectivityBuilder, Duration debounceDuration = kOfflineDebounceDuration, + String hostToCheck = kHostToCheck, + bool checkHost = false, WidgetBuilder builder, Widget child, WidgetBuilder errorBuilder, @@ -20,6 +23,8 @@ class OfflineBuilder extends StatefulWidget { connectivityBuilder: connectivityBuilder, connectivityService: Connectivity(), debounceDuration: debounceDuration, + hostToCheck: hostToCheck, + checkHost: checkHost, builder: builder, child: child, errorBuilder: errorBuilder, @@ -32,6 +37,8 @@ class OfflineBuilder extends StatefulWidget { @required this.connectivityBuilder, @required this.connectivityService, this.debounceDuration = kOfflineDebounceDuration, + this.hostToCheck = kHostToCheck, + this.checkHost = false, this.builder, this.child, this.errorBuilder, @@ -40,6 +47,8 @@ class OfflineBuilder extends StatefulWidget { assert(connectivityService != null, 'connectivityService cannot be null'), assert(!(builder is WidgetBuilder && child is Widget) && !(builder == null && child == null), 'You should specify either a builder or a child'), + assert(hostToCheck != null, 'hostToCheck cannot be null'), + assert(checkHost != null, 'checkHost can only be true or false'), super(key: key); /// Override connectivity service used for testing @@ -48,6 +57,12 @@ class OfflineBuilder extends StatefulWidget { /// Debounce duration from epileptic network situations final Duration debounceDuration; + /// Hostname to test internet connection + final String hostToCheck; + + /// Decide if to use the hostname option + final bool checkHost; + /// Used for building the Offline and/or Online UI final ValueWidgetBuilder connectivityBuilder; @@ -72,8 +87,13 @@ class OfflineBuilderState extends State { super.initState(); _connectivityStream = Stream.fromFuture(widget.connectivityService.checkConnectivity()) - .asyncExpand((data) => widget.connectivityService.onConnectivityChanged.transform(startsWith(data))) - .transform(debounce(widget.debounceDuration)); + .asyncExpand((data) => widget.connectivityService.onConnectivityChanged.transform(startsWith(data))); + + if (widget.checkHost) { + _connectivityStream = _connectivityStream.transform(checkIfHostIsAvailble(widget.hostToCheck)); + } + + _connectivityStream = _connectivityStream.transform(debounce(widget.debounceDuration)); } @override diff --git a/lib/src/utils.dart b/lib/src/utils.dart index cba5858..873f4a3 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:connectivity/connectivity.dart'; @@ -55,3 +56,22 @@ StreamTransformer startsWith( }, ); } + +StreamTransformer checkIfHostIsAvailble(String host) { + return StreamTransformer.fromHandlers( + handleData: (ConnectivityResult data, EventSink sink) async { + try { + final result = await InternetAddress.lookup(host); + if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) { + sink.add(data); + return; + } + + throw SocketException(""); + } on SocketException catch (_) { + sink.add(ConnectivityResult.none); + } + }, + handleDone: (EventSink sink) => sink.close(), + ); +} diff --git a/test/flutter_offline_test.dart b/test/flutter_offline_test.dart index 6484e66..ec93059 100644 --- a/test/flutter_offline_test.dart +++ b/test/flutter_offline_test.dart @@ -74,6 +74,18 @@ void main() { }, throwsAssertionError); }); + testWidgets('Test null Host options', (WidgetTester tester) async { + expect(() { + OfflineBuilder.initialize( + connectivityService: TestConnectivityService(ConnectivityResult.none), + hostToCheck: null, + checkHost: null, + connectivityBuilder: (_, __, Widget child) => child, + child: const Text('child_result'), + ); + }, throwsAssertionError); + }); + testWidgets('Test builder & child param', (WidgetTester tester) async { expect(() { OfflineBuilder.initialize( @@ -159,6 +171,65 @@ void main() { }); }); + group("Test Check If Host", () { + testWidgets('with default host', (WidgetTester tester) async { + const debounceDuration = Duration.zero; + await tester.pumpWidget(MaterialApp( + home: OfflineBuilder.initialize( + connectivityService: + TestConnectivityService(ConnectivityResult.mobile), + checkHost: true, + debounceDuration: debounceDuration, + connectivityBuilder: (_, ConnectivityResult connectivity, __) => + Text('$connectivity'), + child: const SizedBox(), + ), + )); + + await tester.pump(debounceDuration); + expect(find.byType(SizedBox), findsOneWidget); + }); + + testWidgets('with local host', (WidgetTester tester) async { + const debounceDuration = Duration.zero; + await tester.pumpWidget(MaterialApp( + home: OfflineBuilder.initialize( + connectivityService: + TestConnectivityService(ConnectivityResult.mobile), + checkHost: true, + hostToCheck: "127.0.0.1", + debounceDuration: debounceDuration, + connectivityBuilder: (_, ConnectivityResult connectivity, __) => + Text('$connectivity'), + child: const SizedBox(), + ), + )); + + await tester.pump(debounceDuration); + expect(find.byType(SizedBox), findsOneWidget); + }); + + testWidgets('with bad host', (WidgetTester tester) async { + const debounceDuration = Duration.zero; + await tester.pumpWidget(MaterialApp( + home: OfflineBuilder.initialize( + connectivityService: TestConnectivityService(ConnectivityResult.none), + checkHost: true, + hostToCheck: "kkk", + debounceDuration: debounceDuration, + connectivityBuilder: (_, ConnectivityResult connectivity, __) { + print(connectivity); + return Text('$connectivity'); + }, + child: const SizedBox(), + ), + )); + + await tester.pump(debounceDuration); + expect(find.byType(SizedBox), findsOneWidget); + }); + }); + group("Test Debounce", () { testWidgets('Test for Debounce: Zero', (WidgetTester tester) async { final service = TestConnectivityService(ConnectivityResult.none); @@ -280,9 +351,7 @@ class TestConnectivityService implements Connectivity { Stream get onConnectivityChanged => controller.stream; @override - Future checkConnectivity() { - return Future.delayed(Duration.zero, () => initialConnection); - } + Future checkConnectivity() async => initialConnection; @override Future getWifiIP() async => '127.0.0.1'; diff --git a/test/utils_check_if_host_test.dart b/test/utils_check_if_host_test.dart new file mode 100644 index 0000000..412df66 --- /dev/null +++ b/test/utils_check_if_host_test.dart @@ -0,0 +1,105 @@ +import 'dart:async'; + +import 'package:connectivity/connectivity.dart'; +import 'package:flutter_offline/src/utils.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Future waitForTimer(int milliseconds) => Future(() { + /* ensure Timer is started*/ + }) + .then( + (_) => Future.delayed(Duration(milliseconds: milliseconds + 1)), + ); + +void main() { + group('Group', () { + StreamController values; + List emittedValues; + bool valuesCanceled; + bool isDone; + List errors; + StreamSubscription subscription; + + void setUpStreams(StreamTransformer transformer) { + final stream = () => StreamController.broadcast(); + + valuesCanceled = false; + values = stream()..onCancel = () => valuesCanceled = true; + emittedValues = []; + errors = []; + isDone = false; + subscription = values.stream.transform(transformer).listen( + emittedValues.add, + onError: errors.add, + onDone: () => isDone = true, + ); + } + + void cleanUpStreams() { + valuesCanceled = null; + values = null; + emittedValues = null; + errors = null; + isDone = null; + subscription = null; + } + + group('when host is ok', () { + setUp(() async { + setUpStreams(checkIfHostIsAvailble("127.0.0.1")); + }); + + tearDown(() { + cleanUpStreams(); + }); + + test('cancels values', () async { + await subscription.cancel(); + expect(valuesCanceled, true); + }); + + test('works as expected', () async { + values.add(ConnectivityResult.wifi); + await waitForTimer(5); + expect(emittedValues, [ConnectivityResult.wifi]); + }); + + test('waits for pending value to close', () async { + values.add(ConnectivityResult.mobile); + await waitForTimer(5); + await values.close(); + await Future(() {}); + expect(isDone, true); + }); + }); + + group('when host is not ok', () { + setUp(() async { + setUpStreams(checkIfHostIsAvailble("fake")); + }); + + tearDown(() { + cleanUpStreams(); + }); + + test('cancels values', () async { + await subscription.cancel(); + expect(valuesCanceled, true); + }); + + test('returns a ConnectivityResult.none', () async { + values.add(ConnectivityResult.wifi); + await waitForTimer(5); + expect(emittedValues, [ConnectivityResult.none]); + }); + + test('waits for pending value to close', () async { + values.add(ConnectivityResult.mobile); + await waitForTimer(5); + await values.close(); + await Future(() {}); + expect(isDone, true); + }); + }); + }); +}