├── .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 | [](https://github.com/jogboms/flutter_offline/actions/workflows/main.yml) [](https://codecov.io/gh/jogboms/flutter_offline) [](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 |
92 |
93 | |
94 |
95 |
96 | |
97 |
98 |
99 | |
100 |
101 |
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 |
--------------------------------------------------------------------------------