├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── lib │ ├── demo_page.dart │ ├── main.dart │ └── widgets │ │ ├── demo_1.dart │ │ ├── demo_2.dart │ │ └── demo_3.dart ├── pubspec.lock ├── pubspec.yaml └── test │ └── widget_test.dart ├── flutter_offline.iml ├── lib ├── flutter_offline.dart └── src │ ├── main.dart │ └── utils.dart ├── makefile ├── pubspec.lock ├── pubspec.yaml ├── screenshots ├── demo_1.gif ├── demo_2.gif └── demo_3.gif └── test ├── flutter_offline_test.dart ├── utils_debounce_test.dart └── utils_starts_with_test.dart /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['www.paypal.me/jogboms'] 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Format, Analyze and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | schedule: 11 | # runs the CI weekly 12 | - cron: "0 0 * * 0" 13 | 14 | jobs: 15 | default_run: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-java@v3 21 | with: 22 | distribution: 'zulu' 23 | java-version: '17' 24 | - uses: subosito/flutter-action@v2 25 | with: 26 | channel: 'stable' 27 | cache: true 28 | - run: flutter pub get 29 | - run: dart format --set-exit-if-changed -l 120 lib -l 120 example 30 | - run: flutter analyze lib example 31 | - run: flutter test --no-pub --coverage 32 | 33 | - name: Upload coverage to codecov 34 | uses: codecov/codecov-action@v4 35 | with: 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | fail_ci_if_error: true 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | .idea 4 | 5 | .packages 6 | .pub/ 7 | .idea/ 8 | 9 | build/ 10 | ios/ 11 | ios/.generated/ 12 | ios/Flutter/Generated.xcconfig 13 | ios/Runner/GeneratedPluginRegistrant.* 14 | /coverage 15 | /.flutter-plugins 16 | /.flutter-plugins-dependencies 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [5.0.0] 2 | 3 | Bump `package:connectivity_plus` to `^6.1.4` 4 | Bump `package:network_info_plus` to `^6.1.4` 5 | 6 | ## [4.0.0] 7 | 8 | Bump `package:connectivity_plus` to `^6.0.3` 9 | Bump `package:network_info_plus` to `^5.0.3` 10 | 11 | ## [3.0.1] 12 | 13 | Bump `package:connectivity_plus` to `^5.0.1` 14 | 15 | ## [3.0.0] 16 | 17 | Bumped dependencies to support Flutter 3 with Dart 3 18 | 19 | ## [2.1.0] 20 | 21 | - Migrate dependencies to plus packages. `package:connectivity_plus` and `package:network_info_plus` 22 | 23 | ## [2.0.0] 24 | 25 | - Migrate to null-safety 26 | 27 | ## [1.0.0] 28 | 29 | - Improve network and wifi detection 30 | - Initial stable release 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jeremiah Ogbomo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ✈️ Flutter Offline 2 | 3 | [![Format, Analyze and Test](https://github.com/jogboms/flutter_offline/actions/workflows/main.yml/badge.svg)](https://github.com/jogboms/flutter_offline/actions/workflows/main.yml) [![codecov](https://codecov.io/gh/jogboms/flutter_offline/branch/master/graph/badge.svg)](https://codecov.io/gh/jogboms/flutter_offline) [![pub package](https://img.shields.io/pub/v/flutter_offline.svg)](https://pub.dartlang.org/packages/flutter_offline) 4 | 5 | A tidy utility to handle offline/online connectivity like a Boss. It provides support for both iOS and Android platforms (offcourse). 6 | 7 | ## 🎖 Installing 8 | 9 | ```yaml 10 | dependencies: 11 | flutter_offline: "^4.0.0" 12 | ``` 13 | 14 | ### ⚡️ Import 15 | 16 | ```dart 17 | import 'package:flutter_offline/flutter_offline.dart'; 18 | ``` 19 | 20 | ### ✔ Add Permission to Manifest 21 | 22 | ```dart 23 | 24 | ``` 25 | 26 | ## 🎮 How To Use 27 | 28 | ```dart 29 | import 'package:flutter/material.dart'; 30 | import 'package:flutter_offline/flutter_offline.dart'; 31 | 32 | class DemoPage extends StatelessWidget { 33 | @override 34 | Widget build(BuildContext context) { 35 | return new Scaffold( 36 | appBar: new AppBar( 37 | title: new Text("Offline Demo"), 38 | ), 39 | body: OfflineBuilder( 40 | connectivityBuilder: ( 41 | BuildContext context, 42 | List connectivity, 43 | Widget child, 44 | ) { 45 | final bool connected = !connectivity.contains(ConnectivityResult.none); 46 | return new Stack( 47 | fit: StackFit.expand, 48 | children: [ 49 | Positioned( 50 | height: 24.0, 51 | left: 0.0, 52 | right: 0.0, 53 | child: Container( 54 | color: connected ? Color(0xFF00EE44) : Color(0xFFEE4400), 55 | child: Center( 56 | child: Text("${connected ? 'ONLINE' : 'OFFLINE'}"), 57 | ), 58 | ), 59 | ), 60 | Center( 61 | child: new Text( 62 | 'Yay!', 63 | ), 64 | ), 65 | ], 66 | ); 67 | }, 68 | child: Column( 69 | mainAxisAlignment: MainAxisAlignment.center, 70 | children: [ 71 | new Text( 72 | 'There are no bottons to push :)', 73 | ), 74 | new Text( 75 | 'Just turn off your internet.', 76 | ), 77 | ], 78 | ), 79 | ), 80 | ); 81 | } 82 | } 83 | ``` 84 | 85 | For more info, please, refer to the `main.dart` in the example. 86 | 87 | ## 📷 Screenshots 88 | 89 | 90 | 91 | 94 | 97 | 100 | 101 |
92 | 93 | 95 | 96 | 98 | 99 |
102 | 103 | ## 🐛 Bugs/Requests 104 | 105 | If you encounter any problems feel free to open an issue. If you feel the library is 106 | missing a feature, please raise a ticket on Github and I'll look into it. 107 | Pull request are also welcome. 108 | 109 | ### ❗️ Note 110 | 111 | For help getting started with Flutter, view our online 112 | [documentation](https://flutter.io/). 113 | 114 | For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). 115 | 116 | ### 🤓 Mentions 117 | 118 | Simon Lightfoot ([@slightfoot](https://github.com/slightfoot)) is just awesome 👍. 119 | 120 | ## ⭐️ License 121 | 122 | MIT License 123 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | errors: 5 | missing_required_param: error 6 | missing_return: error 7 | unused_import: error 8 | unused_local_variable: error 9 | dead_code: error 10 | todo: ignore 11 | 12 | linter: 13 | rules: 14 | # All rules from pedantic, already enabled rules are left out 15 | # https://github.com/google/pedantic/blob/master/lib/analysis_options.1.11.0.yaml 16 | - always_declare_return_types 17 | - prefer_single_quotes 18 | - unawaited_futures 19 | 20 | # Additional rules from https://github.com/flutter/flutter/blob/master/analysis_options.yaml 21 | # Not all rules are included 22 | - always_put_control_body_on_new_line 23 | - avoid_slow_async_io 24 | - cast_nullable_to_non_nullable 25 | - prefer_final_in_for_each 26 | - prefer_final_locals 27 | - prefer_foreach 28 | - prefer_if_elements_to_conditional_expressions 29 | - sort_constructors_first 30 | - sort_unnamed_constructors_first 31 | - test_types_in_equals 32 | - tighten_type_of_initializing_formals 33 | - unnecessary_await_in_return 34 | - unnecessary_null_aware_assignments 35 | - unnecessary_null_checks 36 | - unnecessary_nullable_for_final_variable_declarations 37 | - unnecessary_statements 38 | - use_late_for_private_fields_and_variables 39 | - use_named_constants 40 | - use_raw_strings -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: cf4400006550b70f28e4b4af815151d1e74846c6 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | 31 | namespace 'com.example.example' 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | 42 | sourceSets { 43 | main.java.srcDirs += 'src/main/kotlin' 44 | } 45 | 46 | defaultConfig { 47 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 48 | applicationId "com.example.example" 49 | minSdkVersion flutter.minSdkVersion 50 | targetSdkVersion flutter.targetSdkVersion 51 | versionCode flutterVersionCode.toInteger() 52 | versionName flutterVersionName 53 | } 54 | 55 | buildTypes { 56 | release { 57 | // TODO: Add your own signing config for the release build. 58 | // Signing with the debug keys for now, so `flutter run --release` works. 59 | signingConfig signingConfigs.debug 60 | } 61 | } 62 | } 63 | 64 | flutter { 65 | source '../..' 66 | } 67 | 68 | dependencies { 69 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 70 | } 71 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 17 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jogboms/flutter_offline/6a20671a70917a774c903b7c411f44aa7c7c4c5a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jogboms/flutter_offline/6a20671a70917a774c903b7c411f44aa7c7c4c5a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jogboms/flutter_offline/6a20671a70917a774c903b7c411f44aa7c7c4c5a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jogboms/flutter_offline/6a20671a70917a774c903b7c411f44aa7c7c4c5a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jogboms/flutter_offline/6a20671a70917a774c903b7c411f44aa7c7c4c5a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.9.0' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:8.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.defaults.buildfeatures.buildconfig=true 5 | android.nonTransitiveRClass=false 6 | android.nonFinalResIds=false 7 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/lib/demo_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DemoPage extends StatelessWidget { 4 | const DemoPage({ 5 | Key? key, 6 | required this.child, 7 | }) : super(key: key); 8 | 9 | final Widget child; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: const Text('Offline Demo'), 16 | ), 17 | body: child, 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import './demo_page.dart'; 4 | import './widgets/demo_1.dart'; 5 | import './widgets/demo_2.dart'; 6 | import './widgets/demo_3.dart'; 7 | 8 | void main() => runApp(const MyApp()); 9 | 10 | class MyApp extends StatelessWidget { 11 | const MyApp({Key? key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return MaterialApp( 16 | title: 'Offline Demo', 17 | theme: ThemeData.dark(), 18 | home: Builder( 19 | builder: (BuildContext context) { 20 | return Column( 21 | mainAxisAlignment: MainAxisAlignment.center, 22 | children: [ 23 | ElevatedButton( 24 | onPressed: () { 25 | navigate(context, const Demo1()); 26 | }, 27 | child: const Text('Demo 1'), 28 | ), 29 | ElevatedButton( 30 | onPressed: () { 31 | navigate(context, const Demo2()); 32 | }, 33 | child: const Text('Demo 2'), 34 | ), 35 | ElevatedButton( 36 | onPressed: () { 37 | navigate(context, const Demo3()); 38 | }, 39 | child: const Text('Demo 3'), 40 | ), 41 | ], 42 | ); 43 | }, 44 | ), 45 | ); 46 | } 47 | 48 | void navigate(BuildContext context, Widget widget) { 49 | Navigator.of(context).push( 50 | MaterialPageRoute( 51 | builder: (BuildContext context) => DemoPage(child: widget), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example/lib/widgets/demo_1.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_offline/flutter_offline.dart'; 3 | 4 | class Demo1 extends StatelessWidget { 5 | const Demo1({Key? key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return OfflineBuilder( 10 | connectivityBuilder: ( 11 | BuildContext context, 12 | List connectivity, 13 | Widget child, 14 | ) { 15 | final connected = !connectivity.contains(ConnectivityResult.none); 16 | return Stack( 17 | fit: StackFit.expand, 18 | children: [ 19 | child, 20 | Positioned( 21 | height: 32.0, 22 | left: 0.0, 23 | right: 0.0, 24 | child: AnimatedContainer( 25 | duration: const Duration(milliseconds: 350), 26 | color: connected ? const Color(0xFF00EE44) : const Color(0xFFEE4400), 27 | child: AnimatedSwitcher( 28 | duration: const Duration(milliseconds: 350), 29 | child: connected 30 | ? const Text('ONLINE') 31 | : const Row( 32 | mainAxisAlignment: MainAxisAlignment.center, 33 | children: [ 34 | Text('OFFLINE'), 35 | SizedBox(width: 8.0), 36 | SizedBox( 37 | width: 12.0, 38 | height: 12.0, 39 | child: CircularProgressIndicator( 40 | strokeWidth: 2.0, 41 | valueColor: AlwaysStoppedAnimation(Colors.white), 42 | ), 43 | ), 44 | ], 45 | ), 46 | ), 47 | ), 48 | ), 49 | ], 50 | ); 51 | }, 52 | child: const Column( 53 | mainAxisAlignment: MainAxisAlignment.center, 54 | children: [ 55 | Text( 56 | 'There are no bottons to push :)', 57 | ), 58 | Text( 59 | 'Just turn off your internet.', 60 | ), 61 | ], 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /example/lib/widgets/demo_2.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_offline/flutter_offline.dart'; 3 | 4 | class Demo2 extends StatelessWidget { 5 | const Demo2({Key? key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return OfflineBuilder( 10 | connectivityBuilder: ( 11 | BuildContext context, 12 | List connectivity, 13 | Widget child, 14 | ) { 15 | if (connectivity.contains(ConnectivityResult.none)) { 16 | return Container( 17 | color: Colors.white, 18 | child: const Center( 19 | child: Text( 20 | 'Oops, \n\nNow we are Offline!', 21 | style: TextStyle(color: Colors.black), 22 | ), 23 | ), 24 | ); 25 | } else { 26 | return child; 27 | } 28 | }, 29 | builder: (BuildContext context) { 30 | return const Center( 31 | child: Column( 32 | mainAxisAlignment: MainAxisAlignment.center, 33 | children: [ 34 | Text( 35 | 'There are no bottons to push :)', 36 | ), 37 | Text( 38 | 'Just turn off your internet.', 39 | ), 40 | ], 41 | ), 42 | ); 43 | }, 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example/lib/widgets/demo_3.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_offline/flutter_offline.dart'; 3 | 4 | class Demo3 extends StatelessWidget { 5 | const Demo3({Key? key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return OfflineBuilder( 10 | debounceDuration: Duration.zero, 11 | connectivityBuilder: ( 12 | BuildContext context, 13 | List connectivity, 14 | Widget child, 15 | ) { 16 | if (connectivity.contains(ConnectivityResult.none)) { 17 | return Container( 18 | color: Colors.white70, 19 | child: const Center( 20 | child: Text( 21 | 'Oops, \n\nWe experienced a Delayed Offline!', 22 | style: TextStyle(color: Colors.black), 23 | ), 24 | ), 25 | ); 26 | } 27 | return child; 28 | }, 29 | child: const Center( 30 | child: Column( 31 | mainAxisAlignment: MainAxisAlignment.center, 32 | children: [ 33 | Text( 34 | 'There are no bottons to push :)', 35 | ), 36 | Text( 37 | 'Just turn off your internet.', 38 | ), 39 | Text( 40 | 'This one has a bit of a delay.', 41 | ), 42 | ], 43 | ), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | args: 5 | dependency: transitive 6 | description: 7 | name: args 8 | sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.3.0" 12 | async: 13 | dependency: transitive 14 | description: 15 | name: async 16 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.11.0" 20 | boolean_selector: 21 | dependency: transitive 22 | description: 23 | name: boolean_selector 24 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.1.1" 28 | characters: 29 | dependency: transitive 30 | description: 31 | name: characters 32 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.3.0" 36 | clock: 37 | dependency: transitive 38 | description: 39 | name: clock 40 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.1.1" 44 | collection: 45 | dependency: transitive 46 | description: 47 | name: collection 48 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.18.0" 52 | connectivity_plus: 53 | dependency: transitive 54 | description: 55 | name: connectivity_plus 56 | sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "6.1.4" 60 | connectivity_plus_platform_interface: 61 | dependency: transitive 62 | description: 63 | name: connectivity_plus_platform_interface 64 | sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "2.0.1" 68 | dbus: 69 | dependency: transitive 70 | description: 71 | name: dbus 72 | sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "0.7.8" 76 | fake_async: 77 | dependency: transitive 78 | description: 79 | name: fake_async 80 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "1.3.1" 84 | ffi: 85 | dependency: transitive 86 | description: 87 | name: ffi 88 | sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "2.1.3" 92 | flutter: 93 | dependency: "direct main" 94 | description: flutter 95 | source: sdk 96 | version: "0.0.0" 97 | flutter_lints: 98 | dependency: "direct dev" 99 | description: 100 | name: flutter_lints 101 | sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" 102 | url: "https://pub.dev" 103 | source: hosted 104 | version: "2.0.2" 105 | flutter_offline: 106 | dependency: "direct main" 107 | description: 108 | path: ".." 109 | relative: true 110 | source: path 111 | version: "5.0.0" 112 | flutter_test: 113 | dependency: "direct dev" 114 | description: flutter 115 | source: sdk 116 | version: "0.0.0" 117 | flutter_web_plugins: 118 | dependency: transitive 119 | description: flutter 120 | source: sdk 121 | version: "0.0.0" 122 | leak_tracker: 123 | dependency: transitive 124 | description: 125 | name: leak_tracker 126 | sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" 127 | url: "https://pub.dev" 128 | source: hosted 129 | version: "10.0.5" 130 | leak_tracker_flutter_testing: 131 | dependency: transitive 132 | description: 133 | name: leak_tracker_flutter_testing 134 | sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" 135 | url: "https://pub.dev" 136 | source: hosted 137 | version: "3.0.5" 138 | leak_tracker_testing: 139 | dependency: transitive 140 | description: 141 | name: leak_tracker_testing 142 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 143 | url: "https://pub.dev" 144 | source: hosted 145 | version: "3.0.1" 146 | lints: 147 | dependency: transitive 148 | description: 149 | name: lints 150 | sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" 151 | url: "https://pub.dev" 152 | source: hosted 153 | version: "2.1.1" 154 | matcher: 155 | dependency: transitive 156 | description: 157 | name: matcher 158 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 159 | url: "https://pub.dev" 160 | source: hosted 161 | version: "0.12.16+1" 162 | material_color_utilities: 163 | dependency: transitive 164 | description: 165 | name: material_color_utilities 166 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 167 | url: "https://pub.dev" 168 | source: hosted 169 | version: "0.11.1" 170 | meta: 171 | dependency: transitive 172 | description: 173 | name: meta 174 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 175 | url: "https://pub.dev" 176 | source: hosted 177 | version: "1.15.0" 178 | network_info_plus: 179 | dependency: transitive 180 | description: 181 | name: network_info_plus 182 | sha256: f926b2ba86aa0086a0dfbb9e5072089bc213d854135c1712f1d29fc89ba3c877 183 | url: "https://pub.dev" 184 | source: hosted 185 | version: "6.1.4" 186 | network_info_plus_platform_interface: 187 | dependency: transitive 188 | description: 189 | name: network_info_plus_platform_interface 190 | sha256: "7e7496a8a9d8136859b8881affc613c4a21304afeb6c324bcefc4bd0aff6b94b" 191 | url: "https://pub.dev" 192 | source: hosted 193 | version: "2.0.2" 194 | nm: 195 | dependency: transitive 196 | description: 197 | name: nm 198 | sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" 199 | url: "https://pub.dev" 200 | source: hosted 201 | version: "0.5.0" 202 | path: 203 | dependency: transitive 204 | description: 205 | name: path 206 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 207 | url: "https://pub.dev" 208 | source: hosted 209 | version: "1.9.0" 210 | petitparser: 211 | dependency: transitive 212 | description: 213 | name: petitparser 214 | sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 215 | url: "https://pub.dev" 216 | source: hosted 217 | version: "5.4.0" 218 | plugin_platform_interface: 219 | dependency: transitive 220 | description: 221 | name: plugin_platform_interface 222 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 223 | url: "https://pub.dev" 224 | source: hosted 225 | version: "2.1.8" 226 | sky_engine: 227 | dependency: transitive 228 | description: flutter 229 | source: sdk 230 | version: "0.0.99" 231 | source_span: 232 | dependency: transitive 233 | description: 234 | name: source_span 235 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 236 | url: "https://pub.dev" 237 | source: hosted 238 | version: "1.10.0" 239 | stack_trace: 240 | dependency: transitive 241 | description: 242 | name: stack_trace 243 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 244 | url: "https://pub.dev" 245 | source: hosted 246 | version: "1.11.1" 247 | stream_channel: 248 | dependency: transitive 249 | description: 250 | name: stream_channel 251 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 252 | url: "https://pub.dev" 253 | source: hosted 254 | version: "2.1.2" 255 | string_scanner: 256 | dependency: transitive 257 | description: 258 | name: string_scanner 259 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 260 | url: "https://pub.dev" 261 | source: hosted 262 | version: "1.2.0" 263 | term_glyph: 264 | dependency: transitive 265 | description: 266 | name: term_glyph 267 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 268 | url: "https://pub.dev" 269 | source: hosted 270 | version: "1.2.1" 271 | test_api: 272 | dependency: transitive 273 | description: 274 | name: test_api 275 | sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" 276 | url: "https://pub.dev" 277 | source: hosted 278 | version: "0.7.2" 279 | vector_math: 280 | dependency: transitive 281 | description: 282 | name: vector_math 283 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 284 | url: "https://pub.dev" 285 | source: hosted 286 | version: "2.1.4" 287 | vm_service: 288 | dependency: transitive 289 | description: 290 | name: vm_service 291 | sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" 292 | url: "https://pub.dev" 293 | source: hosted 294 | version: "14.2.5" 295 | web: 296 | dependency: transitive 297 | description: 298 | name: web 299 | sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" 300 | url: "https://pub.dev" 301 | source: hosted 302 | version: "1.1.1" 303 | win32: 304 | dependency: transitive 305 | description: 306 | name: win32 307 | sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e 308 | url: "https://pub.dev" 309 | source: hosted 310 | version: "5.10.1" 311 | xml: 312 | dependency: transitive 313 | description: 314 | name: xml 315 | sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" 316 | url: "https://pub.dev" 317 | source: hosted 318 | version: "6.3.0" 319 | sdks: 320 | dart: ">=3.5.0 <4.0.0" 321 | flutter: ">=3.18.0-18.0.pre.54" 322 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter project. 3 | version: 1.0.0+1 4 | 5 | publish_to: none 6 | 7 | environment: 8 | sdk: ">=3.0.0 <4.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | flutter_offline: 14 | path: ../ 15 | 16 | dev_dependencies: 17 | flutter_lints: ^2.0.2 18 | flutter_test: 19 | sdk: flutter 20 | 21 | flutter: 22 | uses-material-design: true 23 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // To perform an interaction with a widget in your test, use the WidgetTester utility that Flutter 3 | // provides. For example, you can send tap and scroll gestures. You can also use WidgetTester to 4 | // find child widgets in the widget tree, read text, and verify that the values of widget properties 5 | // are correct. 6 | 7 | void main() {} 8 | -------------------------------------------------------------------------------- /flutter_offline.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/flutter_offline.dart: -------------------------------------------------------------------------------- 1 | library flutter_offline; 2 | 3 | export 'package:connectivity_plus/connectivity_plus.dart' show ConnectivityResult; 4 | 5 | export 'src/main.dart'; 6 | -------------------------------------------------------------------------------- /lib/src/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:connectivity_plus/connectivity_plus.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:flutter_offline/src/utils.dart'; 6 | import 'package:network_info_plus/network_info_plus.dart'; 7 | 8 | const kOfflineDebounceDuration = Duration(seconds: 3); 9 | 10 | typedef ValueWidgetBuilder = Widget Function(BuildContext context, T value, Widget child); 11 | 12 | class OfflineBuilder extends StatefulWidget { 13 | factory OfflineBuilder({ 14 | Key? key, 15 | required ValueWidgetBuilder> connectivityBuilder, 16 | Duration debounceDuration = kOfflineDebounceDuration, 17 | WidgetBuilder? builder, 18 | Widget? child, 19 | WidgetBuilder? errorBuilder, 20 | }) { 21 | return OfflineBuilder.initialize( 22 | key: key, 23 | connectivityBuilder: connectivityBuilder, 24 | connectivityService: Connectivity(), 25 | wifiInfo: NetworkInfo(), 26 | debounceDuration: debounceDuration, 27 | builder: builder, 28 | errorBuilder: errorBuilder, 29 | child: child, 30 | ); 31 | } 32 | 33 | @visibleForTesting 34 | const OfflineBuilder.initialize({ 35 | Key? key, 36 | required this.connectivityBuilder, 37 | required this.connectivityService, 38 | required this.wifiInfo, 39 | this.debounceDuration = kOfflineDebounceDuration, 40 | this.builder, 41 | this.child, 42 | this.errorBuilder, 43 | }) : assert(!(builder is WidgetBuilder && child is Widget) && !(builder == null && child == null), 44 | 'You should specify either a builder or a child'), 45 | super(key: key); 46 | 47 | /// Override connectivity service used for testing 48 | final Connectivity connectivityService; 49 | 50 | final NetworkInfo wifiInfo; 51 | 52 | /// Debounce duration from epileptic network situations 53 | final Duration debounceDuration; 54 | 55 | /// Used for building the Offline and/or Online UI 56 | final ValueWidgetBuilder> connectivityBuilder; 57 | 58 | /// Used for building the child widget 59 | final WidgetBuilder? builder; 60 | 61 | /// The widget below this widget in the tree. 62 | final Widget? child; 63 | 64 | /// Used for building the error widget incase of any platform errors 65 | final WidgetBuilder? errorBuilder; 66 | 67 | @override 68 | OfflineBuilderState createState() => OfflineBuilderState(); 69 | } 70 | 71 | class OfflineBuilderState extends State { 72 | late Stream> _connectivityStream; 73 | 74 | @override 75 | void initState() { 76 | super.initState(); 77 | 78 | _connectivityStream = Stream.fromFuture(widget.connectivityService.checkConnectivity()) 79 | .asyncExpand((data) => widget.connectivityService.onConnectivityChanged.transform(startsWith(data))) 80 | .transform(debounce(widget.debounceDuration)); 81 | } 82 | 83 | @override 84 | Widget build(BuildContext context) { 85 | return StreamBuilder>( 86 | stream: _connectivityStream, 87 | builder: (BuildContext context, AsyncSnapshot> snapshot) { 88 | if (!snapshot.hasData && !snapshot.hasError) { 89 | return const SizedBox(); 90 | } 91 | 92 | if (snapshot.hasError) { 93 | if (widget.errorBuilder != null) { 94 | return widget.errorBuilder!(context); 95 | } 96 | throw OfflineBuilderError(snapshot.error!); 97 | } 98 | 99 | return widget.connectivityBuilder(context, snapshot.data!, widget.child ?? widget.builder!(context)); 100 | }, 101 | ); 102 | } 103 | } 104 | 105 | class OfflineBuilderError extends Error { 106 | OfflineBuilderError(this.error); 107 | 108 | final Object error; 109 | 110 | @override 111 | String toString() => error.toString(); 112 | } 113 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:connectivity_plus/connectivity_plus.dart'; 4 | 5 | StreamTransformer, List> debounce( 6 | Duration debounceDuration, 7 | ) { 8 | var seenFirstData = false; 9 | Timer? debounceTimer; 10 | 11 | return StreamTransformer, List>.fromHandlers( 12 | handleData: (List data, EventSink> sink) { 13 | if (seenFirstData) { 14 | debounceTimer?.cancel(); 15 | debounceTimer = Timer(debounceDuration, () => sink.add(data)); 16 | } else { 17 | sink.add(data); 18 | seenFirstData = true; 19 | } 20 | }, 21 | handleDone: (EventSink> sink) { 22 | debounceTimer?.cancel(); 23 | sink.close(); 24 | }, 25 | ); 26 | } 27 | 28 | StreamTransformer, List> startsWith( 29 | List data, 30 | ) { 31 | return StreamTransformer, List>( 32 | ( 33 | Stream> input, 34 | bool cancelOnError, 35 | ) { 36 | StreamController>? controller; 37 | late StreamSubscription> subscription; 38 | 39 | controller = StreamController>( 40 | sync: true, 41 | onListen: () => controller?.add(data), 42 | onPause: ([Future? resumeSignal]) => subscription.pause(resumeSignal), 43 | onResume: () => subscription.resume(), 44 | onCancel: () => subscription.cancel(), 45 | ); 46 | 47 | subscription = input.listen( 48 | controller.add, 49 | onError: controller.addError, 50 | onDone: controller.close, 51 | cancelOnError: cancelOnError, 52 | ); 53 | 54 | return controller.stream.listen(null); 55 | }, 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | test_coverage: 2 | flutter test --no-pub --coverage 3 | 4 | build_coverage: 5 | make test_coverage && genhtml -o coverage coverage/lcov.info 6 | 7 | open_coverage: 8 | make build_coverage && open coverage/index.html -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | args: 5 | dependency: transitive 6 | description: 7 | name: args 8 | sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.3.0" 12 | async: 13 | dependency: transitive 14 | description: 15 | name: async 16 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.11.0" 20 | boolean_selector: 21 | dependency: transitive 22 | description: 23 | name: boolean_selector 24 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.1.1" 28 | characters: 29 | dependency: transitive 30 | description: 31 | name: characters 32 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.3.0" 36 | clock: 37 | dependency: transitive 38 | description: 39 | name: clock 40 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.1.1" 44 | collection: 45 | dependency: transitive 46 | description: 47 | name: collection 48 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.18.0" 52 | connectivity_plus: 53 | dependency: "direct main" 54 | description: 55 | name: connectivity_plus 56 | sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "6.1.4" 60 | connectivity_plus_platform_interface: 61 | dependency: transitive 62 | description: 63 | name: connectivity_plus_platform_interface 64 | sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "2.0.1" 68 | dbus: 69 | dependency: transitive 70 | description: 71 | name: dbus 72 | sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "0.7.8" 76 | fake_async: 77 | dependency: transitive 78 | description: 79 | name: fake_async 80 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "1.3.1" 84 | ffi: 85 | dependency: transitive 86 | description: 87 | name: ffi 88 | sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "2.1.3" 92 | flutter: 93 | dependency: "direct main" 94 | description: flutter 95 | source: sdk 96 | version: "0.0.0" 97 | flutter_lints: 98 | dependency: "direct dev" 99 | description: 100 | name: flutter_lints 101 | sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" 102 | url: "https://pub.dev" 103 | source: hosted 104 | version: "2.0.2" 105 | flutter_test: 106 | dependency: "direct dev" 107 | description: flutter 108 | source: sdk 109 | version: "0.0.0" 110 | flutter_web_plugins: 111 | dependency: transitive 112 | description: flutter 113 | source: sdk 114 | version: "0.0.0" 115 | leak_tracker: 116 | dependency: transitive 117 | description: 118 | name: leak_tracker 119 | sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" 120 | url: "https://pub.dev" 121 | source: hosted 122 | version: "10.0.5" 123 | leak_tracker_flutter_testing: 124 | dependency: transitive 125 | description: 126 | name: leak_tracker_flutter_testing 127 | sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" 128 | url: "https://pub.dev" 129 | source: hosted 130 | version: "3.0.5" 131 | leak_tracker_testing: 132 | dependency: transitive 133 | description: 134 | name: leak_tracker_testing 135 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 136 | url: "https://pub.dev" 137 | source: hosted 138 | version: "3.0.1" 139 | lints: 140 | dependency: transitive 141 | description: 142 | name: lints 143 | sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" 144 | url: "https://pub.dev" 145 | source: hosted 146 | version: "2.1.1" 147 | matcher: 148 | dependency: transitive 149 | description: 150 | name: matcher 151 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "0.12.16+1" 155 | material_color_utilities: 156 | dependency: transitive 157 | description: 158 | name: material_color_utilities 159 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "0.11.1" 163 | meta: 164 | dependency: transitive 165 | description: 166 | name: meta 167 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "1.15.0" 171 | network_info_plus: 172 | dependency: "direct main" 173 | description: 174 | name: network_info_plus 175 | sha256: f926b2ba86aa0086a0dfbb9e5072089bc213d854135c1712f1d29fc89ba3c877 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "6.1.4" 179 | network_info_plus_platform_interface: 180 | dependency: transitive 181 | description: 182 | name: network_info_plus_platform_interface 183 | sha256: "7e7496a8a9d8136859b8881affc613c4a21304afeb6c324bcefc4bd0aff6b94b" 184 | url: "https://pub.dev" 185 | source: hosted 186 | version: "2.0.2" 187 | nm: 188 | dependency: transitive 189 | description: 190 | name: nm 191 | sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" 192 | url: "https://pub.dev" 193 | source: hosted 194 | version: "0.5.0" 195 | path: 196 | dependency: transitive 197 | description: 198 | name: path 199 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 200 | url: "https://pub.dev" 201 | source: hosted 202 | version: "1.9.0" 203 | petitparser: 204 | dependency: transitive 205 | description: 206 | name: petitparser 207 | sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 208 | url: "https://pub.dev" 209 | source: hosted 210 | version: "5.4.0" 211 | plugin_platform_interface: 212 | dependency: transitive 213 | description: 214 | name: plugin_platform_interface 215 | sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" 216 | url: "https://pub.dev" 217 | source: hosted 218 | version: "2.1.4" 219 | sky_engine: 220 | dependency: transitive 221 | description: flutter 222 | source: sdk 223 | version: "0.0.99" 224 | source_span: 225 | dependency: transitive 226 | description: 227 | name: source_span 228 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 229 | url: "https://pub.dev" 230 | source: hosted 231 | version: "1.10.0" 232 | stack_trace: 233 | dependency: transitive 234 | description: 235 | name: stack_trace 236 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 237 | url: "https://pub.dev" 238 | source: hosted 239 | version: "1.11.1" 240 | stream_channel: 241 | dependency: transitive 242 | description: 243 | name: stream_channel 244 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 245 | url: "https://pub.dev" 246 | source: hosted 247 | version: "2.1.2" 248 | string_scanner: 249 | dependency: transitive 250 | description: 251 | name: string_scanner 252 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 253 | url: "https://pub.dev" 254 | source: hosted 255 | version: "1.2.0" 256 | term_glyph: 257 | dependency: transitive 258 | description: 259 | name: term_glyph 260 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 261 | url: "https://pub.dev" 262 | source: hosted 263 | version: "1.2.1" 264 | test_api: 265 | dependency: transitive 266 | description: 267 | name: test_api 268 | sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" 269 | url: "https://pub.dev" 270 | source: hosted 271 | version: "0.7.2" 272 | vector_math: 273 | dependency: transitive 274 | description: 275 | name: vector_math 276 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 277 | url: "https://pub.dev" 278 | source: hosted 279 | version: "2.1.4" 280 | vm_service: 281 | dependency: transitive 282 | description: 283 | name: vm_service 284 | sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" 285 | url: "https://pub.dev" 286 | source: hosted 287 | version: "14.2.5" 288 | web: 289 | dependency: transitive 290 | description: 291 | name: web 292 | sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" 293 | url: "https://pub.dev" 294 | source: hosted 295 | version: "1.1.1" 296 | win32: 297 | dependency: transitive 298 | description: 299 | name: win32 300 | sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e 301 | url: "https://pub.dev" 302 | source: hosted 303 | version: "5.10.1" 304 | xml: 305 | dependency: transitive 306 | description: 307 | name: xml 308 | sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" 309 | url: "https://pub.dev" 310 | source: hosted 311 | version: "6.3.0" 312 | sdks: 313 | dart: ">=3.5.0 <4.0.0" 314 | flutter: ">=3.18.0-18.0.pre.54" 315 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_offline 2 | description: A tidy utility to handle offline/online connectivity like a Boss. 3 | version: 5.0.0 4 | homepage: https://github.com/jogboms/flutter_offline 5 | 6 | environment: 7 | sdk: '>=3.0.0 <4.0.0' 8 | flutter: ">=3.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | connectivity_plus: ^6.1.4 14 | network_info_plus: ^6.1.4 15 | 16 | dev_dependencies: 17 | flutter_lints: ^2.0.2 18 | flutter_test: 19 | sdk: flutter 20 | -------------------------------------------------------------------------------- /screenshots/demo_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jogboms/flutter_offline/6a20671a70917a774c903b7c411f44aa7c7c4c5a/screenshots/demo_1.gif -------------------------------------------------------------------------------- /screenshots/demo_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jogboms/flutter_offline/6a20671a70917a774c903b7c411f44aa7c7c4c5a/screenshots/demo_2.gif -------------------------------------------------------------------------------- /screenshots/demo_3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jogboms/flutter_offline/6a20671a70917a774c903b7c411f44aa7c7c4c5a/screenshots/demo_3.gif -------------------------------------------------------------------------------- /test/flutter_offline_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:connectivity_plus/connectivity_plus.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_offline/flutter_offline.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:network_info_plus/network_info_plus.dart' as wifi; 8 | 9 | void main() { 10 | group('Test UI Widget', () { 11 | testWidgets('Test w/ factory OfflineBuilder', (WidgetTester tester) async { 12 | final instance = OfflineBuilder( 13 | connectivityBuilder: (_, __, Widget child) => child, 14 | builder: (BuildContext context) => const Text('builder_result'), 15 | ); 16 | 17 | expect(instance.connectivityService, isInstanceOf()); 18 | }); 19 | 20 | testWidgets('Test w/ builder param', (WidgetTester tester) async { 21 | await tester.pumpWidget(MaterialApp( 22 | home: OfflineBuilder.initialize( 23 | connectivityService: TestConnectivityService([ConnectivityResult.none]), 24 | wifiInfo: TestNetworkInfoService(), 25 | connectivityBuilder: (_, __, Widget child) => child, 26 | builder: (BuildContext context) => const Text('builder_result'), 27 | ), 28 | )); 29 | await tester.pump(kOfflineDebounceDuration); 30 | expect(find.text('builder_result'), findsOneWidget); 31 | }); 32 | 33 | testWidgets('Test w/ child param', (WidgetTester tester) async { 34 | await tester.pumpWidget(MaterialApp( 35 | home: OfflineBuilder.initialize( 36 | connectivityService: TestConnectivityService([ConnectivityResult.none]), 37 | wifiInfo: TestNetworkInfoService(), 38 | connectivityBuilder: (_, __, Widget child) => child, 39 | child: const Text('child_result'), 40 | ), 41 | )); 42 | await tester.pump(kOfflineDebounceDuration); 43 | expect(find.text('child_result'), findsOneWidget); 44 | }); 45 | }); 46 | 47 | group('Test Assertions', () { 48 | testWidgets('Test builder & child param', (WidgetTester tester) async { 49 | expect(() { 50 | OfflineBuilder.initialize( 51 | connectivityService: TestConnectivityService([ConnectivityResult.none]), 52 | wifiInfo: TestNetworkInfoService(), 53 | connectivityBuilder: (_, __, Widget child) => child, 54 | builder: (BuildContext context) => const Text('builder_result'), 55 | child: const Text('child_result'), 56 | ); 57 | }, throwsAssertionError); 58 | }); 59 | 60 | testWidgets('Test no builder & child param', (WidgetTester tester) async { 61 | expect(() { 62 | OfflineBuilder.initialize( 63 | connectivityService: TestConnectivityService([ConnectivityResult.none]), 64 | wifiInfo: TestNetworkInfoService(), 65 | connectivityBuilder: (_, __, Widget child) => child, 66 | ); 67 | }, throwsAssertionError); 68 | }); 69 | }); 70 | 71 | group('Test Status', () { 72 | testWidgets('Test builder offline', (WidgetTester tester) async { 73 | const initialConnection = [ConnectivityResult.none]; 74 | await tester.pumpWidget(MaterialApp( 75 | home: OfflineBuilder.initialize( 76 | connectivityService: TestConnectivityService(initialConnection), 77 | wifiInfo: TestNetworkInfoService(), 78 | connectivityBuilder: (_, List connectivity, __) => Text('$connectivity'), 79 | child: const SizedBox(), 80 | ), 81 | )); 82 | await tester.pump(kOfflineDebounceDuration); 83 | expect(find.text(initialConnection.toString()), findsOneWidget); 84 | }); 85 | 86 | testWidgets('Test builder online', (WidgetTester tester) async { 87 | const initialConnection = [ConnectivityResult.wifi]; 88 | await tester.pumpWidget(MaterialApp( 89 | home: OfflineBuilder.initialize( 90 | connectivityService: TestConnectivityService(initialConnection), 91 | wifiInfo: TestNetworkInfoService(), 92 | connectivityBuilder: (_, List connectivity, __) => Text('$connectivity'), 93 | child: const SizedBox(), 94 | ), 95 | )); 96 | await tester.pump(kOfflineDebounceDuration); 97 | expect(find.text(initialConnection.toString()), findsOneWidget); 98 | }); 99 | }); 100 | 101 | group('Test Flipper', () { 102 | testWidgets('Test builder flips online to offline', (WidgetTester tester) async { 103 | const initialConnection = [ConnectivityResult.wifi]; 104 | const lastConnection = [ConnectivityResult.none]; 105 | final service = TestConnectivityService(initialConnection); 106 | await tester.pumpWidget(MaterialApp( 107 | home: OfflineBuilder.initialize( 108 | connectivityService: service, 109 | wifiInfo: TestNetworkInfoService(), 110 | connectivityBuilder: (_, List connectivity, __) => Text('$connectivity'), 111 | child: const SizedBox(), 112 | ), 113 | )); 114 | 115 | await tester.pump(kOfflineDebounceDuration); 116 | expect(find.text(initialConnection.toString()), findsOneWidget); 117 | 118 | service.result = [ConnectivityResult.none]; 119 | await tester.pump(kOfflineDebounceDuration); 120 | expect(find.text(lastConnection.toString()), findsOneWidget); 121 | }); 122 | 123 | testWidgets('Test builder flips offline to online', (WidgetTester tester) async { 124 | const initialConnection = [ConnectivityResult.none]; 125 | const lastConnection = [ConnectivityResult.wifi]; 126 | final service = TestConnectivityService(initialConnection); 127 | await tester.pumpWidget(MaterialApp( 128 | home: OfflineBuilder.initialize( 129 | connectivityService: service, 130 | wifiInfo: TestNetworkInfoService(), 131 | connectivityBuilder: (_, List connectivity, __) => Text('$connectivity'), 132 | child: const SizedBox(), 133 | ), 134 | )); 135 | 136 | await tester.pump(kOfflineDebounceDuration); 137 | expect(find.text(initialConnection.toString()), findsOneWidget); 138 | 139 | service.result = [ConnectivityResult.wifi]; 140 | await tester.pump(kOfflineDebounceDuration); 141 | expect(find.text(lastConnection.toString()), findsOneWidget); 142 | }); 143 | }); 144 | 145 | group('Test Debounce', () { 146 | const initialConnection = [ConnectivityResult.none]; 147 | const connections = [ 148 | [ConnectivityResult.wifi], 149 | [ConnectivityResult.mobile], 150 | [ConnectivityResult.none], 151 | [ConnectivityResult.wifi], 152 | ]; 153 | testWidgets('Test for Debounce: Zero', (WidgetTester tester) async { 154 | final service = TestConnectivityService(initialConnection); 155 | const debounceDuration = Duration.zero; 156 | await tester.pumpWidget(MaterialApp( 157 | home: OfflineBuilder.initialize( 158 | connectivityService: service, 159 | wifiInfo: TestNetworkInfoService(), 160 | debounceDuration: debounceDuration, 161 | connectivityBuilder: (_, List connectivity, __) => Text('$connectivity'), 162 | child: const SizedBox(), 163 | ), 164 | )); 165 | 166 | for (final connection in connections) { 167 | service.result = connection; 168 | await tester.pump(debounceDuration); 169 | expect(find.text(connection.toString()), findsOneWidget); 170 | } 171 | }); 172 | 173 | testWidgets('Test for Debounce: 5 seconds', (WidgetTester tester) async { 174 | const debounceDuration = Duration(seconds: 5); 175 | 176 | const initialConnection = [ConnectivityResult.none]; 177 | const actualConnections = [ 178 | [ConnectivityResult.wifi], 179 | [ConnectivityResult.mobile], 180 | [ConnectivityResult.none], 181 | [ConnectivityResult.wifi], 182 | ]; 183 | const expectedConnections = [ 184 | [ConnectivityResult.none], 185 | [ConnectivityResult.none], 186 | [ConnectivityResult.none], 187 | [ConnectivityResult.wifi], 188 | ]; 189 | const durations = [ 190 | Duration.zero, 191 | Duration.zero, 192 | Duration.zero, 193 | debounceDuration, 194 | ]; 195 | 196 | final service = TestConnectivityService(initialConnection); 197 | await tester.pumpWidget(MaterialApp( 198 | home: OfflineBuilder.initialize( 199 | connectivityService: service, 200 | wifiInfo: TestNetworkInfoService(), 201 | debounceDuration: debounceDuration, 202 | connectivityBuilder: (_, List connectivity, __) { 203 | return Text('$connectivity'); 204 | }, 205 | child: const SizedBox(), 206 | ), 207 | )); 208 | 209 | for (var i = 0; i < actualConnections.length; i++) { 210 | service.result = actualConnections[i]; 211 | await tester.pump(durations[i]); 212 | expect(find.text(expectedConnections[i].toString()), findsOneWidget); 213 | } 214 | }); 215 | }); 216 | 217 | group('Test Platform Errors', () { 218 | testWidgets('Test w/o errorBuilder', (WidgetTester tester) async { 219 | const initialConnection = [ConnectivityResult.none]; 220 | final service = TestConnectivityService(initialConnection); 221 | 222 | await tester.pumpWidget(MaterialApp( 223 | home: OfflineBuilder.initialize( 224 | connectivityService: service, 225 | wifiInfo: TestNetworkInfoService(), 226 | connectivityBuilder: (_, List connectivity, __) => Text('$connectivity'), 227 | debounceDuration: Duration.zero, 228 | child: const SizedBox(), 229 | ), 230 | )); 231 | 232 | await tester.pump(Duration.zero); 233 | expect(find.text(initialConnection.toString()), findsOneWidget); 234 | 235 | service.addError(); 236 | await tester.pump(kOfflineDebounceDuration); 237 | expect(tester.takeException(), isInstanceOf()); 238 | }); 239 | 240 | testWidgets('Test w/ errorBuilder', (WidgetTester tester) async { 241 | const initialConnection = [ConnectivityResult.wifi]; 242 | final service = TestConnectivityService(initialConnection); 243 | 244 | await tester.pumpWidget(MaterialApp( 245 | home: OfflineBuilder.initialize( 246 | connectivityService: service, 247 | wifiInfo: TestNetworkInfoService(), 248 | connectivityBuilder: (_, List connectivity, __) => Text('$connectivity'), 249 | debounceDuration: Duration.zero, 250 | errorBuilder: (context) => const Text('Error'), 251 | child: const SizedBox(), 252 | ), 253 | )); 254 | 255 | await tester.pump(Duration.zero); 256 | expect(find.text(initialConnection.toString()), findsOneWidget); 257 | 258 | service.addError(); 259 | await tester.pump(kOfflineDebounceDuration); 260 | expect(find.text('Error'), findsOneWidget); 261 | }); 262 | }); 263 | } 264 | 265 | class TestConnectivityService implements Connectivity { 266 | TestConnectivityService([this.initialConnection]) : _result = initialConnection ?? [ConnectivityResult.none] { 267 | controller = StreamController>.broadcast( 268 | onListen: () => controller.add(_result), 269 | ); 270 | } 271 | 272 | late final StreamController> controller; 273 | final List? initialConnection; 274 | 275 | List _result; 276 | 277 | set result(List result) { 278 | _result = result; 279 | controller.add(result); 280 | } 281 | 282 | void addError() => controller.addError('Error'); 283 | 284 | @override 285 | Stream> get onConnectivityChanged => controller.stream; 286 | 287 | @override 288 | Future> checkConnectivity() { 289 | return Future.delayed(Duration.zero, () => initialConnection!); 290 | } 291 | } 292 | 293 | class TestNetworkInfoService implements wifi.NetworkInfo { 294 | TestNetworkInfoService(); 295 | 296 | @override 297 | Future getWifiIP() async => '127.0.0.1'; 298 | 299 | @override 300 | Future getWifiName() async => 'Localhost'; 301 | 302 | @override 303 | Future getWifiBSSID() async => ''; 304 | 305 | @override 306 | Future getWifiBroadcast() async => '127.0.0.255'; 307 | 308 | @override 309 | Future getWifiGatewayIP() async => '127.0.0.0'; 310 | 311 | @override 312 | Future getWifiIPv6() async => '2002:7f00:0001:0:0:0:0:0'; 313 | 314 | @override 315 | Future getWifiSubmask() async => '255.255.255.0'; 316 | } 317 | -------------------------------------------------------------------------------- /test/utils_debounce_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:connectivity_plus/connectivity_plus.dart'; 4 | import 'package:flutter_offline/src/utils.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | Future waitForTimer(int milliseconds) => Future(() { 8 | /* ensure Timer is started*/ 9 | }) 10 | .then( 11 | (_) => Future.delayed(Duration(milliseconds: milliseconds + 1)), 12 | ); 13 | 14 | void main() { 15 | StreamController> stream() => StreamController>.broadcast(); 16 | 17 | group('Group', () { 18 | late StreamController> values; 19 | late List emittedValues; 20 | late bool valuesCanceled; 21 | late bool isDone; 22 | late List errors; 23 | late StreamSubscription subscription; 24 | late Stream transformed; 25 | 26 | void setUpStreams(StreamTransformer transformer) { 27 | valuesCanceled = false; 28 | values = stream() 29 | ..onCancel = () { 30 | valuesCanceled = true; 31 | }; 32 | emittedValues = >[]; 33 | errors = >[]; 34 | isDone = false; 35 | transformed = values.stream.transform(transformer as StreamTransformer, void>); 36 | subscription = transformed.listen(emittedValues.add, onError: errors.add, onDone: () { 37 | isDone = true; 38 | }); 39 | } 40 | 41 | group('debounce', () { 42 | setUp(() async { 43 | setUpStreams(debounce(const Duration(milliseconds: 5))); 44 | }); 45 | 46 | test('cancels values', () async { 47 | await subscription.cancel(); 48 | expect(valuesCanceled, true); 49 | }); 50 | 51 | test('swallows values that come faster than duration', () async { 52 | values.add([ConnectivityResult.mobile]); 53 | values.add([ConnectivityResult.wifi]); 54 | await values.close(); 55 | await waitForTimer(5); 56 | expect(emittedValues, [[ConnectivityResult.mobile]]); 57 | }); 58 | 59 | test('outputs multiple values spaced further than duration', () async { 60 | values.add([ConnectivityResult.mobile]); 61 | await waitForTimer(5); 62 | values.add([ConnectivityResult.wifi]); 63 | await waitForTimer(5); 64 | expect( 65 | emittedValues, 66 | [[ConnectivityResult.mobile], [ConnectivityResult.wifi]], 67 | ); 68 | }); 69 | 70 | test('waits for pending value to close', () async { 71 | values.add([ConnectivityResult.mobile]); 72 | await waitForTimer(5); 73 | await values.close(); 74 | await Future(() {}); 75 | expect(isDone, true); 76 | }); 77 | }); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /test/utils_starts_with_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:connectivity_plus/connectivity_plus.dart'; 4 | import 'package:flutter_offline/src/utils.dart' as transformers; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | void main() { 8 | StreamController> stream() => StreamController>(); 9 | 10 | late StreamController> values; 11 | late List> emittedValues; 12 | late bool valuesCanceled; 13 | late bool valuesPaused; 14 | late bool valuesResume; 15 | late StreamSubscription> subscription; 16 | // bool isDone; 17 | late List errors; 18 | 19 | void setupForStreamType(StreamTransformer transformer) { 20 | emittedValues = >[]; 21 | valuesCanceled = false; 22 | errors = []; 23 | // isDone = false; 24 | values = stream() 25 | ..onPause = () { 26 | valuesPaused = true; 27 | } 28 | ..onResume = () { 29 | valuesResume = true; 30 | } 31 | ..onCancel = () { 32 | valuesCanceled = true; 33 | }; 34 | subscription = values.stream 35 | .transform>(transformer as StreamTransformer, List>) 36 | .listen(emittedValues.add, onError: errors.add, onDone: () { 37 | // isDone = true; 38 | }); 39 | } 40 | 41 | group('startWith', () { 42 | setUp(() { 43 | setupForStreamType(transformers.startsWith([ConnectivityResult.none])); 44 | }); 45 | 46 | test('cancels values', () async { 47 | await subscription.cancel(); 48 | expect(valuesCanceled, true); 49 | }); 50 | 51 | test('paused/resume values', () async { 52 | subscription.pause(); 53 | expect(valuesPaused, true); 54 | subscription.resume(); 55 | expect(valuesResume, true); 56 | }); 57 | 58 | test('addError values', () async { 59 | values.addError(45); 60 | await Future(() {}); 61 | expect(errors.length, isNonZero); 62 | }); 63 | 64 | test('outputs initial value', () async { 65 | await Future(() {}); 66 | expect(emittedValues, [[ConnectivityResult.none]]); 67 | }); 68 | 69 | test('outputs all values', () async { 70 | values 71 | ..add([ConnectivityResult.mobile]) 72 | ..add([ConnectivityResult.wifi]); 73 | await Future(() {}); 74 | expect(emittedValues, [[ConnectivityResult.none], [ConnectivityResult.mobile], [ConnectivityResult.wifi]]); 75 | }); 76 | 77 | test('outputs initial when followed by empty stream', () async { 78 | await values.close(); 79 | expect(emittedValues, [[ConnectivityResult.none]]); 80 | }); 81 | 82 | // test('closes with values', () async { 83 | // expect(isDone, false); 84 | // await values.close(); 85 | // expect(isDone, true); 86 | // }); 87 | }); 88 | } 89 | --------------------------------------------------------------------------------