├── .gitignore ├── .metadata ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.java │ │ │ └── res │ │ │ │ ├── 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 │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ ├── Flutter.podspec │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Runner │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── main.m ├── lib │ ├── async_button_example.dart │ ├── failure_handling.dart │ ├── failure_handling_custom.dart │ ├── helpers.dart │ ├── hooks_example.dart │ ├── main.dart │ ├── minimal.dart │ ├── paged_loading.dart │ ├── paged_loading_simple.dart │ ├── provider_example.dart │ ├── pull_to_refresh.dart │ ├── refreshers_page.dart │ ├── sort_and_search.dart │ ├── stream.dart │ ├── translator │ │ └── translator_page.dart │ └── updating_example.dart ├── pubspec.lock └── pubspec.yaml ├── lib ├── async_controller.dart └── src │ ├── async_button.dart │ ├── async_data.dart │ ├── async_theme.dart │ ├── controller.dart │ ├── controller_ext.dart │ ├── debugging.dart │ ├── paged.dart │ ├── refreshers.dart │ ├── updating.dart │ └── utils.dart ├── pubspec.lock ├── pubspec.yaml └── test ├── controller_test.dart ├── paged_test.dart ├── refreshers_test.dart ├── updating_test.dart └── utils.dart /.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 | # Flutter/Dart/Pub related 19 | **/doc/api/ 20 | .dart_tool/ 21 | .flutter-plugins 22 | .packages 23 | .pub-cache/ 24 | .pub/ 25 | build/ 26 | 27 | # Android related 28 | **/android/**/gradle-wrapper.jar 29 | **/android/.gradle 30 | **/android/captures/ 31 | **/android/gradlew 32 | **/android/gradlew.bat 33 | **/android/local.properties 34 | **/android/**/GeneratedPluginRegistrant.java 35 | 36 | # iOS/XCode related 37 | **/ios/**/*.mode1v3 38 | **/ios/**/*.mode2v3 39 | **/ios/**/*.moved-aside 40 | **/ios/**/*.pbxuser 41 | **/ios/**/*.perspectivev3 42 | **/ios/**/*sync/ 43 | **/ios/**/.sconsign.dblite 44 | **/ios/**/.tags* 45 | **/ios/**/.vagrant/ 46 | **/ios/**/DerivedData/ 47 | **/ios/**/Icon? 48 | **/ios/**/Pods/ 49 | **/ios/**/.symlinks/ 50 | **/ios/**/profile 51 | **/ios/**/xcuserdata 52 | **/ios/.generated/ 53 | **/ios/Flutter/App.framework 54 | **/ios/Flutter/Flutter.framework 55 | **/ios/Flutter/Generated.xcconfig 56 | **/ios/Flutter/app.flx 57 | **/ios/Flutter/app.zip 58 | **/ios/Flutter/flutter_assets/ 59 | **/ios/ServiceDefinitions.json 60 | **/ios/Runner/GeneratedPluginRegistrant.* 61 | 62 | # Exceptions to above rules. 63 | !**/ios/**/default.mode1v3 64 | !**/ios/**/default.mode2v3 65 | !**/ios/**/default.pbxuser 66 | !**/ios/**/default.perspectivev3 67 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 68 | example/ios/Flutter/flutter_export_environment.sh 69 | .flutter-plugins-dependencies 70 | Podfile.lock 71 | -------------------------------------------------------------------------------- /.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: 0545c63b9baca0d40c2126dba6366fad2b16d3c1 8 | channel: master 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Flutter", 9 | "request": "launch", 10 | "type": "dart", 11 | "program": "example/lib/main.dart" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.lineLength": 80, 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.2] - 10.02.2020 2 | * fix snapshot getter 3 | * add last fetch timing 4 | 5 | ## [1.0.1] - 10.02.2020 6 | * improve refreshers 7 | * reintroduce performFetch method 8 | 9 | ## [1.0.0] - 09.02.2020 10 | * release stable version 11 | * fix few bugs 12 | * add better logging 13 | * rewrite paged loading 14 | 15 | ## [0.0.3] - 09.12.2019 16 | * fix missing removeListener 17 | 18 | ## [0.0.2] - 09.09.2019 19 | * Formatting and README improvements 20 | 21 | ## [0.0.1] - 09.09.2019 22 | * Initial release 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # async_controller 2 | 3 | A library for managing asynchronously loaded data in Flutter. 4 | 5 | ### Do I need this? 6 | If your project contains `isLoading` flags or error handling duplicated across many pages, you may benefit from this package. It will let you write async loading with minimal amount of boilerplate. Unlike FutureBuilder, AsyncController ensures that fetch is performed only when necessary. You may configure when controller should refresh using provided Refreshers. More on that later. 7 | 8 | ```dart 9 | final _controller = AsyncController.method(() async { 10 | await Future.delayed(Duration(seconds: 1)); 11 | return 'Hello world'; 12 | }); 13 | 14 | class Minimal extends StatelessWidget { 15 | @override 16 | Widget build(BuildContext context) { 17 | return _controller.buildAsyncData(builder: (_, data) { 18 | // This builder runs only if data is available. 19 | // buildAsyncData takes care of other situations 20 | return Text(data); 21 | }); 22 | } 23 | } 24 | ``` 25 | 26 | Example: [minimal.dart](example/lib/minimal.dart) 27 | 28 | ### Pull to refresh 29 | AsynController provides a method that you can plug right into a RefreshIndicator. Also, if user tries to refresh while loading is already pending the previous loading will be cancelled. 30 | 31 | ```dart 32 | final _controller = AsyncController.method(fetchSomething); 33 | RefreshIndicator( 34 | onRefresh: _controller.performUserInitiatedRefresh, 35 | child: _controller.buildAsyncData(builder: buildContent), 36 | ) 37 | ``` 38 | Example: [pull_to_refresh.dart](example/lib/pull_to_refresh.dart) 39 | 40 | ### Loading and error handling 41 | AsyncController used with `buildAsyncData`, automatically handles loading and error states. There is no need to manually change isLoading flag, or catch. AsyncController will do the right thing by default, while allowing for customizations. 42 | 43 | ```dart 44 | final _controller = AsyncController.method(() => throw 'error'); 45 | _controller.buildAsyncData(builder: builder: (_, data) { 46 | // this function runs only on success 47 | return Text(data); 48 | }) 49 | ``` 50 | Example: [failure_handling.dart](example/lib/failure_handling.dart) 51 | 52 | ### Custom loading and error handling 53 | Loading and error handling widgets are created by AsyncDataDecoration. You may override their behavior by creating custom AsyncDataDecoration. The same decorator can then be used in every AsyncData in your app. 54 | ```dart 55 | class CustomAsyncDataDecoration extends AsyncDataDecoration { 56 | @override 57 | Widget buildError(BuildContext context, dynamic error, VoidCallback tryAgain) { 58 | return Text('Sorry :('); 59 | } 60 | } 61 | 62 | final _controller = AsyncController.method(() => throw 'error'); 63 | _controller.buildAsyncData( 64 | builder: buildContent, 65 | decorator: CustomAsyncDataDecoration(), 66 | ) 67 | ``` 68 | 69 | Example: [failure_handling_custom.dart](example/lib/failure_handling_custom.dart) 70 | 71 | ### Automatic refresh 72 | 73 | Every AsyncController can be customized to automatically refresh itself in certain situations. 74 | 75 | * Refresh after network connection comes back. 76 | ```dart 77 | controller.addRefresher(OnReconnectedRefresher()); 78 | ``` 79 | 80 | * Refresh data every X seconds. 81 | ```dart 82 | controller.addRefresher(PeriodicRefresher(Duration(seconds: 3))); 83 | ``` 84 | 85 | * Refresh after user resumes the app from background. 86 | ```dart 87 | controller.addRefresher(InForegroundRefresher()); 88 | ``` 89 | 90 | * Refresh when another ChangeNotifier updates. 91 | ```dart 92 | controller.addRefresher(ListenerRefresher(listenable)); 93 | ``` 94 | 95 | 5. Easy to use customization through AsyncDataDecoration. 96 | ```dart 97 | class MyDecoration extends AsyncDataDecoration { 98 | @override 99 | Widget buildNoDataYet(BuildContext context) => MyProgressIndicator(); 100 | } 101 | ``` 102 | Example: [refreshers_page.dart](example/lib/refreshers_page.dart) 103 | 104 | #### Paginated data 105 | 106 | `PagedAsyncController` class, which extends AsyncController, is capable of loading data in pages. 107 | 108 | Example: [paged_loading.dart](example/lib/paged_loading.dart) 109 | 110 | #### Filtering & searching 111 | 112 | AsyncController can be extended to implement filtering & searching. You do this by extending FilteringAsyncController. 113 | Example: [sort_and_search.dart](example/lib/sort_and_search.dart) 114 | 115 | #### Async button 116 | 117 | Not really related to AsyncController, but still useful. AsyncButton is a button that handles async onPressed methods. When user presses the button: 118 | * starts the async operation provided in onPressed method 119 | * shows loading indicator 120 | * blocks the user interface to avoid typing on keyboard or leaving the page 121 | * in case error, shows snackbar 122 | * finally, cleans up loading indicator & interface lock 123 | 124 | ```dart 125 | AsyncButton( 126 | // AsyncButtons takes a child like typical button 127 | child: const Text('Press me!'), 128 | // AsyncButton accepts async onPressed method and handles it 129 | onPressed: () => Future.delayed(Duration(seconds: 1)), 130 | // Through builder method we can support any kind of button 131 | builder: (x) => FlatButton( 132 | onPressed: x.onPressed, 133 | child: x.child, 134 | ), 135 | ), 136 | ``` 137 | Example: [async_button_example.dart](example/lib/async_button_example.dart) 138 | 139 | ### Interaction with other packages 140 | 141 | AsyncController plays nicely with others. It implements ChangeNotifier and ValueListenable - classes commonly used inside Flutter. You can use it with any state management / dependency injection that you want. The example project includes samples for flutter_hooks and provider. 142 | 143 | Example 1: [hooks_example.dart](example/lib/hooks_example.dart) 144 | 145 | Example 2: [provider_example.dart](example/lib/provider_example.dart) -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lint/analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | # it should be automatic 6 | prefer_const_constructors: false 7 | 8 | # occurs even if type was inferred correctly 9 | type_annotate_public_apis: false 10 | 11 | # statics are better because they can be used as callbacks 12 | prefer_constructors_over_static_methods: false 13 | 14 | # Ii prefer to sort by importance 15 | sort_pub_dependencies: false -------------------------------------------------------------------------------- /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 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .packages 26 | .pub-cache/ 27 | .pub/ 28 | /build/ 29 | 30 | # Android related 31 | **/android/**/gradle-wrapper.jar 32 | **/android/.gradle 33 | **/android/captures/ 34 | **/android/gradlew 35 | **/android/gradlew.bat 36 | **/android/local.properties 37 | **/android/**/GeneratedPluginRegistrant.java 38 | 39 | # iOS/XCode related 40 | **/ios/**/*.mode1v3 41 | **/ios/**/*.mode2v3 42 | **/ios/**/*.moved-aside 43 | **/ios/**/*.pbxuser 44 | **/ios/**/*.perspectivev3 45 | **/ios/**/*sync/ 46 | **/ios/**/.sconsign.dblite 47 | **/ios/**/.tags* 48 | **/ios/**/.vagrant/ 49 | **/ios/**/DerivedData/ 50 | **/ios/**/Icon? 51 | **/ios/**/Pods/ 52 | **/ios/**/.symlinks/ 53 | **/ios/**/profile 54 | **/ios/**/xcuserdata 55 | **/ios/.generated/ 56 | **/ios/Flutter/App.framework 57 | **/ios/Flutter/Flutter.framework 58 | **/ios/Flutter/Generated.xcconfig 59 | **/ios/Flutter/app.flx 60 | **/ios/Flutter/app.zip 61 | **/ios/Flutter/flutter_assets/ 62 | **/ios/ServiceDefinitions.json 63 | **/ios/Runner/GeneratedPluginRegistrant.* 64 | 65 | # Exceptions to above rules. 66 | !**/ios/**/default.mode1v3 67 | !**/ios/**/default.mode2v3 68 | !**/ios/**/default.pbxuser 69 | !**/ios/**/default.perspectivev3 70 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 71 | -------------------------------------------------------------------------------- /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: 0545c63b9baca0d40c2126dba6366fad2b16d3c1 8 | channel: master 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/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 from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.example.example" 37 | minSdkVersion 16 38 | targetSdkVersion 28 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.12' 59 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 60 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 61 | } 62 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 13 | 20 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.example; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/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/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/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/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/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/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/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/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.2.1' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /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-4.10.2-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: This podspec is NOT to be published. It is only used as a local source! 3 | # This is a generated file; do not edit or check into version control. 4 | # 5 | 6 | Pod::Spec.new do |s| 7 | s.name = 'Flutter' 8 | s.version = '1.0.0' 9 | s.summary = 'High-performance, high-fidelity mobile apps.' 10 | s.homepage = 'https://flutter.io' 11 | s.license = { :type => 'MIT' } 12 | s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } 13 | s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } 14 | s.ios.deployment_target = '8.0' 15 | # Framework linking is handled by Flutter tooling, not CocoaPods. 16 | # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. 17 | s.vendored_frameworks = 'path/to/nothing' 18 | end 19 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 32 | end 33 | 34 | post_install do |installer| 35 | installer.pods_project.targets.each do |target| 36 | flutter_additional_ios_build_settings(target) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - connectivity (0.0.1): 3 | - Flutter 4 | - Reachability 5 | - Flutter (1.0.0) 6 | - Reachability (3.2) 7 | 8 | DEPENDENCIES: 9 | - connectivity (from `.symlinks/plugins/connectivity/ios`) 10 | - Flutter (from `Flutter`) 11 | 12 | SPEC REPOS: 13 | trunk: 14 | - Reachability 15 | 16 | EXTERNAL SOURCES: 17 | connectivity: 18 | :path: ".symlinks/plugins/connectivity/ios" 19 | Flutter: 20 | :path: Flutter 21 | 22 | SPEC CHECKSUMS: 23 | connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 24 | Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c 25 | Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 26 | 27 | PODFILE CHECKSUM: 8e679eca47255a8ca8067c4c67aab20e64cb974d 28 | 29 | COCOAPODS: 1.10.0 30 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 13 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 14 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 15 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 16 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 17 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 18 | F78427E9E14AAECE807EB6A9 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C613A7FBB93633DC304EE9B1 /* libPods-Runner.a */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXCopyFilesBuildPhase section */ 22 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 23 | isa = PBXCopyFilesBuildPhase; 24 | buildActionMask = 2147483647; 25 | dstPath = ""; 26 | dstSubfolderSpec = 10; 27 | files = ( 28 | ); 29 | name = "Embed Frameworks"; 30 | runOnlyForDeploymentPostprocessing = 0; 31 | }; 32 | /* End PBXCopyFilesBuildPhase section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | 02D6F39DD50523BA0E598591 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 36 | 04ACF6CE63BDCE5BC5CB54CB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 37 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 38 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 39 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 40 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 41 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 42 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 43 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 44 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 45 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 47 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 48 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 49 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 50 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | 9CA13D0217CEA4D5BDB52C3E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 52 | C613A7FBB93633DC304EE9B1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | /* End PBXFileReference section */ 54 | 55 | /* Begin PBXFrameworksBuildPhase section */ 56 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | F78427E9E14AAECE807EB6A9 /* libPods-Runner.a in Frameworks */, 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | 5641CDED384EE72F094EBD54 /* Frameworks */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | C613A7FBB93633DC304EE9B1 /* libPods-Runner.a */, 71 | ); 72 | name = Frameworks; 73 | sourceTree = ""; 74 | }; 75 | 9740EEB11CF90186004384FC /* Flutter */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 79 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 80 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 81 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 82 | ); 83 | name = Flutter; 84 | sourceTree = ""; 85 | }; 86 | 97C146E51CF9000F007C117D = { 87 | isa = PBXGroup; 88 | children = ( 89 | 9740EEB11CF90186004384FC /* Flutter */, 90 | 97C146F01CF9000F007C117D /* Runner */, 91 | 97C146EF1CF9000F007C117D /* Products */, 92 | DEDC32FD1CE0FEE2C2DDB3C8 /* Pods */, 93 | 5641CDED384EE72F094EBD54 /* Frameworks */, 94 | ); 95 | sourceTree = ""; 96 | }; 97 | 97C146EF1CF9000F007C117D /* Products */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 97C146EE1CF9000F007C117D /* Runner.app */, 101 | ); 102 | name = Products; 103 | sourceTree = ""; 104 | }; 105 | 97C146F01CF9000F007C117D /* Runner */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 109 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 110 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 111 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 112 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 113 | 97C147021CF9000F007C117D /* Info.plist */, 114 | 97C146F11CF9000F007C117D /* Supporting Files */, 115 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 116 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 117 | ); 118 | path = Runner; 119 | sourceTree = ""; 120 | }; 121 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 97C146F21CF9000F007C117D /* main.m */, 125 | ); 126 | name = "Supporting Files"; 127 | sourceTree = ""; 128 | }; 129 | DEDC32FD1CE0FEE2C2DDB3C8 /* Pods */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 9CA13D0217CEA4D5BDB52C3E /* Pods-Runner.debug.xcconfig */, 133 | 04ACF6CE63BDCE5BC5CB54CB /* Pods-Runner.release.xcconfig */, 134 | 02D6F39DD50523BA0E598591 /* Pods-Runner.profile.xcconfig */, 135 | ); 136 | path = Pods; 137 | sourceTree = ""; 138 | }; 139 | /* End PBXGroup section */ 140 | 141 | /* Begin PBXNativeTarget section */ 142 | 97C146ED1CF9000F007C117D /* Runner */ = { 143 | isa = PBXNativeTarget; 144 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 145 | buildPhases = ( 146 | F076B6EBEB3B8CDB118E6DD9 /* [CP] Check Pods Manifest.lock */, 147 | 9740EEB61CF901F6004384FC /* Run Script */, 148 | 97C146EA1CF9000F007C117D /* Sources */, 149 | 97C146EB1CF9000F007C117D /* Frameworks */, 150 | 97C146EC1CF9000F007C117D /* Resources */, 151 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 152 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 153 | ); 154 | buildRules = ( 155 | ); 156 | dependencies = ( 157 | ); 158 | name = Runner; 159 | productName = Runner; 160 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 161 | productType = "com.apple.product-type.application"; 162 | }; 163 | /* End PBXNativeTarget section */ 164 | 165 | /* Begin PBXProject section */ 166 | 97C146E61CF9000F007C117D /* Project object */ = { 167 | isa = PBXProject; 168 | attributes = { 169 | LastUpgradeCheck = 0910; 170 | ORGANIZATIONNAME = "The Chromium Authors"; 171 | TargetAttributes = { 172 | 97C146ED1CF9000F007C117D = { 173 | CreatedOnToolsVersion = 7.3.1; 174 | DevelopmentTeam = T9NKFAZ2T3; 175 | }; 176 | }; 177 | }; 178 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 179 | compatibilityVersion = "Xcode 3.2"; 180 | developmentRegion = English; 181 | hasScannedForEncodings = 0; 182 | knownRegions = ( 183 | English, 184 | en, 185 | Base, 186 | ); 187 | mainGroup = 97C146E51CF9000F007C117D; 188 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 189 | projectDirPath = ""; 190 | projectRoot = ""; 191 | targets = ( 192 | 97C146ED1CF9000F007C117D /* Runner */, 193 | ); 194 | }; 195 | /* End PBXProject section */ 196 | 197 | /* Begin PBXResourcesBuildPhase section */ 198 | 97C146EC1CF9000F007C117D /* Resources */ = { 199 | isa = PBXResourcesBuildPhase; 200 | buildActionMask = 2147483647; 201 | files = ( 202 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 203 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 204 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 205 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 206 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 207 | ); 208 | runOnlyForDeploymentPostprocessing = 0; 209 | }; 210 | /* End PBXResourcesBuildPhase section */ 211 | 212 | /* Begin PBXShellScriptBuildPhase section */ 213 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 214 | isa = PBXShellScriptBuildPhase; 215 | buildActionMask = 2147483647; 216 | files = ( 217 | ); 218 | inputPaths = ( 219 | ); 220 | name = "Thin Binary"; 221 | outputPaths = ( 222 | ); 223 | runOnlyForDeploymentPostprocessing = 0; 224 | shellPath = /bin/sh; 225 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 226 | }; 227 | 9740EEB61CF901F6004384FC /* Run Script */ = { 228 | isa = PBXShellScriptBuildPhase; 229 | buildActionMask = 2147483647; 230 | files = ( 231 | ); 232 | inputPaths = ( 233 | ); 234 | name = "Run Script"; 235 | outputPaths = ( 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | shellPath = /bin/sh; 239 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 240 | }; 241 | F076B6EBEB3B8CDB118E6DD9 /* [CP] Check Pods Manifest.lock */ = { 242 | isa = PBXShellScriptBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | ); 246 | inputFileListPaths = ( 247 | ); 248 | inputPaths = ( 249 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 250 | "${PODS_ROOT}/Manifest.lock", 251 | ); 252 | name = "[CP] Check Pods Manifest.lock"; 253 | outputFileListPaths = ( 254 | ); 255 | outputPaths = ( 256 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 257 | ); 258 | runOnlyForDeploymentPostprocessing = 0; 259 | shellPath = /bin/sh; 260 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 261 | showEnvVarsInLog = 0; 262 | }; 263 | /* End PBXShellScriptBuildPhase section */ 264 | 265 | /* Begin PBXSourcesBuildPhase section */ 266 | 97C146EA1CF9000F007C117D /* Sources */ = { 267 | isa = PBXSourcesBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 271 | 97C146F31CF9000F007C117D /* main.m in Sources */, 272 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 273 | ); 274 | runOnlyForDeploymentPostprocessing = 0; 275 | }; 276 | /* End PBXSourcesBuildPhase section */ 277 | 278 | /* Begin PBXVariantGroup section */ 279 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 280 | isa = PBXVariantGroup; 281 | children = ( 282 | 97C146FB1CF9000F007C117D /* Base */, 283 | ); 284 | name = Main.storyboard; 285 | sourceTree = ""; 286 | }; 287 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 288 | isa = PBXVariantGroup; 289 | children = ( 290 | 97C147001CF9000F007C117D /* Base */, 291 | ); 292 | name = LaunchScreen.storyboard; 293 | sourceTree = ""; 294 | }; 295 | /* End PBXVariantGroup section */ 296 | 297 | /* Begin XCBuildConfiguration section */ 298 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ALWAYS_SEARCH_USER_PATHS = NO; 302 | CLANG_ANALYZER_NONNULL = YES; 303 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 304 | CLANG_CXX_LIBRARY = "libc++"; 305 | CLANG_ENABLE_MODULES = YES; 306 | CLANG_ENABLE_OBJC_ARC = YES; 307 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 308 | CLANG_WARN_BOOL_CONVERSION = YES; 309 | CLANG_WARN_COMMA = YES; 310 | CLANG_WARN_CONSTANT_CONVERSION = YES; 311 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 312 | CLANG_WARN_EMPTY_BODY = YES; 313 | CLANG_WARN_ENUM_CONVERSION = YES; 314 | CLANG_WARN_INFINITE_RECURSION = YES; 315 | CLANG_WARN_INT_CONVERSION = YES; 316 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 317 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 318 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 319 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 320 | CLANG_WARN_STRICT_PROTOTYPES = YES; 321 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 322 | CLANG_WARN_UNREACHABLE_CODE = YES; 323 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 324 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 325 | COPY_PHASE_STRIP = NO; 326 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 327 | ENABLE_NS_ASSERTIONS = NO; 328 | ENABLE_STRICT_OBJC_MSGSEND = YES; 329 | GCC_C_LANGUAGE_STANDARD = gnu99; 330 | GCC_NO_COMMON_BLOCKS = YES; 331 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 332 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 333 | GCC_WARN_UNDECLARED_SELECTOR = YES; 334 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 335 | GCC_WARN_UNUSED_FUNCTION = YES; 336 | GCC_WARN_UNUSED_VARIABLE = YES; 337 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 338 | MTL_ENABLE_DEBUG_INFO = NO; 339 | SDKROOT = iphoneos; 340 | TARGETED_DEVICE_FAMILY = "1,2"; 341 | VALIDATE_PRODUCT = YES; 342 | }; 343 | name = Profile; 344 | }; 345 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 346 | isa = XCBuildConfiguration; 347 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 348 | buildSettings = { 349 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 350 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 351 | DEVELOPMENT_TEAM = S8QB4VV633; 352 | ENABLE_BITCODE = NO; 353 | FRAMEWORK_SEARCH_PATHS = ( 354 | "$(inherited)", 355 | "$(PROJECT_DIR)/Flutter", 356 | ); 357 | INFOPLIST_FILE = Runner/Info.plist; 358 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 359 | LIBRARY_SEARCH_PATHS = ( 360 | "$(inherited)", 361 | "$(PROJECT_DIR)/Flutter", 362 | ); 363 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 364 | PRODUCT_NAME = "$(TARGET_NAME)"; 365 | VERSIONING_SYSTEM = "apple-generic"; 366 | }; 367 | name = Profile; 368 | }; 369 | 97C147031CF9000F007C117D /* Debug */ = { 370 | isa = XCBuildConfiguration; 371 | buildSettings = { 372 | ALWAYS_SEARCH_USER_PATHS = NO; 373 | CLANG_ANALYZER_NONNULL = YES; 374 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 375 | CLANG_CXX_LIBRARY = "libc++"; 376 | CLANG_ENABLE_MODULES = YES; 377 | CLANG_ENABLE_OBJC_ARC = YES; 378 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 379 | CLANG_WARN_BOOL_CONVERSION = YES; 380 | CLANG_WARN_COMMA = YES; 381 | CLANG_WARN_CONSTANT_CONVERSION = YES; 382 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 383 | CLANG_WARN_EMPTY_BODY = YES; 384 | CLANG_WARN_ENUM_CONVERSION = YES; 385 | CLANG_WARN_INFINITE_RECURSION = YES; 386 | CLANG_WARN_INT_CONVERSION = YES; 387 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 388 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 389 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 390 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 391 | CLANG_WARN_STRICT_PROTOTYPES = YES; 392 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 393 | CLANG_WARN_UNREACHABLE_CODE = YES; 394 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 395 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 396 | COPY_PHASE_STRIP = NO; 397 | DEBUG_INFORMATION_FORMAT = dwarf; 398 | ENABLE_STRICT_OBJC_MSGSEND = YES; 399 | ENABLE_TESTABILITY = YES; 400 | GCC_C_LANGUAGE_STANDARD = gnu99; 401 | GCC_DYNAMIC_NO_PIC = NO; 402 | GCC_NO_COMMON_BLOCKS = YES; 403 | GCC_OPTIMIZATION_LEVEL = 0; 404 | GCC_PREPROCESSOR_DEFINITIONS = ( 405 | "DEBUG=1", 406 | "$(inherited)", 407 | ); 408 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 409 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 410 | GCC_WARN_UNDECLARED_SELECTOR = YES; 411 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 412 | GCC_WARN_UNUSED_FUNCTION = YES; 413 | GCC_WARN_UNUSED_VARIABLE = YES; 414 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 415 | MTL_ENABLE_DEBUG_INFO = YES; 416 | ONLY_ACTIVE_ARCH = YES; 417 | SDKROOT = iphoneos; 418 | TARGETED_DEVICE_FAMILY = "1,2"; 419 | }; 420 | name = Debug; 421 | }; 422 | 97C147041CF9000F007C117D /* Release */ = { 423 | isa = XCBuildConfiguration; 424 | buildSettings = { 425 | ALWAYS_SEARCH_USER_PATHS = NO; 426 | CLANG_ANALYZER_NONNULL = YES; 427 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 428 | CLANG_CXX_LIBRARY = "libc++"; 429 | CLANG_ENABLE_MODULES = YES; 430 | CLANG_ENABLE_OBJC_ARC = YES; 431 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 432 | CLANG_WARN_BOOL_CONVERSION = YES; 433 | CLANG_WARN_COMMA = YES; 434 | CLANG_WARN_CONSTANT_CONVERSION = YES; 435 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 436 | CLANG_WARN_EMPTY_BODY = YES; 437 | CLANG_WARN_ENUM_CONVERSION = YES; 438 | CLANG_WARN_INFINITE_RECURSION = YES; 439 | CLANG_WARN_INT_CONVERSION = YES; 440 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 441 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 442 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 443 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 444 | CLANG_WARN_STRICT_PROTOTYPES = YES; 445 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 446 | CLANG_WARN_UNREACHABLE_CODE = YES; 447 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 448 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 449 | COPY_PHASE_STRIP = NO; 450 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 451 | ENABLE_NS_ASSERTIONS = NO; 452 | ENABLE_STRICT_OBJC_MSGSEND = YES; 453 | GCC_C_LANGUAGE_STANDARD = gnu99; 454 | GCC_NO_COMMON_BLOCKS = YES; 455 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 456 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 457 | GCC_WARN_UNDECLARED_SELECTOR = YES; 458 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 459 | GCC_WARN_UNUSED_FUNCTION = YES; 460 | GCC_WARN_UNUSED_VARIABLE = YES; 461 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 462 | MTL_ENABLE_DEBUG_INFO = NO; 463 | SDKROOT = iphoneos; 464 | TARGETED_DEVICE_FAMILY = "1,2"; 465 | VALIDATE_PRODUCT = YES; 466 | }; 467 | name = Release; 468 | }; 469 | 97C147061CF9000F007C117D /* Debug */ = { 470 | isa = XCBuildConfiguration; 471 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 472 | buildSettings = { 473 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 474 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 475 | DEVELOPMENT_TEAM = T9NKFAZ2T3; 476 | ENABLE_BITCODE = NO; 477 | FRAMEWORK_SEARCH_PATHS = ( 478 | "$(inherited)", 479 | "$(PROJECT_DIR)/Flutter", 480 | ); 481 | INFOPLIST_FILE = Runner/Info.plist; 482 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 483 | LIBRARY_SEARCH_PATHS = ( 484 | "$(inherited)", 485 | "$(PROJECT_DIR)/Flutter", 486 | ); 487 | PRODUCT_BUNDLE_IDENTIFIER = szotp.Sample; 488 | PRODUCT_NAME = "$(TARGET_NAME)"; 489 | VERSIONING_SYSTEM = "apple-generic"; 490 | }; 491 | name = Debug; 492 | }; 493 | 97C147071CF9000F007C117D /* Release */ = { 494 | isa = XCBuildConfiguration; 495 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 496 | buildSettings = { 497 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 498 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 499 | DEVELOPMENT_TEAM = T9NKFAZ2T3; 500 | ENABLE_BITCODE = NO; 501 | FRAMEWORK_SEARCH_PATHS = ( 502 | "$(inherited)", 503 | "$(PROJECT_DIR)/Flutter", 504 | ); 505 | INFOPLIST_FILE = Runner/Info.plist; 506 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 507 | LIBRARY_SEARCH_PATHS = ( 508 | "$(inherited)", 509 | "$(PROJECT_DIR)/Flutter", 510 | ); 511 | PRODUCT_BUNDLE_IDENTIFIER = szotp.Sample; 512 | PRODUCT_NAME = "$(TARGET_NAME)"; 513 | VERSIONING_SYSTEM = "apple-generic"; 514 | }; 515 | name = Release; 516 | }; 517 | /* End XCBuildConfiguration section */ 518 | 519 | /* Begin XCConfigurationList section */ 520 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 521 | isa = XCConfigurationList; 522 | buildConfigurations = ( 523 | 97C147031CF9000F007C117D /* Debug */, 524 | 97C147041CF9000F007C117D /* Release */, 525 | 249021D3217E4FDB00AE95B9 /* Profile */, 526 | ); 527 | defaultConfigurationIsVisible = 0; 528 | defaultConfigurationName = Release; 529 | }; 530 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 531 | isa = XCConfigurationList; 532 | buildConfigurations = ( 533 | 97C147061CF9000F007C117D /* Debug */, 534 | 97C147071CF9000F007C117D /* Release */, 535 | 249021D4217E4FDB00AE95B9 /* Profile */, 536 | ); 537 | defaultConfigurationIsVisible = 0; 538 | defaultConfigurationName = Release; 539 | }; 540 | /* End XCConfigurationList section */ 541 | }; 542 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 543 | } 544 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szotp/async_controller/1f64f1135bdfe6a7f9515fd8b7083e02ae985c1a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/lib/async_button_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/async_controller.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'helpers.dart'; 5 | 6 | class AsyncButtonPage extends StatefulWidget with ExamplePage { 7 | @override 8 | String get title => 'Async button'; 9 | 10 | @override 11 | _AsyncButtonPageState createState() => _AsyncButtonPageState(); 12 | } 13 | 14 | class _AsyncButtonPageState extends State { 15 | int _counter = 0; 16 | 17 | Future success() async { 18 | await Future.delayed(const Duration(seconds: 1)); 19 | setState(() { 20 | _counter++; 21 | }); 22 | } 23 | 24 | Future failure() async { 25 | await Future.delayed(const Duration(seconds: 1)); 26 | throw 'Failed'; 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return Scaffold( 32 | appBar: widget.buildAppBar(), 33 | body: Center( 34 | child: Column( 35 | mainAxisSize: MainAxisSize.min, 36 | children: [ 37 | Text('Clicks: $_counter'), 38 | const SizedBox(height: 16), 39 | AsyncButton( 40 | onPressed: success, 41 | builder: buttonStyleOne, 42 | child: const Text('This will work'), 43 | ), 44 | AsyncButton( 45 | onPressed: failure, 46 | builder: buttonStyleTwo, 47 | loadingColor: Colors.white, 48 | child: const Text('This will fail'), 49 | ), 50 | AsyncButton( 51 | onPressed: success, 52 | builder: buttonStyleOne, 53 | lockInterface: false, 54 | child: const Text('This will not lock interface'), 55 | ), 56 | AsyncButton( 57 | // AsyncButton accepts async onPressed method and handles it 58 | onPressed: () => Future.delayed(const Duration(seconds: 1)), 59 | // Through builder method we can support any kind of button 60 | builder: (x) => TextButton( 61 | onPressed: x.onPressed, 62 | child: x.child, 63 | ), 64 | // AsyncButtons takes a child like typical button 65 | child: const Text('Press me!'), 66 | ), 67 | ], 68 | ), 69 | ), 70 | ); 71 | } 72 | 73 | Widget buttonStyleOne(AsyncButtonSettings settings) { 74 | return SizedBox( 75 | width: 200, 76 | child: OutlinedButton( 77 | onPressed: settings.onPressed, 78 | style: 79 | ButtonStyle(foregroundColor: MaterialStateProperty.all(Colors.red)), 80 | child: settings.child, 81 | ), 82 | ); 83 | } 84 | 85 | Widget buttonStyleTwo(AsyncButtonSettings settings) { 86 | return SizedBox( 87 | width: 200, 88 | child: TextButton( 89 | onPressed: settings.onPressed, 90 | style: ButtonStyle( 91 | backgroundColor: MaterialStateProperty.all(Colors.orange), 92 | foregroundColor: MaterialStateProperty.all(Colors.white), 93 | ), 94 | child: settings.child, 95 | ), 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /example/lib/failure_handling.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/async_controller.dart'; 2 | import 'package:example/helpers.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class FailureHandlingPage extends StatefulWidget with ExamplePage { 6 | @override 7 | _FailureHandlingPageState createState() => _FailureHandlingPageState(); 8 | 9 | @override 10 | String get title => 'Failure handling'; 11 | } 12 | 13 | class _FailureHandlingPageState extends State { 14 | final _controller = AsyncController.method(() async { 15 | await Future.delayed(Duration(seconds: 1)); 16 | throw 'Sorry, loading failed.'; 17 | }); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Scaffold( 22 | appBar: widget.buildAppBar(), 23 | body: _controller.buildAsyncData(builder: (_, data) { 24 | return const Text('OK'); 25 | }), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/lib/failure_handling_custom.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/async_controller.dart'; 2 | import 'package:example/helpers.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class FailureHandlingCustomPage extends StatefulWidget with ExamplePage { 6 | @override 7 | _FailureHandlingCustomPageState createState() => 8 | _FailureHandlingCustomPageState(); 9 | 10 | @override 11 | String get title => 'Failure handling custom'; 12 | } 13 | 14 | class _FailureHandlingCustomPageState extends State { 15 | final _controller = AsyncController.method(() async { 16 | await Future.delayed(Duration(seconds: 1)); 17 | throw 'Sorry, loading failed.'; 18 | }); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Scaffold( 23 | appBar: widget.buildAppBar(), 24 | body: _controller.buildAsyncData( 25 | builder: (_, data) { 26 | return const Text('OK'); 27 | }, 28 | decorator: CustomAsyncDataDecoration(), 29 | ), 30 | ); 31 | } 32 | } 33 | 34 | class CustomAsyncDataDecoration extends AsyncDataDecoration { 35 | @override 36 | Widget buildError( 37 | BuildContext context, dynamic error, VoidCallback tryAgain) { 38 | return Center(child: Text('Sorry :(')); 39 | } 40 | 41 | @override 42 | Widget buildNoDataYet(BuildContext context) { 43 | return Center(child: Text('Loading...')); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/lib/helpers.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CasePickerItem { 4 | const CasePickerItem(this.title, this.builder); 5 | 6 | final String title; 7 | final WidgetBuilder builder; 8 | } 9 | 10 | // ignore: avoid_implementing_value_types 11 | abstract class ExamplePage implements Widget { 12 | String get title; 13 | 14 | AppBar buildAppBar() { 15 | return AppBar(title: Text(title)); 16 | } 17 | } 18 | 19 | class CasePicker extends StatefulWidget { 20 | const CasePicker({Key? key, this.cases, this.appBar}) : super(key: key); 21 | 22 | final AppBar? appBar; 23 | final List? cases; 24 | 25 | @override 26 | _CasePickerState createState() => _CasePickerState(); 27 | } 28 | 29 | class _CasePickerState extends State { 30 | int? index = 0; 31 | 32 | void setIndex(int newIndex) { 33 | var index = newIndex; 34 | final last = widget.cases!.length - 1; 35 | 36 | if (newIndex < 0) { 37 | index = last; 38 | } 39 | 40 | if (newIndex > last) { 41 | index = 0; 42 | } 43 | 44 | setState(() { 45 | this.index = index; 46 | }); 47 | } 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | final item = widget.cases![index!]; 52 | 53 | var i = 0; 54 | final items = widget.cases!.map((x) { 55 | return DropdownMenuItem( 56 | value: i++, 57 | child: Text(x.title), 58 | ); 59 | }); 60 | 61 | return Scaffold( 62 | appBar: widget.appBar, 63 | body: Column( 64 | crossAxisAlignment: CrossAxisAlignment.stretch, 65 | children: [ 66 | Row( 67 | children: [ 68 | IconButton( 69 | icon: Icon(Icons.chevron_left), 70 | onPressed: () { 71 | setIndex(index! - 1); 72 | }, 73 | ), 74 | Expanded( 75 | child: DropdownButton( 76 | isExpanded: true, 77 | value: index, 78 | items: items.toList(), 79 | onChanged: (i) { 80 | setState(() { 81 | index = i; 82 | }); 83 | }, 84 | ), 85 | ), 86 | IconButton( 87 | icon: Icon(Icons.chevron_right), 88 | onPressed: () { 89 | setIndex(index! + 1); 90 | }, 91 | ), 92 | ], 93 | ), 94 | Expanded( 95 | child: Builder(builder: item.builder), 96 | ), 97 | ], 98 | ), 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /example/lib/hooks_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/async_controller.dart'; 2 | import 'package:example/helpers.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_hooks/flutter_hooks.dart'; 5 | 6 | class HooksExamplePage extends HookWidget with ExamplePage { 7 | Future fetch() async { 8 | await Future.delayed(Duration(seconds: 1)); 9 | return 'Hello world'; 10 | } 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final loader = useNewChangeNotifier(() => AsyncController.method(fetch))!; 15 | return Scaffold( 16 | appBar: buildAppBar(), 17 | body: loader.buildAsyncData(builder: (_, data) { 18 | return Center(child: Text(data)); 19 | }), 20 | ); 21 | } 22 | 23 | @override 24 | String get title => 'flutter_hooks'; 25 | } 26 | 27 | /// Creates the provided notifier once, and then disposes it when appropriate. 28 | T? useNewChangeNotifier(T Function() builder) { 29 | return use(_NewChangeNotifierHook(builder: builder)); 30 | } 31 | 32 | class _NewChangeNotifierHook extends Hook { 33 | const _NewChangeNotifierHook({this.builder}); 34 | 35 | final T Function()? builder; 36 | 37 | @override 38 | _NewChangeNotifierHookState createState() => 39 | _NewChangeNotifierHookState(); 40 | } 41 | 42 | class _NewChangeNotifierHookState 43 | extends HookState> { 44 | T? notifier; 45 | 46 | @override 47 | void initHook() { 48 | super.initHook(); 49 | notifier = hook.builder!(); 50 | } 51 | 52 | @override 53 | T? build(BuildContext context) { 54 | return notifier; 55 | } 56 | 57 | @override 58 | void dispose() { 59 | notifier!.dispose(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/helpers.dart'; 2 | import 'package:example/hooks_example.dart'; 3 | import 'package:example/paged_loading.dart'; 4 | import 'package:example/refreshers_page.dart'; 5 | import 'package:example/sort_and_search.dart'; 6 | import 'package:example/stream.dart'; 7 | import 'package:example/updating_example.dart'; 8 | import 'package:flutter/material.dart'; 9 | 10 | // ignore: implementation_imports 11 | import 'package:async_controller/src/debugging.dart'; 12 | 13 | import 'async_button_example.dart'; 14 | import 'failure_handling.dart'; 15 | import 'failure_handling_custom.dart'; 16 | import 'minimal.dart'; 17 | import 'paged_loading_simple.dart'; 18 | import 'provider_example.dart'; 19 | import 'pull_to_refresh.dart'; 20 | import 'translator/translator_page.dart'; 21 | 22 | void main() { 23 | internalDebugLogEnabled = true; 24 | runApp(MyApp()); 25 | } 26 | 27 | class MyApp extends StatelessWidget { 28 | @override 29 | Widget build(BuildContext context) { 30 | return MaterialApp( 31 | home: ExampleSwitcher(), 32 | ); 33 | } 34 | } 35 | 36 | class ExampleSwitcher extends StatelessWidget { 37 | @override 38 | Widget build(BuildContext context) { 39 | final examples = [ 40 | MinimalExample(), 41 | PullToRefreshPage(), 42 | FailureHandlingPage(), 43 | FailureHandlingCustomPage(), 44 | PagedLoadingSimplePage(), 45 | PagedLoadingPage(), 46 | SortAndSearchPage(), 47 | AsyncButtonPage(), 48 | RefreshersPage(), 49 | TranslatorPage(), 50 | ProviderExamplePage(), 51 | HooksExamplePage(), 52 | StreamExamplePage(), 53 | UpdatingExample(), 54 | ]; 55 | 56 | return Scaffold( 57 | appBar: AppBar( 58 | title: Text('async_controller'), 59 | ), 60 | body: ListView.builder( 61 | itemCount: examples.length, 62 | itemBuilder: (context, i) { 63 | final example = examples[i]; 64 | return ListTile( 65 | title: Text(example.title), 66 | onTap: () { 67 | final route = 68 | MaterialPageRoute(builder: (context) => examples[i]); 69 | Navigator.of(context).push(route); 70 | }, 71 | ); 72 | }, 73 | ), 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /example/lib/minimal.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/async_controller.dart'; 2 | import 'package:example/helpers.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class MinimalExample extends StatelessWidget with ExamplePage { 6 | @override 7 | String get title => 'Minimal example'; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: buildAppBar(), 13 | body: Center( 14 | child: Minimal(), 15 | ), 16 | ); 17 | } 18 | } 19 | 20 | /// In real world app, I would recommend using provider or flutter_hooks to create the controller 21 | /// Storing data globally is generally a bad practice. 22 | final _controller = AsyncController.method(() async { 23 | await Future.delayed(Duration(seconds: 1)); 24 | return 'Hello world'; 25 | }); 26 | 27 | class Minimal extends StatelessWidget { 28 | @override 29 | Widget build(BuildContext context) { 30 | return _controller.buildAsyncData(builder: (_, data) { 31 | // This builder runs only if data is available. 32 | // buildAsyncData takes care of other situations 33 | return Text(data); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/lib/paged_loading.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:async_controller/async_controller.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | import 'helpers.dart'; 7 | 8 | /// Utility to mock paged data loading with various situations. 9 | class FakePageDataProvider extends PagedAsyncController { 10 | FakePageDataProvider( 11 | this.totalCount, { 12 | this.errorChance = 0, 13 | this.errorChanceOnFirstPage, 14 | }); 15 | 16 | @override 17 | final int totalCount; 18 | final int errorChance; 19 | final int? errorChanceOnFirstPage; 20 | 21 | @override 22 | Future> fetchPage(int pageIndex) async { 23 | final index = pageSize * pageIndex; 24 | await Future.delayed(Duration(milliseconds: 500)); 25 | 26 | int? chance = errorChance; 27 | if (pageIndex == 0 && errorChanceOnFirstPage != null) { 28 | chance = errorChanceOnFirstPage; 29 | } 30 | 31 | if (chance! > Random().nextInt(100)) { 32 | throw 'Random failure'; 33 | } 34 | 35 | final count = min(totalCount, index + pageSize) - index; 36 | final list = 37 | Iterable.generate(count, (i) => 'Item ${index + i + 1}').toList(); 38 | 39 | return PagedData(pageIndex, totalCount, list); 40 | } 41 | 42 | @override 43 | void deactivate() { 44 | super.deactivate(); 45 | reset(); 46 | } 47 | 48 | int get pageSize => 5; 49 | } 50 | 51 | class PagedLoadingPage extends StatefulWidget with ExamplePage { 52 | @override 53 | String get title => 'Paged data'; 54 | 55 | @override 56 | _PagedLoadingPageState createState() => _PagedLoadingPageState(); 57 | } 58 | 59 | class _PagedLoadingPageState extends State { 60 | final _decorator = PagedListDecoration( 61 | noDataContent: Text('Sorry, no data'), 62 | ); 63 | 64 | CasePickerItem buildCase(String title, FakePageDataProvider provider, 65 | [Widget Function(FakePageDataProvider)? builder]) { 66 | return CasePickerItem(title, (context) => (builder ?? buildList)(provider)); 67 | } 68 | 69 | List? cases; 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | cases ??= [ 74 | buildCase('Always works', FakePageDataProvider(25)), 75 | buildCase('No content', FakePageDataProvider(0)), 76 | buildCase('Always error', FakePageDataProvider(0, errorChance: 100)), 77 | buildCase('Sometimes error', FakePageDataProvider(1000, errorChance: 50)), 78 | buildCase( 79 | 'Always error on next page', 80 | FakePageDataProvider(1000, errorChance: 100, errorChanceOnFirstPage: 0), 81 | ), 82 | buildCase('Grid', FakePageDataProvider(1000), buildGridExample), 83 | ]; 84 | 85 | return CasePicker(appBar: widget.buildAppBar(), cases: cases); 86 | } 87 | 88 | Widget buildGridExample(FakePageDataProvider provider) { 89 | return PagedListView( 90 | controller: provider, 91 | itemBuilder: (context, i, dynamic item) { 92 | return Container( 93 | color: Colors.grey[300], 94 | child: Center(child: Text('$item')), 95 | ); 96 | }, 97 | listBuilder: (context, itemCount, itemBuilder) { 98 | return GridView.builder( 99 | itemBuilder: itemBuilder, 100 | itemCount: itemCount, 101 | padding: EdgeInsets.all(8), 102 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 103 | crossAxisCount: 3, mainAxisSpacing: 8, crossAxisSpacing: 8), 104 | ); 105 | }, 106 | ); 107 | } 108 | 109 | Widget buildList(FakePageDataProvider _controller) { 110 | return PagedListView( 111 | controller: _controller, 112 | itemBuilder: (_, i, data) { 113 | return Text(data); 114 | }, 115 | decoration: _decorator, 116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /example/lib/paged_loading_simple.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/async_controller.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'helpers.dart'; 5 | 6 | class _SimpleController extends PagedAsyncController { 7 | _SimpleController(); 8 | 9 | @override 10 | Future> fetchPage(int pageIndex) async { 11 | await Future.delayed(Duration(milliseconds: 150)); //<----- delay 12 | 13 | return PagedData( 14 | pageIndex, 15 | 1000, 16 | List.generate(10, (i) => i + pageIndex * 10), 17 | ); 18 | } 19 | } 20 | 21 | class PagedLoadingSimplePage extends StatefulWidget with ExamplePage { 22 | @override 23 | String get title => 'Paged data - simplest'; 24 | 25 | @override 26 | _PagedLoadingSimplePageState createState() => _PagedLoadingSimplePageState(); 27 | } 28 | 29 | class _PagedLoadingSimplePageState extends State { 30 | final _controller = _SimpleController(); 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return Scaffold( 35 | appBar: widget.buildAppBar(), 36 | body: PagedListView( 37 | controller: _controller, 38 | itemBuilder: (_, __, item) { 39 | return ListTile(title: Text(item.toString())); 40 | }, 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /example/lib/provider_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/async_controller.dart'; 2 | import 'package:example/helpers.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class _Loader extends AsyncController { 7 | @override 8 | Future fetch(AsyncFetchItem status) async { 9 | await Future.delayed(Duration(seconds: 1)); 10 | return 'Hello world'; 11 | } 12 | 13 | static _Loader create(BuildContext context) => _Loader(); 14 | } 15 | 16 | class ProviderExamplePage extends StatelessWidget with ExamplePage { 17 | @override 18 | Widget build(BuildContext context) { 19 | return ChangeNotifierProvider( 20 | create: _Loader.create, 21 | child: Scaffold( 22 | appBar: buildAppBar(), 23 | body: Builder( 24 | builder: buildBody, 25 | ), 26 | ), 27 | ); 28 | } 29 | 30 | Widget buildBody(BuildContext context) { 31 | final loader = Provider.of<_Loader>(context, listen: false); 32 | return loader.buildAsyncData(builder: (context, data) { 33 | return Center(child: Text(data)); 34 | }); 35 | } 36 | 37 | @override 38 | String get title => 'provider example'; 39 | } 40 | -------------------------------------------------------------------------------- /example/lib/pull_to_refresh.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/async_controller.dart'; 2 | import 'package:example/helpers.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:intl/intl.dart'; 5 | 6 | Future> fetch() async { 7 | await Future.delayed(Duration(seconds: 1)); 8 | final date = DateTime.now(); 9 | final format = DateFormat.Hms(); 10 | return [format.format(date), 'Hello', 'World']; 11 | } 12 | 13 | class PullToRefreshPage extends StatefulWidget with ExamplePage { 14 | @override 15 | _PullToRefreshPageState createState() => _PullToRefreshPageState(); 16 | 17 | @override 18 | String get title => 'Pull to refresh'; 19 | } 20 | 21 | class _PullToRefreshPageState extends State { 22 | final controller = AsyncController.method(fetch) 23 | ..addRefresher(OnReconnectedRefresher()) 24 | ..addRefresher(PeriodicRefresher(Duration(seconds: 10))); 25 | 26 | final formatter = DateFormat.Hms(); 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return Scaffold( 31 | appBar: widget.buildAppBar(), 32 | body: Column( 33 | children: [ 34 | TextButton(onPressed: controller.reset, child: Text('Reset')), 35 | Expanded( 36 | child: RefreshIndicator( 37 | /// Calling controller.refresh in RefreshIndicator is all you need to implement pull to refresh 38 | onRefresh: controller.performUserInitiatedRefresh, 39 | child: AsyncData>( 40 | controller: controller, 41 | builder: (context, data) { 42 | return ListView.builder( 43 | itemCount: data.length, 44 | itemBuilder: (context, i) { 45 | return ListTile( 46 | title: Text(data[i]), 47 | ); 48 | }, 49 | ); 50 | }, 51 | ), 52 | ), 53 | ), 54 | ], 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /example/lib/refreshers_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/async_controller.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | import 'helpers.dart'; 6 | 7 | class RefreshersPage extends StatefulWidget with ExamplePage { 8 | @override 9 | String get title => 'Refreshers'; 10 | 11 | @override 12 | _RefreshersPageState createState() => _RefreshersPageState(); 13 | } 14 | 15 | class _RefreshersPageState extends State { 16 | final controllerA = 17 | AsyncController.method(() => Future.value(DateTime.now())) 18 | ..addRefresher( 19 | PeriodicRefresher(Duration(seconds: 1)), 20 | ); 21 | 22 | final controllerB = 23 | AsyncController.method(() => Future.value(DateTime.now())) 24 | ..addRefresher( 25 | InForegroundRefresher(), 26 | ); 27 | 28 | final controllerC = AsyncController.method(() async { 29 | await Future.delayed(Duration(seconds: 3)); 30 | throw 'Failed'; 31 | }) 32 | ..addRefresher( 33 | OnReconnectedRefresher(), 34 | ); 35 | 36 | final formatter = DateFormat.Hms(); 37 | 38 | Widget buildClock(AsyncController controller, String description) { 39 | return Padding( 40 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), 41 | child: Column( 42 | crossAxisAlignment: CrossAxisAlignment.stretch, 43 | children: [ 44 | Text(description), 45 | SizedBox(height: 4), 46 | Container( 47 | alignment: Alignment.center, 48 | child: controller.buildAsyncData( 49 | builder: (context, date) { 50 | return Text( 51 | formatter.format(date), 52 | style: TextStyle(fontSize: 30), 53 | ); 54 | }, 55 | ), 56 | ), 57 | ], 58 | ), 59 | ); 60 | } 61 | 62 | @override 63 | Widget build(BuildContext context) { 64 | return Scaffold( 65 | appBar: widget.buildAppBar(), 66 | body: ListView( 67 | children: [ 68 | buildClock(controllerA, 'This timer updates every second'), 69 | buildClock( 70 | controllerB, 'This timer updates when app goes to foreground'), 71 | buildClock(controllerC, 72 | 'This timer always fails but tries to refresh when connection goes back. Try toggling airplane mode.') 73 | ], 74 | ), 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /example/lib/sort_and_search.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/async_controller.dart'; 2 | import 'package:faker/faker.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'helpers.dart'; 6 | 7 | enum Sorting { ascending, descending } 8 | 9 | class SearchingController extends FilteringAsyncController { 10 | @override 11 | Future> fetchBase() async { 12 | await Future.delayed(Duration(seconds: 1)); 13 | final faker = Faker(); 14 | return List.generate(100, (_) => faker.person.name()); 15 | } 16 | 17 | @override 18 | Future> transform(List data) async { 19 | final result = data.toList(); 20 | 21 | // apply searching 22 | if (_searchText?.isNotEmpty == true) { 23 | final searchingFor = _searchText!.toLowerCase(); 24 | bool shouldRemove(String x) { 25 | return !x.toLowerCase().contains(searchingFor); 26 | } 27 | 28 | result.removeWhere(shouldRemove); 29 | } 30 | 31 | // apply sorting 32 | result.sort((lhs, rhs) { 33 | if (sorting == Sorting.ascending) { 34 | return lhs.compareTo(rhs); 35 | } else { 36 | return rhs.compareTo(lhs); 37 | } 38 | }); 39 | return result; 40 | } 41 | 42 | String? _searchText; 43 | Sorting sorting = Sorting.ascending; 44 | 45 | void setText(String value) { 46 | _searchText = value; 47 | setNeedsLocalTransform(); 48 | } 49 | 50 | void toggleSorting() { 51 | if (sorting == Sorting.ascending) { 52 | sorting = Sorting.descending; 53 | } else { 54 | sorting = Sorting.ascending; 55 | } 56 | setNeedsLocalTransform(); 57 | } 58 | } 59 | 60 | class SortAndSearchPage extends StatefulWidget with ExamplePage { 61 | @override 62 | String get title => 'Sort & search'; 63 | 64 | @override 65 | _SortAndSearchPageState createState() => _SortAndSearchPageState(); 66 | } 67 | 68 | class _SortAndSearchPageState extends State { 69 | final _controller = SearchingController(); 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | return Scaffold( 74 | appBar: widget.buildAppBar(), 75 | body: Column( 76 | children: [ 77 | Stack( 78 | children: [ 79 | TextField( 80 | onChanged: _controller.setText, 81 | decoration: InputDecoration( 82 | hintText: 'Search', 83 | prefixIcon: Icon(Icons.search), 84 | ), 85 | ), 86 | Align( 87 | alignment: Alignment.centerRight, 88 | child: TextButton( 89 | onPressed: _controller.toggleSorting, 90 | child: _controller.buildAsyncProperty( 91 | selector: () => _controller.sorting, 92 | builder: (context, sorting) { 93 | final asc = sorting == Sorting.ascending; 94 | if (asc) { 95 | return Text('A -> Z'); 96 | } else { 97 | return Text('Z -> A'); 98 | } 99 | }, 100 | ), 101 | ), 102 | ), 103 | ], 104 | ), 105 | Expanded( 106 | child: _controller.buildAsyncData( 107 | builder: (context, data) { 108 | return ListView.builder( 109 | itemCount: data.length, 110 | itemBuilder: (context, i) { 111 | return ListTile( 112 | title: Text(data[i]), 113 | ); 114 | }, 115 | ); 116 | }, 117 | decorator: const PagedListDecoration( 118 | noDataContent: Text('I found nothing...'), 119 | ), 120 | ), 121 | ), 122 | ], 123 | ), 124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /example/lib/stream.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:async_controller/async_controller.dart'; 4 | import 'package:example/helpers.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class _Loader extends StreamAsyncControlelr { 8 | final int failureChance; 9 | 10 | _Loader(this.failureChance); 11 | 12 | final _random = Random(); 13 | 14 | @override 15 | Stream getStream(AsyncFetchItem item) { 16 | return Stream.periodic(Duration(seconds: 1), (i) => i.toString()).map((x) { 17 | final random = _random.nextInt(100); 18 | 19 | if (random < failureChance) { 20 | throw 'error'; 21 | } else { 22 | return x; 23 | } 24 | }); 25 | } 26 | } 27 | 28 | class _ClosingLoader extends StreamAsyncControlelr { 29 | @override 30 | final Duration renewAfter; 31 | 32 | _ClosingLoader(this.renewAfter); 33 | 34 | @override 35 | Stream getStream(AsyncFetchItem status) async* { 36 | await Future.delayed(Duration(seconds: 1)); 37 | yield '0'; 38 | await Future.delayed(Duration(seconds: 1)); 39 | yield '1'; 40 | await Future.delayed(Duration(seconds: 1)); 41 | yield '2'; 42 | } 43 | } 44 | 45 | class StreamExamplePage extends StatefulWidget with ExamplePage { 46 | @override 47 | _StreamExamplePageState createState() => _StreamExamplePageState(); 48 | 49 | @override 50 | String get title => 'stream example'; 51 | } 52 | 53 | class _StreamExamplePageState extends State { 54 | final _loader1 = _Loader(0); 55 | final _loader2 = _Loader(50); 56 | final _loader3 = _ClosingLoader(Duration(seconds: 2)); 57 | 58 | bool _isActive = true; 59 | 60 | Iterable get loaders => [_loader1, _loader2, _loader3]; 61 | 62 | void refreshAll() { 63 | for (final loader in loaders) { 64 | loader.performUserInitiatedRefresh(); 65 | } 66 | } 67 | 68 | void resetAll() { 69 | for (final loader in loaders) { 70 | loader.reset(); 71 | } 72 | } 73 | 74 | void toggle() { 75 | setState(() { 76 | _isActive = !_isActive; 77 | }); 78 | } 79 | 80 | Widget buildContent(BuildContext context, String string) { 81 | final ctrl = AsyncData.of(context)!.controller; 82 | return ctrl.buildAsyncOpacity( 83 | selector: () => ctrl.error == null, 84 | child: Text(string, style: TextStyle(fontSize: 30)), 85 | ); 86 | } 87 | 88 | @override 89 | Widget build(BuildContext context) { 90 | return Scaffold( 91 | appBar: widget.buildAppBar(), 92 | body: Center( 93 | child: Column( 94 | children: [ 95 | TextButton( 96 | onPressed: refreshAll, 97 | child: 98 | Text('Refresh (restarts streams without losing last value)'), 99 | ), 100 | TextButton( 101 | onPressed: resetAll, 102 | child: Text('Reset (clears everything)'), 103 | ), 104 | TextButton( 105 | onPressed: toggle, 106 | child: Text( 107 | 'Toggle activation (restarts stream after becoming visible)'), 108 | ), 109 | SizedBox(height: 30), 110 | Expanded(child: SizedBox()), 111 | if (_isActive) 112 | Column( 113 | children: [ 114 | Text('Always works:'), 115 | _loader1.buildAsyncData(builder: buildContent), 116 | Text('Sometimes fails (becomes gray):'), 117 | _loader2.buildAsyncData(builder: buildContent), 118 | Text('Closes after 3, renews itself:'), 119 | _loader3.buildAsyncData(builder: buildContent), 120 | ], 121 | ), 122 | Expanded(child: SizedBox()), 123 | ], 124 | ), 125 | ), 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /example/lib/translator/translator_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:async_controller/async_controller.dart'; 4 | import 'package:example/helpers.dart'; 5 | import 'package:example/updating_example.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:translator/translator.dart'; 8 | 9 | class TranslatorController extends UpdatingController { 10 | final _service = GoogleTranslator(); 11 | 12 | TranslatorController() : super(''); 13 | 14 | @override 15 | Future update(AsyncFetchItem item, Set keys) async { 16 | if (data.value.length > 3) { 17 | _translated = (await _service.translate(data.value)).text; 18 | } else { 19 | _translated = ''; 20 | } 21 | } 22 | 23 | String? _translated; 24 | 25 | String? get translated => _translated; 26 | } 27 | 28 | class TranslatorPage extends StatefulWidget with ExamplePage { 29 | @override 30 | String get title => 'translator'; 31 | 32 | @override 33 | _TranslatorPageState createState() => _TranslatorPageState(); 34 | } 35 | 36 | class _TranslatorPageState extends State { 37 | final _translator = TranslatorController(); 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return Scaffold( 42 | appBar: widget.buildAppBar(), 43 | body: Padding( 44 | padding: const EdgeInsets.all(8.0), 45 | child: Column( 46 | children: [ 47 | TextField( 48 | onChanged: _translator.data.update, 49 | decoration: InputDecoration( 50 | suffixIcon: SyncSuffix(property: _translator.data), 51 | ), 52 | ), 53 | SizedBox(height: 16), 54 | _translator.buildAsyncProperty( 55 | selector: () => _translator.translated, 56 | builder: (context, translated) { 57 | if (translated?.isNotEmpty == true) { 58 | return Text(translated ?? '', style: TextStyle(fontSize: 30)); 59 | } else { 60 | return Text('Please type more than 3 characters...'); 61 | } 62 | }, 63 | ), 64 | ], 65 | ), 66 | ), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /example/lib/updating_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/async_controller.dart'; 2 | import 'package:example/helpers.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class Settings { 6 | String login = ''; 7 | String password = ''; 8 | double limit = 0; 9 | } 10 | 11 | class UpdatingExampleController extends UpdatingController { 12 | UpdatingExampleController() : super(Settings()..login = 'x'); 13 | 14 | @override 15 | Future update(AsyncFetchItem item, Set keys) async { 16 | await Future.delayed(Duration(seconds: 1)); 17 | return Future.value(); 18 | } 19 | 20 | UpdatingProperty get login => 21 | bind('login', (x) => x.login, (x, v) => x.login = v); 22 | UpdatingProperty get password => 23 | bind('password', (x) => x.password, (x, v) => x.password = v); 24 | UpdatingProperty get limit => 25 | bind('limit', (x) => x.limit, (x, v) => x.limit = v); 26 | } 27 | 28 | class UpdatingExample extends StatefulWidget with ExamplePage { 29 | @override 30 | String get title => 'Updating'; 31 | 32 | @override 33 | _UpdatingExampleState createState() => _UpdatingExampleState(); 34 | } 35 | 36 | class _UpdatingExampleState extends State { 37 | final _c = UpdatingExampleController(); 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return Scaffold( 42 | appBar: widget.buildAppBar(), 43 | body: ListView( 44 | children: [ 45 | buildTextField(property: _c.login), 46 | buildTextField(property: _c.password), 47 | Row( 48 | children: [ 49 | Expanded( 50 | child: AsyncPropertyBuilder( 51 | listenable: _c.limit, 52 | selector: _c.limit.read, 53 | builder: (context, v, __) { 54 | return Slider( 55 | value: _c.limit.value, 56 | onChanged: _c.limit.update, 57 | ); 58 | }, 59 | ), 60 | ), 61 | SyncSuffix(property: _c.limit), 62 | SizedBox(width: 24), 63 | ], 64 | ), 65 | ], 66 | ), 67 | ); 68 | } 69 | 70 | Widget buildTextField({required UpdatingProperty property}) { 71 | return Padding( 72 | padding: const EdgeInsets.symmetric(horizontal: 24.0), 73 | child: TextFormField( 74 | autovalidateMode: AutovalidateMode.always, 75 | initialValue: property.value, 76 | onChanged: property.update, 77 | decoration: InputDecoration( 78 | suffixIcon: SyncSuffix(property: property), 79 | ), 80 | ), 81 | ); 82 | } 83 | } 84 | 85 | /// Displays icon when property is syncing 86 | class SyncSuffix extends StatelessWidget { 87 | final UpdatingProperty property; 88 | 89 | const SyncSuffix({Key? key, required this.property}) : super(key: key); 90 | 91 | @override 92 | Widget build(BuildContext context) { 93 | return SizedBox( 94 | width: 48, 95 | height: 48, 96 | child: UpdatingPropertyListener( 97 | property: property, 98 | decorator: (context, status, info) { 99 | switch (status) { 100 | case UpdatingPropertyStatus.ok: 101 | return Icon(null); 102 | case UpdatingPropertyStatus.error: 103 | return IconButton( 104 | icon: Icon(Icons.sync_problem), 105 | onPressed: property.recoverFromError, 106 | ); 107 | case UpdatingPropertyStatus.needsUpdate: 108 | return Opacity( 109 | opacity: 0.5, 110 | child: Icon(Icons.sync), 111 | ); 112 | 113 | case UpdatingPropertyStatus.isUpdating: 114 | return Icon(Icons.sync); 115 | } 116 | }, 117 | ), 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.5.0" 11 | async_controller: 12 | dependency: "direct main" 13 | description: 14 | path: ".." 15 | relative: true 16 | source: path 17 | version: "1.0.2" 18 | boolean_selector: 19 | dependency: transitive 20 | description: 21 | name: boolean_selector 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.1.0" 25 | characters: 26 | dependency: transitive 27 | description: 28 | name: characters 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.1.0" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.2.0" 39 | clock: 40 | dependency: transitive 41 | description: 42 | name: clock 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.1.0" 46 | collection: 47 | dependency: transitive 48 | description: 49 | name: collection 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.15.0" 53 | connectivity: 54 | dependency: transitive 55 | description: 56 | name: connectivity 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "3.0.3" 60 | connectivity_for_web: 61 | dependency: transitive 62 | description: 63 | name: connectivity_for_web 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "0.4.0" 67 | connectivity_macos: 68 | dependency: transitive 69 | description: 70 | name: connectivity_macos 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "0.2.0" 74 | connectivity_platform_interface: 75 | dependency: transitive 76 | description: 77 | name: connectivity_platform_interface 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "2.0.1" 81 | crypto: 82 | dependency: transitive 83 | description: 84 | name: crypto 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "3.0.0" 88 | fake_async: 89 | dependency: transitive 90 | description: 91 | name: fake_async 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "1.2.0" 95 | faker: 96 | dependency: "direct main" 97 | description: 98 | name: faker 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "2.0.0-rc.2" 102 | flutter: 103 | dependency: "direct main" 104 | description: flutter 105 | source: sdk 106 | version: "0.0.0" 107 | flutter_hooks: 108 | dependency: "direct main" 109 | description: 110 | name: flutter_hooks 111 | url: "https://pub.dartlang.org" 112 | source: hosted 113 | version: "0.16.0" 114 | flutter_test: 115 | dependency: "direct dev" 116 | description: flutter 117 | source: sdk 118 | version: "0.0.0" 119 | flutter_web_plugins: 120 | dependency: transitive 121 | description: flutter 122 | source: sdk 123 | version: "0.0.0" 124 | http: 125 | dependency: transitive 126 | description: 127 | name: http 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "0.13.1" 131 | http_parser: 132 | dependency: transitive 133 | description: 134 | name: http_parser 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "4.0.0" 138 | intl: 139 | dependency: "direct main" 140 | description: 141 | name: intl 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "0.17.0" 145 | js: 146 | dependency: transitive 147 | description: 148 | name: js 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "0.6.3" 152 | lint: 153 | dependency: "direct dev" 154 | description: 155 | name: lint 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "1.5.3" 159 | matcher: 160 | dependency: transitive 161 | description: 162 | name: matcher 163 | url: "https://pub.dartlang.org" 164 | source: hosted 165 | version: "0.12.10" 166 | meta: 167 | dependency: transitive 168 | description: 169 | name: meta 170 | url: "https://pub.dartlang.org" 171 | source: hosted 172 | version: "1.3.0" 173 | nested: 174 | dependency: transitive 175 | description: 176 | name: nested 177 | url: "https://pub.dartlang.org" 178 | source: hosted 179 | version: "1.0.0" 180 | path: 181 | dependency: transitive 182 | description: 183 | name: path 184 | url: "https://pub.dartlang.org" 185 | source: hosted 186 | version: "1.8.0" 187 | pedantic: 188 | dependency: transitive 189 | description: 190 | name: pedantic 191 | url: "https://pub.dartlang.org" 192 | source: hosted 193 | version: "1.11.0" 194 | plugin_platform_interface: 195 | dependency: transitive 196 | description: 197 | name: plugin_platform_interface 198 | url: "https://pub.dartlang.org" 199 | source: hosted 200 | version: "2.0.0" 201 | provider: 202 | dependency: "direct main" 203 | description: 204 | name: provider 205 | url: "https://pub.dartlang.org" 206 | source: hosted 207 | version: "5.0.0" 208 | sky_engine: 209 | dependency: transitive 210 | description: flutter 211 | source: sdk 212 | version: "0.0.99" 213 | source_span: 214 | dependency: transitive 215 | description: 216 | name: source_span 217 | url: "https://pub.dartlang.org" 218 | source: hosted 219 | version: "1.8.0" 220 | stack_trace: 221 | dependency: transitive 222 | description: 223 | name: stack_trace 224 | url: "https://pub.dartlang.org" 225 | source: hosted 226 | version: "1.10.0" 227 | stream_channel: 228 | dependency: transitive 229 | description: 230 | name: stream_channel 231 | url: "https://pub.dartlang.org" 232 | source: hosted 233 | version: "2.1.0" 234 | string_scanner: 235 | dependency: transitive 236 | description: 237 | name: string_scanner 238 | url: "https://pub.dartlang.org" 239 | source: hosted 240 | version: "1.1.0" 241 | term_glyph: 242 | dependency: transitive 243 | description: 244 | name: term_glyph 245 | url: "https://pub.dartlang.org" 246 | source: hosted 247 | version: "1.2.0" 248 | test_api: 249 | dependency: transitive 250 | description: 251 | name: test_api 252 | url: "https://pub.dartlang.org" 253 | source: hosted 254 | version: "0.2.19" 255 | translator: 256 | dependency: "direct main" 257 | description: 258 | name: translator 259 | url: "https://pub.dartlang.org" 260 | source: hosted 261 | version: "0.1.6+1" 262 | typed_data: 263 | dependency: transitive 264 | description: 265 | name: typed_data 266 | url: "https://pub.dartlang.org" 267 | source: hosted 268 | version: "1.3.0" 269 | vector_math: 270 | dependency: transitive 271 | description: 272 | name: vector_math 273 | url: "https://pub.dartlang.org" 274 | source: hosted 275 | version: "2.1.0" 276 | sdks: 277 | dart: ">=2.12.0 <3.0.0" 278 | flutter: ">=1.20.0" 279 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: Example for async_builder. 3 | publish_to: none 4 | 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: '>=2.12.0 <3.0.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | async_controller: 14 | path: ../ 15 | intl: ^0.17.0 16 | faker: ^2.0.0-rc 17 | translator: 18 | provider: ^5.0.0 19 | flutter_hooks: 20 | 21 | 22 | dev_dependencies: 23 | flutter_test: 24 | sdk: flutter 25 | lint: any 26 | 27 | flutter: 28 | uses-material-design: true -------------------------------------------------------------------------------- /lib/async_controller.dart: -------------------------------------------------------------------------------- 1 | library async_controller; 2 | 3 | export 'src/async_button.dart'; 4 | export 'src/async_data.dart'; 5 | export 'src/async_theme.dart'; 6 | export 'src/controller.dart'; 7 | export 'src/controller_ext.dart'; 8 | export 'src/paged.dart'; 9 | export 'src/refreshers.dart'; 10 | export 'src/updating.dart'; 11 | export 'src/utils.dart'; 12 | -------------------------------------------------------------------------------- /lib/src/async_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/src/async_theme.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | typedef AsyncButtonFunction = Future Function(); 5 | typedef AsyncButtonBuilder = Widget Function(AsyncButtonSettings settings); 6 | 7 | class AsyncButtonSettings { 8 | AsyncButtonSettings(this.context, this.child, this.onPressed); 9 | 10 | final BuildContext context; 11 | final Opacity child; 12 | final VoidCallback? onPressed; 13 | 14 | Color? loadingColor; 15 | } 16 | 17 | class AsyncButton extends StatefulWidget { 18 | static Widget _defaultBuilder(AsyncButtonSettings settings) { 19 | return TextButton(onPressed: settings.onPressed, child: settings.child); 20 | } 21 | 22 | const AsyncButton({ 23 | Key? key, 24 | required this.onPressed, 25 | required this.child, 26 | this.builder = _defaultBuilder, 27 | this.loadingColor, 28 | this.lockInterface = true, 29 | }) : super(key: key); 30 | 31 | final AsyncButtonFunction? onPressed; 32 | final Widget child; 33 | final AsyncButtonBuilder builder; 34 | final Color? loadingColor; 35 | 36 | /// Should the UI be completely locked when operation is pending? Default: true. 37 | /// Note: if false, this button will still support only one execution at a time 38 | /// This flag is more useful to prevent user from pressing on multiple different commands. 39 | final bool lockInterface; 40 | 41 | @override 42 | AsyncButtonState createState() => AsyncButtonState(); 43 | 44 | void showError(Object error, BuildContext context) { 45 | final theme = DefaultAsyncDataDecoration.of(context); 46 | 47 | theme.showError(context, error); 48 | } 49 | 50 | Widget buildLoadingIndicator(Color? loadingColor) { 51 | Animation? valueColor; 52 | 53 | final loadingColorMerged = loadingColor ?? this.loadingColor; 54 | 55 | if (loadingColorMerged != null) { 56 | valueColor = AlwaysStoppedAnimation(loadingColorMerged); 57 | } 58 | 59 | return Padding( 60 | padding: const EdgeInsets.all(12.0), 61 | child: AspectRatio( 62 | aspectRatio: 1, 63 | child: CircularProgressIndicator(valueColor: valueColor), 64 | ), 65 | ); 66 | } 67 | 68 | Widget overlayContent(BuildContext context) { 69 | return Container( 70 | color: Colors.transparent, 71 | ); 72 | } 73 | 74 | Widget build(AsyncButtonState state) { 75 | final settings = AsyncButtonSettings( 76 | state.context, 77 | Opacity( 78 | opacity: state.isLoading ? 0.5 : 1.0, 79 | child: child, 80 | ), 81 | state.onPressed, 82 | ); 83 | return Stack( 84 | fit: StackFit.passthrough, 85 | children: [ 86 | builder(settings), 87 | Visibility( 88 | visible: state.isLoading, 89 | child: Positioned.fill( 90 | child: Center(child: buildLoadingIndicator(settings.loadingColor)), 91 | ), 92 | ) 93 | ], 94 | ); 95 | } 96 | 97 | double get childOpacityWhenLoading => 0.5; 98 | } 99 | 100 | class AsyncButtonState extends State { 101 | bool _isLoading = false; 102 | bool get isLoading => _isLoading; 103 | 104 | void _update(bool isLoading) { 105 | setState(() { 106 | _isLoading = isLoading; 107 | }); 108 | } 109 | 110 | VoidCallback? get onPressed { 111 | if (widget.onPressed != null) { 112 | return execute; 113 | } else { 114 | return null; 115 | } 116 | } 117 | 118 | Future execute() async { 119 | if (isLoading) { 120 | return; 121 | } 122 | 123 | OverlayEntry? _entry; 124 | 125 | if (widget.lockInterface) { 126 | final overlay = Overlay.of(context)!; 127 | _entry = OverlayEntry(builder: widget.overlayContent); 128 | overlay.insert(_entry); 129 | } 130 | 131 | try { 132 | _update(true); 133 | 134 | await widget.onPressed?.call(); 135 | } catch (e) { 136 | if (mounted) { 137 | widget.showError(e, context); 138 | } 139 | } finally { 140 | _entry?.remove(); 141 | if (mounted) { 142 | _update(false); 143 | } 144 | } 145 | } 146 | 147 | @override 148 | Widget build(BuildContext context) { 149 | return widget.build(this); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lib/src/async_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | 5 | import 'controller.dart'; 6 | 7 | typedef AsyncDataFunction = Widget Function(BuildContext context, T data); 8 | 9 | /// A widget that let's the user specify builder for asynchronously loaded data. 10 | /// Error handling, empty state and loading are handled by the widget. 11 | /// The automatic handling is customizable with AsyncDataDecoration. 12 | class AsyncData extends StatefulWidget { 13 | /// Creates AsyncData with already existing controller. 14 | const AsyncData({ 15 | Key? key, 16 | required this.controller, 17 | required this.builder, 18 | this.decorator = const AsyncDataDecoration(), 19 | }) : super(key: key); 20 | 21 | final AsyncController controller; 22 | 23 | /// This builder runs only when data is available. 24 | final AsyncDataFunction builder; 25 | 26 | /// Provides widgets for AsyncData when there is no data to show. 27 | final AsyncDataDecoration decorator; 28 | 29 | @override 30 | _AsyncDataState createState() => _AsyncDataState(); 31 | 32 | static _AsyncDataState? of(BuildContext context) { 33 | final result = context.findAncestorStateOfType<_AsyncDataState>(); 34 | if (result == null && context is StatefulElement) { 35 | final state = context.state; 36 | if (state is _AsyncDataState) { 37 | return state; 38 | } 39 | } 40 | 41 | return result; 42 | } 43 | } 44 | 45 | class _AsyncDataState extends State> { 46 | int _version = 0; 47 | 48 | AsyncController get controller => widget.controller; 49 | 50 | @override 51 | void initState() { 52 | widget.controller.addListener(_handleChange); 53 | super.initState(); 54 | } 55 | 56 | @override 57 | void didUpdateWidget(AsyncData oldWidget) { 58 | super.didUpdateWidget(oldWidget); 59 | if (oldWidget.controller != widget.controller) { 60 | oldWidget.controller.removeListener(_handleChange); 61 | widget.controller.addListener(_handleChange); 62 | } 63 | } 64 | 65 | @override 66 | void dispose() { 67 | widget.controller.removeListener(_handleChange); 68 | super.dispose(); 69 | } 70 | 71 | void _handleChange() { 72 | if (_version > 0 && _version == widget.controller.version) { 73 | // skip unnecessary rebuilds after data is available 74 | return; 75 | } 76 | 77 | setState(() {}); 78 | } 79 | 80 | @override 81 | Widget build(BuildContext context) { 82 | _version = widget.controller.version; 83 | 84 | Widget buildContent() { 85 | switch (widget.controller.state) { 86 | case AsyncControllerState.hasData: 87 | final data = widget.controller.value; 88 | assert(data != null); 89 | if (data == null) { 90 | return SizedBox(); 91 | } 92 | return widget.builder(context, data); 93 | case AsyncControllerState.failed: 94 | return widget.decorator.buildError(context, widget.controller.error, 95 | widget.controller.performUserInitiatedRefresh); 96 | case AsyncControllerState.noDataYet: 97 | return widget.decorator.buildNoDataYet(context); 98 | case AsyncControllerState.noData: 99 | return widget.decorator.buildNoData(context); 100 | } 101 | } 102 | 103 | final child = buildContent(); 104 | return widget.decorator.decorate(child, widget); 105 | } 106 | } 107 | 108 | /// Provides widgets for AsyncData when there is no data to show. 109 | class AsyncDataDecoration { 110 | const AsyncDataDecoration(); 111 | 112 | factory AsyncDataDecoration.customized({Widget? noData}) { 113 | return _CustomizedAsyncDataDecoration(noData); 114 | } 115 | 116 | /// Constructs widget (usually Text) to describe given error 117 | Widget buildErrorDescription(BuildContext context, dynamic error) { 118 | return Text(error.toString()); 119 | } 120 | 121 | /// There was error during fetch, we don't data to show so we may show error with try again button. 122 | Widget buildError( 123 | BuildContext context, dynamic error, VoidCallback tryAgain) { 124 | final errorWidget = buildErrorDescription(context, error); 125 | 126 | return Padding( 127 | padding: const EdgeInsets.all(8.0), 128 | child: Center( 129 | child: Column( 130 | mainAxisSize: MainAxisSize.min, 131 | children: [ 132 | errorWidget, 133 | IconButton(icon: Icon(Icons.refresh), onPressed: tryAgain), 134 | ], 135 | ), 136 | ), 137 | ); 138 | } 139 | 140 | /// Shows error after AsyncButton failed. 141 | /// By default, a snackbar. 142 | void showError(BuildContext context, Object error) { 143 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( 144 | content: buildErrorDescription(context, error), 145 | )); 146 | } 147 | 148 | /// There is no data because it was not loaded yet. 149 | Widget buildNoDataYet(BuildContext context) { 150 | return const Center( 151 | child: CircularProgressIndicator(), 152 | ); 153 | } 154 | 155 | /// There is not data because fetch returned null. 156 | Widget buildNoData(BuildContext context) { 157 | return buildNoDataYet(context); 158 | } 159 | 160 | /// Always runs, gives possiblity to add the same widget for each state. 161 | Widget decorate(Widget child, AsyncData builder) { 162 | return child; 163 | } 164 | } 165 | 166 | class _CustomizedAsyncDataDecoration extends AsyncDataDecoration { 167 | _CustomizedAsyncDataDecoration(this.customNoData); 168 | final Widget? customNoData; 169 | 170 | @override 171 | Widget buildNoData(BuildContext context) { 172 | return customNoData ?? SizedBox(); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/src/async_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | import '../async_controller.dart'; 4 | import 'async_data.dart'; 5 | 6 | class DefaultAsyncDataDecoration extends StatelessWidget { 7 | final AsyncDataDecoration data; 8 | final Widget child; 9 | 10 | const DefaultAsyncDataDecoration({ 11 | Key? key, 12 | required this.data, 13 | required this.child, 14 | }) : super(key: key); 15 | 16 | static AsyncDataDecoration of(BuildContext context) => 17 | context 18 | .findAncestorWidgetOfExactType() 19 | ?.data ?? 20 | const AsyncDataDecoration(); 21 | 22 | @override 23 | Widget build(BuildContext context) => child; 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:async_controller/async_controller.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/widgets.dart'; 7 | 8 | import 'refreshers.dart'; 9 | 10 | /// Runs one fetch at a time. 11 | /// If performFetch is called when old fetch is running, the old fetch will be canceled 12 | class AsyncFetchCore { 13 | AsyncFetchItem? _current; 14 | 15 | bool get isRunning => _current != null; 16 | 17 | Future perform(AsyncControllerFetchExpanded fetch) async { 18 | _current?._isCancelled = true; 19 | 20 | final status = AsyncFetchItem(); 21 | _current = status; 22 | 23 | try { 24 | status._runningFuture = fetch(status); 25 | await status._runningFuture; 26 | } on AsyncFetchItemCanceled { 27 | // ignore canceled error 28 | } finally { 29 | if (_current == status) { 30 | _current = null; 31 | } 32 | } 33 | } 34 | 35 | /// Cancels current fetch and stops running 36 | void cancel() { 37 | _current?._isCancelled = true; 38 | _current = null; 39 | } 40 | 41 | /// Waits until core stops running 42 | Future waitIfNeeded() async { 43 | while (_current?._runningFuture != null) { 44 | await _current?._runningFuture; 45 | } 46 | assert(!isRunning); 47 | } 48 | } 49 | 50 | class AsyncFetchItemCanceled implements Exception { 51 | const AsyncFetchItemCanceled(); 52 | } 53 | 54 | /// Object created for every fetch to control cancellation. 55 | class AsyncFetchItem { 56 | static const cancelledError = AsyncFetchItemCanceled(); 57 | 58 | bool _isCancelled = false; 59 | bool get isCancelled => _isCancelled; 60 | 61 | /// Waits until feature is finished, and then ensures that fetch was not cancelled 62 | Future ifNotCancelled(Future future) async { 63 | final result = await future; 64 | if (isCancelled) { 65 | throw cancelledError; 66 | } 67 | 68 | return result; 69 | } 70 | 71 | Future? _runningFuture; 72 | } 73 | 74 | enum AsyncControllerState { 75 | /// Controller was just created and there is nothing to show. 76 | /// Usually loading indicator will be shown in this case. data == nil, error == nil 77 | noDataYet, 78 | 79 | /// The fetch was successful, and we have something to show. data.isNotEmpty 80 | hasData, 81 | 82 | /// The fetch was successful, but there is nothing to show. data == nil || data.isEmpty 83 | noData, 84 | 85 | /// Fetch failed. error != nil 86 | failed, 87 | } 88 | 89 | /// Simplified fetch function that does not care about cancellation. 90 | typedef AsyncControllerFetch = Future Function(); 91 | typedef AsyncControllerFetchExpanded = Future Function( 92 | AsyncFetchItem status); 93 | 94 | /// A controller for managing asynchronously loading data. 95 | abstract class AsyncController extends ChangeNotifier 96 | implements ValueListenable, Refreshable { 97 | AsyncController([T? initialValue]) { 98 | if (initialValue != null) { 99 | _value = initialValue; 100 | _version = 1; 101 | _needsRefresh = false; 102 | } 103 | } 104 | 105 | factory AsyncController.method(AsyncControllerFetch method) { 106 | return _SimpleAsyncController(method); 107 | } 108 | 109 | // prints errors in debug mode, ensures that they are not programmer's mistake 110 | static bool debugCheckErrors = true; 111 | 112 | /// _version == 0 means that there was no fetch yet 113 | int _version = 0; 114 | T? _value; 115 | Object? _error; 116 | bool _isLoading = false; 117 | 118 | final _core = AsyncFetchCore(); 119 | 120 | AsyncFetchItem? _lastFetch; 121 | bool _needsRefresh = true; 122 | 123 | /// Behaviors dictate when loading controller needs to reload. 124 | final List _behaviors = []; 125 | 126 | @override 127 | T? get value => _value; 128 | 129 | Object? get error => _error; 130 | bool get isLoading => _isLoading; 131 | 132 | /// Number of finished fetches since last reset. 133 | int get version => _version; 134 | 135 | Duration? _lastFetchDuration; 136 | Duration? get lastFetchDuration => _lastFetchDuration; 137 | 138 | AsyncControllerState get state { 139 | if (hasData) { 140 | return AsyncControllerState.hasData; 141 | } else if (error != null && !isLoading) { 142 | return AsyncControllerState.failed; 143 | } else if (version == 0) { 144 | return AsyncControllerState.noDataYet; 145 | } else { 146 | return AsyncControllerState.noData; 147 | } 148 | } 149 | 150 | @override 151 | void setNeedsRefresh( 152 | [SetNeedsRefreshFlag flags = SetNeedsRefreshFlag.always]) { 153 | if (flags.flagOnlyIfNotLoading && isLoading || 154 | flags.flagOnlyIfError && error == null) { 155 | return; 156 | } 157 | 158 | if (flags == SetNeedsRefreshFlag.reset) { 159 | reset(); 160 | return; 161 | } 162 | 163 | _core.cancel(); 164 | _needsRefresh = true; 165 | 166 | if (hasListeners) { 167 | performFetch(); 168 | } 169 | } 170 | 171 | void _cancelCurrentFetch([AsyncFetchItem? nextFetch]) { 172 | _lastFetch?._isCancelled = true; 173 | _lastFetch = nextFetch; 174 | } 175 | 176 | /// Clears all stored data. Will fetch again if controller has listeners. 177 | Future reset() { 178 | _version = 0; 179 | _cancelCurrentFetch(); 180 | _value = null; 181 | _error = null; 182 | _isLoading = false; 183 | 184 | if (hasListeners) { 185 | return performFetch(); 186 | } else { 187 | return Future.value(); 188 | } 189 | } 190 | 191 | /// Indicates if controller has data that could be displayed. 192 | bool get hasData => _value != null; 193 | 194 | @protected 195 | Future fetch(AsyncFetchItem status); 196 | 197 | /// Immediately runs default fetch or provided func. Previous fetch will be cancelled. 198 | @protected 199 | Future performFetch([AsyncControllerFetchExpanded? fetch]) { 200 | return _core.perform((status) async { 201 | _cancelCurrentFetch(status); 202 | final start = DateTime.now(); 203 | 204 | if (!_isLoading || error != null) { 205 | _isLoading = true; 206 | 207 | if (clearsErrorOnStart) { 208 | _error = null; 209 | } 210 | 211 | // microtask avoids crash that would happen when executing loadIfNeeded from build method 212 | Future.microtask(notifyListeners); 213 | } 214 | 215 | try { 216 | final value = 217 | await status.ifNotCancelled((fetch ?? this.fetch)(status)); 218 | 219 | _value = value; 220 | _version += 1; 221 | _error = null; 222 | _needsRefresh = false; 223 | } catch (e, trace) { 224 | if (e == AsyncFetchItem.cancelledError) { 225 | return; 226 | } 227 | 228 | if (kDebugMode && AsyncController.debugCheckErrors) { 229 | // this is disabled in production code and behind a flag 230 | // ignore: avoid_print 231 | print('${this} got error:\n$e $trace'); 232 | 233 | if (e is NoSuchMethodError) { 234 | rethrow; 235 | } 236 | } 237 | 238 | _error = e; 239 | } 240 | 241 | _lastFetchDuration = DateTime.now().difference(start); 242 | _isLoading = false; 243 | notifyListeners(); 244 | }); 245 | } 246 | 247 | /// Notify that currently held value changed without doing new fetch. 248 | @protected 249 | void internallyUpdateVersion() { 250 | assert(_version > 0, 251 | 'Attempted to raise version on empty controller. Something needs to be loaded.'); 252 | _version++; 253 | notifyListeners(); 254 | } 255 | 256 | /// Intended to use for pull to refresh. 257 | Future performUserInitiatedRefresh() { 258 | return performFetch(); 259 | } 260 | 261 | /// Perform fetch if there is no data yet. 262 | /// This future never fails - there is no need to catch. 263 | /// If there is error during loading it will handled by the controller. 264 | /// If multiple widgets call this method, they will get the same future. 265 | Future loadIfNeeded() { 266 | final running = _core._current?._runningFuture; 267 | if (running != null) { 268 | return running; 269 | } 270 | 271 | if (_needsRefresh) { 272 | return performFetch(); 273 | } else { 274 | return Future.value(); 275 | } 276 | } 277 | 278 | /// Adds loading refresher that will have capability to trigger a reload of controller. 279 | void addRefresher(LoadingRefresher behavior) { 280 | behavior.mount(this); 281 | _behaviors.add(behavior); 282 | 283 | if (hasListeners) { 284 | behavior.activate(); 285 | } 286 | } 287 | 288 | @protected 289 | void activate() { 290 | for (final b in _behaviors) { 291 | b.activate(); 292 | } 293 | loadIfNeeded(); 294 | } 295 | 296 | @protected 297 | void deactivate() { 298 | _cancelCurrentFetch(); 299 | for (final b in _behaviors) { 300 | b.deactivate(); 301 | } 302 | } 303 | 304 | @override 305 | void addListener(void Function() listener) { 306 | if (!hasListeners) { 307 | activate(); 308 | } 309 | super.addListener(listener); 310 | } 311 | 312 | @override 313 | void removeListener(void Function() listener) { 314 | super.removeListener(listener); 315 | if (!hasListeners) { 316 | deactivate(); 317 | } 318 | } 319 | 320 | @override 321 | void dispose() { 322 | if (hasListeners) { 323 | deactivate(); 324 | } 325 | super.dispose(); 326 | } 327 | 328 | /// If true (default), error from previous fetch will be cleared at the start of new fetch 329 | /// If false, error will remain until fetch is completed. 330 | @protected 331 | bool get clearsErrorOnStart => true; 332 | } 333 | 334 | class _SimpleAsyncController extends AsyncController { 335 | _SimpleAsyncController(this.method); 336 | 337 | final AsyncControllerFetch method; 338 | 339 | @override 340 | Future fetch(AsyncFetchItem status) => method(); 341 | } 342 | 343 | /// A controller that does additonal processing after fetching base data. 344 | /// Useful for local filtering, sorting, etc. 345 | abstract class MappedAsyncController 346 | extends AsyncController { 347 | Future fetchBase(); 348 | 349 | /// A method that runs after expensive base fetch. Call setNeedsLocalTransform if conditions affecting the transform has changed. 350 | /// For example if searchText for locally implemented search has changed. 351 | Future transform(BaseValue data); 352 | 353 | Future? _cachedBase; 354 | 355 | @override 356 | Future fetch(AsyncFetchItem status) async { 357 | _cachedBase ??= fetchBase(); 358 | 359 | try { 360 | return transform(await _cachedBase!); 361 | } catch (e) { 362 | _cachedBase = null; 363 | rethrow; 364 | } 365 | } 366 | 367 | @override 368 | Future performUserInitiatedRefresh() { 369 | _cachedBase = null; 370 | return super.performUserInitiatedRefresh(); 371 | } 372 | 373 | /// Re-run fetch on existing cached base 374 | @protected 375 | void setNeedsLocalTransform() { 376 | performFetch(); 377 | } 378 | } 379 | 380 | /// A controller that loads a list and then removes some items from it. 381 | abstract class FilteringAsyncController 382 | extends MappedAsyncController, List> { 383 | @override 384 | bool get hasData => super.hasData && value!.isNotEmpty; 385 | } 386 | 387 | /// Provides the latest value from a stream. 388 | /// Automatically closes / recreates the stream when activated / deactivated. 389 | abstract class StreamAsyncControlelr extends AsyncController { 390 | Stream getStream(AsyncFetchItem status); 391 | 392 | StreamSubscription? _sub; 393 | 394 | Duration? get renewAfter => null; 395 | 396 | @override 397 | void dispose() { 398 | _sub?.cancel(); 399 | super.dispose(); 400 | } 401 | 402 | void _onData(T data) { 403 | performFetch((_) => Future.value(data)); 404 | } 405 | 406 | void _onError(Object error, stack) { 407 | performFetch((_) => throw error); 408 | } 409 | 410 | void _onDone() { 411 | final duration = renewAfter; 412 | if (renewAfter != null) { 413 | performFetch((item) async { 414 | await item.ifNotCancelled(Future.delayed(duration!)); 415 | return fetch(item); 416 | }); 417 | } 418 | } 419 | 420 | @override 421 | void deactivate() { 422 | _sub?.cancel(); 423 | _sub = null; 424 | super.deactivate(); 425 | } 426 | 427 | @override 428 | void activate() { 429 | super.activate(); 430 | if (_sub == null) { 431 | performFetch(); 432 | } 433 | } 434 | 435 | @override 436 | Future fetch(AsyncFetchItem status) { 437 | final stream = getStream(status); 438 | _sub?.cancel(); 439 | _sub = stream.listen(_onData, onError: _onError, onDone: _onDone); 440 | 441 | return Future.value(value); 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /lib/src/controller_ext.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | import 'async_data.dart'; 4 | import 'controller.dart'; 5 | import 'utils.dart'; 6 | 7 | extension AsyncControllerExt on AsyncController { 8 | /// Provides AsyncSnapshot for compability with other widgets. 9 | /// It is usually better to use state property. 10 | AsyncSnapshot get snapshot { 11 | ConnectionState connection; 12 | 13 | if (isLoading) { 14 | connection = ConnectionState.waiting; 15 | } else { 16 | connection = ConnectionState.done; 17 | } 18 | 19 | final value = this.value; 20 | 21 | if (version > 0 && value != null) { 22 | return AsyncSnapshot.withData(connection, value); 23 | } else if (error != null) { 24 | return AsyncSnapshot.withError(connection, error!); 25 | } else { 26 | return AsyncSnapshot.waiting(); 27 | } 28 | } 29 | 30 | AsyncData buildAsyncData({ 31 | required AsyncDataFunction builder, 32 | AsyncDataDecoration decorator = const AsyncDataDecoration(), 33 | }) { 34 | return AsyncData( 35 | controller: this, 36 | decorator: decorator, 37 | builder: builder, 38 | ); 39 | } 40 | } 41 | 42 | extension ListenableExt on Listenable { 43 | /// Returns reactive widget that builds when value returned from selector is different than before. 44 | /// The selector runs only when this controller changes. 45 | Widget buildAsyncProperty

({ 46 | required P Function() selector, 47 | required Widget Function(BuildContext, P) builder, 48 | }) { 49 | return AsyncPropertyBuilder

( 50 | selector: selector, 51 | listenable: this, 52 | builder: (context, value, child) => builder(context, value), 53 | ); 54 | } 55 | 56 | Widget buildAsyncVisibility( 57 | {required bool Function() selector, required Widget child}) { 58 | return AsyncPropertyBuilder( 59 | selector: selector, 60 | listenable: this, 61 | builder: (_, visible, __) { 62 | return Visibility( 63 | visible: visible, 64 | child: child, 65 | ); 66 | }, 67 | ); 68 | } 69 | 70 | Widget buildAsyncOpacity({ 71 | required bool Function() selector, 72 | required Widget child, 73 | double opacityForTrue = 1.0, 74 | double opacityForFalse = 0.5, 75 | }) { 76 | return AsyncPropertyBuilder( 77 | selector: selector, 78 | listenable: this, 79 | builder: (_, value, __) { 80 | return Opacity( 81 | opacity: value ? opacityForTrue : opacityForFalse, 82 | child: child, 83 | ); 84 | }, 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/debugging.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | bool internalDebugLogEnabled = false; 4 | 5 | void debugLog(obj) { 6 | if (kDebugMode && internalDebugLogEnabled) { 7 | // ignore: avoid_print 8 | print(obj); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/paged.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/src/debugging.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | import '../async_controller.dart'; 6 | import 'async_data.dart'; 7 | import 'controller.dart'; 8 | import 'controller_ext.dart'; 9 | 10 | /// A slice of bigger array, returned from backend. All values must not be null. 11 | class PagedData { 12 | PagedData(this.pageIndex, this.totalCount, this.data); 13 | 14 | final int pageIndex; 15 | final int totalCount; 16 | final List data; 17 | } 18 | 19 | /// Loads data into array, in pages. 20 | /// The value of loader is totalCount of items available. The actual items can be fetched using getItem method. 21 | abstract class PagedAsyncController extends AsyncController { 22 | final List _items = []; 23 | 24 | PagedAsyncController({this.loadingMargin = 10}); 25 | 26 | /// Amount of extra items to load that are not yet visible. 27 | final int loadingMargin; 28 | 29 | int _nextPage = 0; 30 | 31 | /// Widget uses this property to determine curently visible amount of items. 32 | int get loadedItemsCount => _items.length; 33 | 34 | int? get totalCount => value; 35 | 36 | /// If data for given item is not loaded, getItem will return null and schedule a page load. 37 | /// After page is loaded, notifyListeners will be called, which will trigger listener widget to reload. 38 | T? getItem(int itemIndex) { 39 | if (itemIndex < _items.length) { 40 | if (itemIndex > (_items.length - loadingMargin)) { 41 | loadMoreIfPossible(); 42 | } 43 | 44 | return _items[itemIndex]; 45 | } 46 | 47 | loadMoreIfPossible(); 48 | return null; 49 | } 50 | 51 | Future> fetchPage(int pageIndex); 52 | 53 | bool get hasMoreToLoad { 54 | final totalCount = this.totalCount; 55 | return totalCount == null || loadedItemsCount < totalCount; 56 | } 57 | 58 | /// Fetches more data, if there is anything to fetch and controller is not already loading it. 59 | void loadMoreIfPossible() { 60 | if (hasMoreToLoad && !isLoading) { 61 | performFetch(_fetchNext); 62 | } 63 | } 64 | 65 | @override 66 | Future fetch(AsyncFetchItem status) async { 67 | debugLog('Fetching page 0'); 68 | final run = await status.ifNotCancelled(fetchPage(0)); 69 | _items.clear(); 70 | _items.addAll(run.data); 71 | 72 | _nextPage = 1; 73 | return run.totalCount; 74 | } 75 | 76 | Future _fetchNext(AsyncFetchItem status) async { 77 | final nextPage = _nextPage; 78 | debugLog('Fetching page $nextPage'); 79 | 80 | final run = await status.ifNotCancelled(fetchPage(nextPage)); 81 | _items.addAll(run.data); 82 | 83 | _nextPage = nextPage + 1; 84 | return run.totalCount; 85 | } 86 | 87 | @override 88 | Future reset() { 89 | _nextPage = 0; 90 | _items.clear(); 91 | return super.reset(); 92 | } 93 | 94 | @override 95 | bool get hasData => value != 0 && version > 0; 96 | } 97 | 98 | typedef PagedItemBuilder = Widget Function( 99 | BuildContext context, int i, T item); 100 | 101 | typedef CollectionViewBuilder = Widget Function( 102 | BuildContext, int itemCount, IndexedWidgetBuilder); 103 | 104 | class PagedListView extends StatelessWidget { 105 | final PagedListDecoration decoration; 106 | final PagedAsyncController controller; 107 | final PagedItemBuilder itemBuilder; 108 | 109 | /// Builder that will create ListView or something else for given parameters. 110 | /// Use cases: 111 | /// - custom scroll physics 112 | /// - custom scroll controller 113 | /// - using grid view instead of list view 114 | final CollectionViewBuilder listBuilder; 115 | 116 | /// Default listBuilder. Returns ListView with default parameters 117 | static Widget buildSimpleList( 118 | BuildContext context, int itemCount, IndexedWidgetBuilder itemBuilder) { 119 | return ListView.builder(itemBuilder: itemBuilder, itemCount: itemCount); 120 | } 121 | 122 | const PagedListView({ 123 | Key? key, 124 | required this.controller, 125 | required this.itemBuilder, 126 | this.listBuilder = buildSimpleList, 127 | this.decoration = PagedListDecoration.empty, 128 | }) : super(key: key); 129 | 130 | @override 131 | Widget build(BuildContext context) { 132 | return controller.buildAsyncData( 133 | decorator: decoration, 134 | builder: (context, __) { 135 | return listBuilder(context, getItemCount(), (context, i) { 136 | final item = controller.getItem(i); 137 | if (item != null) { 138 | return itemBuilder(context, i, item); 139 | } else { 140 | return buildMissingTile(context, i); 141 | } 142 | }); 143 | }, 144 | ); 145 | } 146 | 147 | @protected 148 | int getItemCount() { 149 | final totalCount = controller.totalCount; 150 | if (totalCount != null && controller.loadedItemsCount < totalCount) { 151 | return controller.loadedItemsCount + 1; 152 | } else { 153 | return controller.totalCount ?? 0; 154 | } 155 | } 156 | 157 | @protected 158 | Widget buildMissingTile(BuildContext context, int i) { 159 | return controller.buildAsyncProperty( 160 | selector: () => controller.error, 161 | builder: (_, error) { 162 | if (error != null) { 163 | return decoration.buildErrorTile( 164 | context, 165 | error, 166 | () => controller.loadMoreIfPossible(), 167 | i, 168 | ); 169 | } else { 170 | return decoration.buildNoDataYetTile(context, i); 171 | } 172 | }, 173 | ); 174 | } 175 | } 176 | 177 | /// Adds custom handling for empty content. Can be reused across the app. 178 | /// Automatically adds RefreshIndicator. Set addRefreshIndicator to false to disable it. 179 | class PagedListDecoration extends AsyncDataDecoration { 180 | const PagedListDecoration({ 181 | this.noDataContent = const SizedBox(), 182 | this.addRefreshIndicator = true, 183 | }); 184 | 185 | // Widget to display when there is zero items. 186 | final Widget noDataContent; 187 | 188 | /// Whether refresh indicator should be inserted. It will call controller.performUserInitiatedRefresh method. 189 | final bool addRefreshIndicator; 190 | 191 | static const empty = PagedListDecoration(); 192 | 193 | @override 194 | Widget buildNoData(BuildContext context) { 195 | return LayoutBuilder( 196 | builder: (context, constraints) { 197 | return SingleChildScrollView( 198 | child: Container( 199 | alignment: Alignment.center, 200 | width: constraints.maxWidth, 201 | 202 | /// Ensures that pull to refresh is possible 203 | height: constraints.maxHeight + 0.1, 204 | child: noDataContent, 205 | ), 206 | ); 207 | }, 208 | ); 209 | } 210 | 211 | @override 212 | Widget decorate(Widget child, AsyncData builder) { 213 | if (addRefreshIndicator) { 214 | return RefreshIndicator( 215 | onRefresh: builder.controller.performUserInitiatedRefresh, 216 | child: child, 217 | ); 218 | } else { 219 | return child; 220 | } 221 | } 222 | 223 | /// Called to build tile when incremental loading failed. 224 | /// By default returns the same content as buildError 225 | Widget buildErrorTile( 226 | BuildContext context, Object error, Function() tryAgain, int index) { 227 | return buildError(context, error, tryAgain); 228 | } 229 | 230 | /// Called to build tile when data is still loading. 231 | /// By default returns the same content as buildNoDataYet. 232 | Widget buildNoDataYetTile(BuildContext context, int index) { 233 | return buildNoDataYet(context); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /lib/src/refreshers.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:connectivity/connectivity.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | 6 | class SetNeedsRefreshFlag { 7 | final bool flagOnlyIfError; 8 | final bool flagReset; 9 | final bool flagOnlyIfNotLoading; 10 | 11 | static const always = SetNeedsRefreshFlag(); 12 | static const ifError = SetNeedsRefreshFlag(flagOnlyIfError: true); 13 | static const reset = SetNeedsRefreshFlag(flagReset: true); 14 | static const ifNotLoading = SetNeedsRefreshFlag(flagOnlyIfNotLoading: true); 15 | 16 | const SetNeedsRefreshFlag({ 17 | this.flagOnlyIfError = false, 18 | this.flagReset = false, 19 | this.flagOnlyIfNotLoading = false, 20 | }); 21 | } 22 | 23 | abstract class Refreshable { 24 | void setNeedsRefresh(SetNeedsRefreshFlag flag); 25 | } 26 | 27 | abstract class LoadingRefresher { 28 | /// Flag that will be used to refresh the controller. 29 | SetNeedsRefreshFlag flag = SetNeedsRefreshFlag.always; 30 | Refreshable? _controller; 31 | 32 | /// Performs setNeedsRefresh on controller with stored flag 33 | @protected 34 | void setNeedsRefresh() { 35 | _controller?.setNeedsRefresh(flag); 36 | } 37 | 38 | //ignore: use_setters_to_change_properties 39 | void mount(Refreshable controller) { 40 | _controller = controller; 41 | } 42 | 43 | void activate(); 44 | void deactivate(); 45 | } 46 | 47 | class InForegroundRefresher extends LoadingRefresher 48 | with WidgetsBindingObserver { 49 | @override 50 | void activate() { 51 | WidgetsBinding.instance!.addObserver(this); 52 | } 53 | 54 | @override 55 | void didChangeAppLifecycleState(AppLifecycleState state) { 56 | if (state == AppLifecycleState.resumed) { 57 | setNeedsRefresh(); 58 | } 59 | } 60 | 61 | @override 62 | void deactivate() { 63 | WidgetsBinding.instance!.removeObserver(this); 64 | } 65 | } 66 | 67 | class StreamRefresher extends LoadingRefresher { 68 | StreamRefresher(this.stream); 69 | 70 | final Stream stream; 71 | late StreamSubscription _sub; 72 | 73 | @override 74 | void activate() { 75 | _sub = stream.listen(onData); 76 | } 77 | 78 | @protected 79 | void onData(T data) { 80 | setNeedsRefresh(); 81 | } 82 | 83 | @override 84 | void deactivate() { 85 | _sub.cancel(); 86 | } 87 | } 88 | 89 | class OnReconnectedRefresher extends StreamRefresher { 90 | OnReconnectedRefresher() : super(Connectivity().onConnectivityChanged) { 91 | flag = SetNeedsRefreshFlag.ifError; 92 | } 93 | 94 | bool _wasConnected = false; 95 | 96 | @override 97 | void onData(ConnectivityResult data) { 98 | final isConnected = data != ConnectivityResult.none; 99 | if (isConnected != _wasConnected) { 100 | _wasConnected = isConnected; 101 | setNeedsRefresh(); 102 | } 103 | } 104 | } 105 | 106 | /// Updates loading controller every given period. 107 | class PeriodicRefresher extends LoadingRefresher { 108 | PeriodicRefresher(this.period); 109 | 110 | final Duration period; 111 | 112 | Timer? _timer; 113 | 114 | void onTick(Timer timer) { 115 | setNeedsRefresh(); 116 | } 117 | 118 | @override 119 | void activate() { 120 | _timer = Timer.periodic(period, onTick); 121 | } 122 | 123 | @override 124 | void deactivate() { 125 | _timer?.cancel(); 126 | } 127 | } 128 | 129 | class ListeningRefresher extends LoadingRefresher { 130 | ListeningRefresher(this.listenable); 131 | 132 | final Listenable listenable; 133 | 134 | @override 135 | void activate() { 136 | listenable.addListener(onChange); 137 | } 138 | 139 | @override 140 | void deactivate() { 141 | listenable.removeListener(onChange); 142 | } 143 | 144 | void onChange() { 145 | setNeedsRefresh(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/src/updating.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/async_controller.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | abstract class UpdatingController

extends ChangeNotifier { 6 | UpdatingController(this._data); 7 | 8 | P _data; 9 | 10 | late final UpdatingProperty

_dataProperty = 11 | bind('data', (x) => x, (x, v) => _data = v); 12 | UpdatingProperty

get data { 13 | return _dataProperty; 14 | } 15 | 16 | final Set _toUpdate = {}; 17 | final _core = AsyncFetchCore(); 18 | 19 | int _isUpdatingCounter = 0; 20 | bool get isUpdating => _isUpdatingCounter > 0; 21 | 22 | Object? _error; 23 | Object? get error => _error; 24 | 25 | /// Delay between update and actually sending the requests 26 | Duration get delay => Duration(seconds: 1); 27 | 28 | bool needsUpdate(String key) => _toUpdate.contains(key); 29 | 30 | /// Replaces contained data without triggering an update 31 | @protected 32 | void replaceData(P data) { 33 | _data = data; 34 | notifyListeners(); 35 | } 36 | 37 | Future ensureUpdatedOrThrow() async { 38 | await _core.waitIfNeeded(); 39 | if (hasUpdates) { 40 | await performUpdate(); 41 | } 42 | 43 | if (error != null) { 44 | throw error!; 45 | } 46 | assert(!hasUpdates); 47 | } 48 | 49 | @protected 50 | Future setNeedsUpdate(String key) { 51 | _toUpdate.add(key); 52 | notifyListeners(); 53 | return performUpdate(); 54 | } 55 | 56 | Future performUpdate() => _core.perform(_updateAndNotify); 57 | 58 | Future _updateAndNotify(AsyncFetchItem item) async { 59 | if (_error != null) { 60 | _error = null; 61 | notifyListeners(); 62 | } 63 | 64 | await item.ifNotCancelled(Future.delayed(delay)); 65 | 66 | _isUpdatingCounter++; 67 | notifyListeners(); 68 | 69 | try { 70 | await update(item, _toUpdate); 71 | 72 | if (!item.isCancelled) { 73 | _toUpdate.clear(); 74 | _error = null; 75 | } 76 | } catch (e) { 77 | if (!item.isCancelled) { 78 | _error = e; 79 | } 80 | } 81 | 82 | _isUpdatingCounter--; 83 | notifyListeners(); 84 | } 85 | 86 | bool get hasUpdates => _toUpdate.isNotEmpty; 87 | 88 | @protected 89 | Future update(AsyncFetchItem item, Set keys); 90 | 91 | UpdatingPropertyStatus statusOf(String key) { 92 | if (!needsUpdate(key)) return UpdatingPropertyStatus.ok; 93 | if (isUpdating) return UpdatingPropertyStatus.isUpdating; 94 | if (error != null) return UpdatingPropertyStatus.error; 95 | return UpdatingPropertyStatus.needsUpdate; 96 | } 97 | 98 | @protected 99 | UpdatingProperty bind( 100 | String key, T Function(P) getter, void Function(P, T) setter) { 101 | return UpdatingProperty( 102 | () => getter(_data), 103 | (newValue) { 104 | setter(_data, newValue); 105 | setNeedsUpdate(key); 106 | }, 107 | this, 108 | key, 109 | ); 110 | } 111 | 112 | @override 113 | void dispose() { 114 | assert(!hasUpdates); 115 | super.dispose(); 116 | } 117 | } 118 | 119 | enum UpdatingPropertyStatus { 120 | /// Property is synced 121 | ok, 122 | 123 | /// Sync failed 124 | error, 125 | 126 | /// Property is not synced but parent is waiting 127 | needsUpdate, 128 | 129 | /// Property is being synced right now 130 | isUpdating, 131 | } 132 | 133 | /// Represents a value that can be updated in UpdatingController. 134 | /// Setting value or calling update will register the change an schedule it for update in the controller 135 | /// If something goes wrong, update can be retried using recoverFromError method 136 | class UpdatingProperty implements Property { 137 | final UpdatingController _parent; 138 | final ValueGetter _getter; 139 | final ValueSetter _setter; 140 | 141 | final String key; 142 | 143 | UpdatingProperty(this._getter, this._setter, this._parent, this.key); 144 | 145 | UpdatingPropertyStatus get status => _parent.statusOf(key); 146 | 147 | Object? get error => _parent.error; 148 | 149 | /// Tells parent to retry updates 150 | Future recoverFromError() { 151 | assert(status == UpdatingPropertyStatus.error); 152 | return _parent.performUpdate(); 153 | } 154 | 155 | @override 156 | String toString() { 157 | return '$key: $value'; 158 | } 159 | 160 | @override 161 | void update(T newValue) { 162 | _setter(newValue); 163 | } 164 | 165 | @override 166 | T get value => _getter(); 167 | 168 | @override 169 | void addListener(listener) { 170 | _parent.addListener(listener); 171 | } 172 | 173 | @override 174 | void removeListener(listener) { 175 | _parent.removeListener(listener); 176 | } 177 | } 178 | 179 | /// Displays decoration around the child to indicate that update is happening 180 | class UpdatingPropertyListener extends StatelessWidget { 181 | final UpdatingProperty property; 182 | final Widget? child; 183 | 184 | final Widget Function( 185 | BuildContext context, 186 | UpdatingPropertyStatus status, 187 | UpdatingPropertyListener listener, 188 | ) decorator; 189 | 190 | const UpdatingPropertyListener({ 191 | required this.property, 192 | required this.decorator, 193 | this.child, 194 | }); 195 | 196 | @override 197 | Widget build(BuildContext context) { 198 | Widget builder( 199 | BuildContext context, UpdatingPropertyStatus status, Widget? _) { 200 | return decorator(context, status, this); 201 | } 202 | 203 | return AsyncPropertyBuilder( 204 | listenable: property, 205 | selector: () => property.status, 206 | builder: builder, 207 | ); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | class AsyncPropertyBuilder

extends StatefulWidget { 5 | const AsyncPropertyBuilder({ 6 | Key? key, 7 | required this.selector, 8 | required this.builder, 9 | required this.listenable, 10 | this.child, 11 | }) : super(key: key); 12 | 13 | final Listenable listenable; 14 | final P Function() selector; 15 | final ValueWidgetBuilder

builder; 16 | final Widget? child; 17 | 18 | @override 19 | _AsyncPropertyBuilderState createState() => _AsyncPropertyBuilderState

(); 20 | } 21 | 22 | class _AsyncPropertyBuilderState

extends State> { 23 | late P _current; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | widget.listenable.addListener(_handleChange); 29 | _current = widget.selector(); 30 | } 31 | 32 | @override 33 | void didUpdateWidget(AsyncPropertyBuilder

oldWidget) { 34 | super.didUpdateWidget(oldWidget); 35 | if (widget.listenable != oldWidget.listenable) { 36 | oldWidget.listenable.removeListener(_handleChange); 37 | widget.listenable.addListener(_handleChange); 38 | } 39 | } 40 | 41 | @override 42 | void dispose() { 43 | widget.listenable.removeListener(_handleChange); 44 | super.dispose(); 45 | } 46 | 47 | void _handleChange() { 48 | final now = widget.selector(); 49 | if (now != _current) { 50 | setState(() { 51 | _current = now; 52 | }); 53 | } 54 | } 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return widget.builder(context, _current, widget.child); 59 | } 60 | } 61 | 62 | /// A getter and setter associated with given listenable. 63 | abstract class Property extends ValueListenable { 64 | void update(T newValue); 65 | } 66 | 67 | extension ValueListenableRead on ValueListenable { 68 | T read() => value; 69 | } 70 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.5.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.1.0" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.2.0" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.0" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.15.0" 46 | connectivity: 47 | dependency: "direct main" 48 | description: 49 | name: connectivity 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "3.0.3" 53 | connectivity_for_web: 54 | dependency: transitive 55 | description: 56 | name: connectivity_for_web 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "0.4.0" 60 | connectivity_macos: 61 | dependency: transitive 62 | description: 63 | name: connectivity_macos 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "0.2.0" 67 | connectivity_platform_interface: 68 | dependency: transitive 69 | description: 70 | name: connectivity_platform_interface 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "2.0.1" 74 | fake_async: 75 | dependency: transitive 76 | description: 77 | name: fake_async 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "1.2.0" 81 | flutter: 82 | dependency: "direct main" 83 | description: flutter 84 | source: sdk 85 | version: "0.0.0" 86 | flutter_test: 87 | dependency: "direct dev" 88 | description: flutter 89 | source: sdk 90 | version: "0.0.0" 91 | flutter_web_plugins: 92 | dependency: transitive 93 | description: flutter 94 | source: sdk 95 | version: "0.0.0" 96 | js: 97 | dependency: transitive 98 | description: 99 | name: js 100 | url: "https://pub.dartlang.org" 101 | source: hosted 102 | version: "0.6.3" 103 | lint: 104 | dependency: "direct dev" 105 | description: 106 | name: lint 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "1.5.3" 110 | matcher: 111 | dependency: transitive 112 | description: 113 | name: matcher 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "0.12.10" 117 | meta: 118 | dependency: transitive 119 | description: 120 | name: meta 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "1.3.0" 124 | path: 125 | dependency: transitive 126 | description: 127 | name: path 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.8.0" 131 | plugin_platform_interface: 132 | dependency: transitive 133 | description: 134 | name: plugin_platform_interface 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "2.0.0" 138 | sky_engine: 139 | dependency: transitive 140 | description: flutter 141 | source: sdk 142 | version: "0.0.99" 143 | source_span: 144 | dependency: transitive 145 | description: 146 | name: source_span 147 | url: "https://pub.dartlang.org" 148 | source: hosted 149 | version: "1.8.0" 150 | stack_trace: 151 | dependency: transitive 152 | description: 153 | name: stack_trace 154 | url: "https://pub.dartlang.org" 155 | source: hosted 156 | version: "1.10.0" 157 | stream_channel: 158 | dependency: transitive 159 | description: 160 | name: stream_channel 161 | url: "https://pub.dartlang.org" 162 | source: hosted 163 | version: "2.1.0" 164 | string_scanner: 165 | dependency: transitive 166 | description: 167 | name: string_scanner 168 | url: "https://pub.dartlang.org" 169 | source: hosted 170 | version: "1.1.0" 171 | term_glyph: 172 | dependency: transitive 173 | description: 174 | name: term_glyph 175 | url: "https://pub.dartlang.org" 176 | source: hosted 177 | version: "1.2.0" 178 | test_api: 179 | dependency: transitive 180 | description: 181 | name: test_api 182 | url: "https://pub.dartlang.org" 183 | source: hosted 184 | version: "0.2.19" 185 | typed_data: 186 | dependency: transitive 187 | description: 188 | name: typed_data 189 | url: "https://pub.dartlang.org" 190 | source: hosted 191 | version: "1.3.0" 192 | vector_math: 193 | dependency: transitive 194 | description: 195 | name: vector_math 196 | url: "https://pub.dartlang.org" 197 | source: hosted 198 | version: "2.1.0" 199 | sdks: 200 | dart: ">=2.12.0 <3.0.0" 201 | flutter: ">=1.20.0" 202 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: async_controller 2 | description: A library for managing asynchronously loaded data in Flutter. 3 | version: 1.0.2 4 | homepage: https://github.com/szotp/async_controller 5 | 6 | environment: 7 | sdk: '>=2.12.0 <3.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | connectivity: ^3.0.3 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | lint: any -------------------------------------------------------------------------------- /test/controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import 'package:async_controller/async_controller.dart'; 5 | 6 | import 'utils.dart'; 7 | 8 | void main() { 9 | test('test initial conditions', () { 10 | final loader = Controller(); 11 | expect(loader.value, null); 12 | expect(loader.snapshot, 13 | const AsyncSnapshot.nothing().inState(ConnectionState.done)); 14 | }); 15 | 16 | test('test loads on listener', () { 17 | final loader = Controller(); 18 | loader.addListener(() {}); 19 | expect(loader.snapshot, 20 | const AsyncSnapshot.nothing().inState(ConnectionState.waiting)); 21 | }); 22 | 23 | test('test loads on listeners', () async { 24 | final loader = Controller(); 25 | 26 | expect(loader.isLoading, isFalse); 27 | loader.addListener(() {}); 28 | expect(loader.isLoading, isTrue); 29 | }); 30 | 31 | test('test loadIfNeeded once', () async { 32 | final loader = Controller(); 33 | final f1 = loader.loadIfNeeded(); 34 | final f2 = loader.loadIfNeeded(); 35 | final f3 = loader.loadIfNeeded(); 36 | await Future.wait([f1, f2, f3]); 37 | expect(loader.value, 1); 38 | }); 39 | 40 | test('test refresh works multiple times', () async { 41 | final loader = Controller(); 42 | final recorder = Recorder(loader); 43 | final f1 = loader.performUserInitiatedRefresh(); 44 | final f2 = loader.performUserInitiatedRefresh(); 45 | final f3 = loader.performUserInitiatedRefresh(); 46 | await Future.wait([f1, f2, f3]); 47 | expect(loader.value, 4); 48 | expect(recorder.snapshots, [ 49 | 'noDataYet : null', 50 | 'hasData : 4', 51 | ]); 52 | }); 53 | 54 | test('test refresh keeps data', () async { 55 | final loader = Controller(); 56 | final recorder = Recorder(loader); 57 | await loader.loadIfNeeded(); 58 | await loader.performUserInitiatedRefresh(); 59 | 60 | expect(recorder.snapshots, [ 61 | 'noDataYet : null', 62 | 'hasData : 1', 63 | 'hasData : 2', 64 | ]); 65 | }); 66 | 67 | test('test refresh erases error', () async { 68 | final loader = Controller(); 69 | loader.shouldFail = true; 70 | final recorder = Recorder(loader); 71 | await loader.loadIfNeeded(); 72 | await loader.performUserInitiatedRefresh(); 73 | expect(recorder.snapshots, [ 74 | 'noDataYet : null', 75 | 'failed : failed', 76 | 'noDataYet : null', 77 | 'hasData : 1' 78 | ]); 79 | }); 80 | 81 | test('test reset', () async { 82 | final loader = Controller(); 83 | final recorder = Recorder(loader); 84 | await loader.loadIfNeeded(); 85 | await loader.reset(); 86 | 87 | expect(recorder.snapshots, [ 88 | 'noDataYet : null', 89 | 'hasData : 1', 90 | 'noDataYet : null', 91 | 'hasData : 2', 92 | ]); 93 | }); 94 | 95 | test('test multiple loadIfNeeded', () async { 96 | final loader = Controller(); 97 | final recorder = Recorder(loader); 98 | loader.loadIfNeeded(); 99 | loader.loadIfNeeded(); 100 | loader.loadIfNeeded(); 101 | await loader.loadIfNeeded(); 102 | 103 | expect(recorder.snapshots, [ 104 | 'noDataYet : null', 105 | 'hasData : 1', 106 | ]); 107 | }); 108 | 109 | test('test instant success', () async { 110 | final loader = AsyncController.method(() async => 1); 111 | loader.addListener(() {}); 112 | await Future.microtask(() {}); 113 | expect(loader.value, 1); 114 | expect(loader.state, AsyncControllerState.hasData); 115 | }); 116 | 117 | test('test instant failure', () async { 118 | final loader = AsyncController.method( 119 | () async => throw 'failed' as Future Function()); 120 | loader.addListener(() {}); 121 | await Future.microtask(() {}); 122 | expect(loader.error, 'failed'); 123 | expect(loader.state, AsyncControllerState.failed); 124 | }); 125 | 126 | test('test duration', () async { 127 | final loader = AsyncController.method(() async => 1); 128 | await loader.loadIfNeeded(); 129 | 130 | expect(loader.lastFetchDuration, greaterThan(Duration.zero)); 131 | }); 132 | } 133 | -------------------------------------------------------------------------------- /test/paged_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:async_controller/async_controller.dart'; 3 | 4 | class Controller extends PagedAsyncController { 5 | Controller() : super(); 6 | 7 | static const largeCount = 10000000; 8 | 9 | int get pageSize => 10; 10 | 11 | @override 12 | Future> fetchPage(int pageIndex) async { 13 | final data = List.generate(pageSize, (i) => i + pageIndex * pageSize); 14 | return PagedData(pageIndex, largeCount, data); 15 | } 16 | 17 | int notifyCount = 0; 18 | 19 | @override 20 | void notifyListeners() { 21 | notifyCount++; 22 | super.notifyListeners(); 23 | } 24 | } 25 | 26 | void main() { 27 | test('fetch distant item', () async { 28 | final c = Controller(); 29 | c.addListener(() {}); 30 | await c.loadIfNeeded(); 31 | 32 | expect(c.totalCount, Controller.largeCount); 33 | 34 | for (int i = 0; i < 1000; i++) { 35 | final item = c.getItem(100); 36 | 37 | if (c.isLoading) { 38 | await c.loadIfNeeded(); 39 | } 40 | 41 | if (item != null) { 42 | break; 43 | } 44 | } 45 | 46 | expect(c.loadedItemsCount, 110); 47 | }); 48 | 49 | test('getItem must not notify', () async { 50 | final c = Controller(); 51 | await c.loadIfNeeded(); 52 | 53 | final counter1 = c.notifyCount; 54 | c.getItem(Controller.largeCount - 1); 55 | final counter2 = c.notifyCount; 56 | expect(counter2, counter1); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /test/refreshers_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:async_controller/async_controller.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | 8 | import 'utils.dart'; 9 | 10 | void main() { 11 | test('test change notifier refresher', () async { 12 | final notifier = ChangeNotifier(); 13 | 14 | final loader = Controller(); 15 | final recorder = loader.r; 16 | loader.addRefresher(ListeningRefresher(notifier)); 17 | 18 | await loader.loadIfNeeded(); 19 | notifier.notifyListeners(); 20 | await loader.loadIfNeeded(); 21 | 22 | expect(recorder.snapshots, [ 23 | 'noDataYet : null', 24 | 'hasData : 1', 25 | 'hasData : 2', 26 | ]); 27 | }); 28 | 29 | test('test periodic refresher', () async { 30 | final fake = FakeTimer(); 31 | fake.run(() async { 32 | final notifier = PeriodicRefresher(const Duration(seconds: 1)); 33 | expect(fake.tick, 0); 34 | 35 | final loader = Controller(); 36 | loader.addRefresher(notifier); 37 | final recorder = Recorder(loader); 38 | await loader.loadIfNeeded(); 39 | 40 | expect(recorder.snapshots, [ 41 | 'noDataYet : null', 42 | 'hasData : 1', 43 | ]); 44 | 45 | fake.fakeTick(); 46 | 47 | expect(recorder.snapshots, [ 48 | 'noDataYet : null', 49 | 'hasData : 1', 50 | 'hasData : 1', 51 | 'hasData : 2', 52 | ]); 53 | 54 | expect(fake.isActive, true); 55 | recorder.dispose(); 56 | expect(fake.isActive, false); 57 | }); 58 | }); 59 | 60 | test('test ifError ignored', () async { 61 | final loader = await Controller.withData(); 62 | 63 | loader.setNeedsRefresh(SetNeedsRefreshFlag.ifError); 64 | await loader.pump(); 65 | expect(loader.r.snapshots, []); 66 | }); 67 | 68 | test('test ifError not ignored', () async { 69 | final loader = await Controller.failed(); 70 | 71 | loader.setNeedsRefresh(SetNeedsRefreshFlag.ifError); 72 | await loader.waitUntilFinished(); 73 | expect(loader.r.snapshots, [ 74 | 'noDataYet : null', 75 | 'hasData : 1', 76 | ]); 77 | }); 78 | 79 | test('test reset', () async { 80 | final loader = await Controller.withData(); 81 | 82 | loader.setNeedsRefresh(SetNeedsRefreshFlag.reset); 83 | await loader.pump(); 84 | expect(loader.r.snapshots, [ 85 | 'noDataYet : null', 86 | ]); 87 | }); 88 | } 89 | 90 | class FakeTimer implements Timer { 91 | Function(Timer timer)? f; 92 | 93 | @override 94 | void cancel() { 95 | f = null; 96 | } 97 | 98 | void run(AsyncCallback callback) { 99 | runZoned( 100 | callback, 101 | zoneSpecification: 102 | ZoneSpecification(createPeriodicTimer: createPeriodicTimer), 103 | ); 104 | } 105 | 106 | @override 107 | bool get isActive => f != null; 108 | 109 | @override 110 | int tick = 0; 111 | 112 | void fakeTick() { 113 | tick++; 114 | f!(this); 115 | } 116 | 117 | Timer createPeriodicTimer(Zone self, ZoneDelegate parent, Zone zone, 118 | Duration period, void Function(Timer timer) f) { 119 | return this; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/updating_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import 'package:async_controller/async_controller.dart'; 5 | 6 | import 'utils.dart'; 7 | 8 | class Storage { 9 | late String string; 10 | late int number; 11 | } 12 | 13 | class TestUpdating extends UpdatingController { 14 | final Future Function(Set keys) updater; 15 | 16 | TestUpdating(this.updater) : super(Storage()); 17 | 18 | @override 19 | Future update(AsyncFetchItem item, Set keys) => updater(keys); 20 | 21 | UpdatingProperty get string => 22 | bind('string', (x) => x.string, (x, v) => x.string = v); 23 | UpdatingProperty get number => 24 | bind('login', (x) => x.number, (x, v) => x.number = v); 25 | } 26 | 27 | void main() { 28 | test('test simple', () async { 29 | final loader = TestUpdating((keys) => Future.value()); 30 | final p = loader.string; 31 | final r = Recorder.custom(p, () => describeEnum(p.status)); 32 | 33 | p.update('x'); 34 | expect(p.status, UpdatingPropertyStatus.needsUpdate); 35 | 36 | await loader.ensureUpdatedOrThrow(); 37 | 38 | expect(p.status, UpdatingPropertyStatus.ok); 39 | 40 | expect(r.snapshots, ['ok', 'needsUpdate', 'isUpdating', 'ok']); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:async_controller/async_controller.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | class Controller extends AsyncController { 5 | int counter = 1; 6 | 7 | bool shouldFail = false; 8 | 9 | late final _recorder = Recorder(this); 10 | Recorder get r { 11 | return _recorder; 12 | } 13 | 14 | static Future failed() async { 15 | final p = Controller(); 16 | p.shouldFail = true; 17 | await p.loadIfNeeded(); 18 | p.r.erase(); 19 | return p; 20 | } 21 | 22 | static Future withData() async { 23 | final p = Controller(); 24 | await p.loadIfNeeded(); 25 | p.r.erase(); 26 | return p; 27 | } 28 | 29 | @override 30 | Future fetch(AsyncFetchItem status) async { 31 | final willFail = shouldFail; 32 | shouldFail = false; 33 | 34 | await Future.delayed(Duration.zero); 35 | if (willFail) { 36 | throw 'failed'; 37 | } else { 38 | return counter++; 39 | } 40 | } 41 | 42 | Future waitUntilFinished() async { 43 | while (isLoading) { 44 | await Future.delayed(Duration(milliseconds: 10)); 45 | } 46 | } 47 | 48 | /// Ensures that futures were handled 49 | Future pump() => Future.microtask(() {}); 50 | } 51 | 52 | class Recorder { 53 | factory Recorder(AsyncController input) { 54 | String takeSnapshot() { 55 | final s = input.state; 56 | 57 | var name = describeEnum(s); 58 | name = name.padRight(10); 59 | 60 | final snapshot = '$name: ${input.value ?? input.error}'; 61 | 62 | return snapshot; 63 | } 64 | 65 | return Recorder.custom(input, takeSnapshot); 66 | } 67 | 68 | Recorder.custom(this.input, this.snapshotter) { 69 | input.addListener(onChanged); 70 | onChanged(); 71 | } 72 | 73 | final Listenable input; 74 | final String Function() snapshotter; 75 | final List snapshots = []; 76 | 77 | void dispose() { 78 | input.removeListener(onChanged); 79 | } 80 | 81 | void erase() { 82 | snapshots.clear(); 83 | } 84 | 85 | void onChanged() { 86 | final snapshot = snapshotter(); 87 | 88 | if (snapshots.isNotEmpty && snapshots.last == snapshot) { 89 | return; 90 | } 91 | 92 | snapshots.add(snapshot); 93 | } 94 | } 95 | --------------------------------------------------------------------------------