├── .DS_Store ├── .gitignore ├── .idea ├── flutter_develop_template.iml ├── git_toolbox_blame.xml ├── gradle.xml ├── libraries │ ├── Dart_Packages.xml │ ├── Dart_SDK.xml │ └── Flutter_Plugins.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml ├── vcs.xml └── workspace.xml ├── .vscode └── launch.json ├── 01.png ├── 02.png ├── 03.gif ├── 04.gif ├── 05.gif ├── 06.gif ├── 07.gif ├── 08.gif ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── .idea │ ├── caches │ │ └── deviceStreaming.xml │ ├── compiler.xml │ ├── deploymentTargetSelector.xml │ ├── git_toolbox_blame.xml │ ├── gradle.xml │ ├── jarRepositories.xml │ ├── kotlinc.xml │ ├── migrations.xml │ ├── misc.xml │ ├── runConfigurations.xml │ ├── vcs.xml │ └── workspace.xml ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── flutter_develop_template │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── flutter_develop_template_android.iml ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── flutter_develop_template.iml ├── ios ├── .DS_Store ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── 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 │ └── Runner-Bridging-Header.h └── RunnerTests │ └── RunnerTests.swift ├── lib ├── .DS_Store ├── common │ ├── .DS_Store │ ├── mvvm │ │ ├── base_change_notifier.dart │ │ ├── base_model.dart │ │ ├── base_page.dart │ │ ├── base_view_model.dart │ │ └── base_view_model_widget.dart │ ├── net │ │ ├── dio_client.dart │ │ ├── dio_interceptor.dart │ │ └── dio_response.dart │ ├── paging │ │ ├── base_paging_model.dart │ │ └── paging_data_model.dart │ ├── repository │ │ └── base_repository.dart │ ├── util │ │ └── global.dart │ └── widget │ │ ├── global_notification_widget.dart │ │ ├── notifier_widget.dart │ │ └── refresh_load_widget.dart ├── main.dart ├── main │ ├── app.dart │ ├── application.dart │ ├── main_dev.dart │ └── main_pre.dart ├── module │ ├── .DS_Store │ ├── home │ │ ├── .DS_Store │ │ ├── api │ │ │ └── home_repository.dart │ │ ├── model │ │ │ └── home_list_m.dart │ │ ├── view │ │ │ └── home_v.dart │ │ └── view_model │ │ │ └── home_vm.dart │ ├── message │ │ ├── api │ │ │ └── message_repository.dart │ │ ├── model │ │ │ └── message_list_m.dart │ │ ├── view │ │ │ └── message_v.dart │ │ └── view_model │ │ │ └── message_vm.dart │ ├── order │ │ ├── view │ │ │ └── order_v.dart │ │ └── view_model │ │ │ └── order_vm.dart │ ├── personal │ │ ├── api │ │ │ └── personal_repository.dart │ │ ├── model │ │ │ └── user_info_m.dart │ │ ├── view │ │ │ └── personal_v.dart │ │ └── view_model │ │ │ └── personal_vm.dart │ └── test_fluro │ │ ├── page_a.dart │ │ ├── page_a2.dart │ │ ├── page_b.dart │ │ ├── page_c.dart │ │ └── page_d.dart ├── res │ ├── .DS_Store │ ├── string │ │ ├── str_common.dart │ │ ├── str_home.dart │ │ ├── str_message.dart │ │ ├── str_order.dart │ │ └── str_personal.dart │ └── style │ │ ├── color_styles.dart │ │ ├── text_styles.dart │ │ └── theme_styles.dart └── router │ ├── navigator_util.dart │ ├── page_route_observer.dart │ └── routers.dart ├── pubspec.yaml └── test └── widget_test.dart /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | 14 | # dotenv environment variables file 15 | .env* 16 | 17 | # Avoid committing generated Javascript files: 18 | *.dart.js 19 | *.info.json # Produced by the --dump-info flag. 20 | *.js # When generated by dart2js. Don't specify *.js if your 21 | # project includes source files written in JavaScript. 22 | *.js_ 23 | *.js.deps 24 | *.js.map 25 | 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | -------------------------------------------------------------------------------- /.idea/flutter_develop_template.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/git_toolbox_blame.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/libraries/Dart_SDK.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.idea/libraries/Flutter_Plugins.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 28 | 29 | 30 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 53 | { 54 | "associatedIndex": 5 55 | } 56 | 57 | 58 | 59 | 60 | 61 | 63 | { 64 | "keyToString": { 65 | "Flutter.main.dart.executor": "Run", 66 | "Flutter.main_dev.dart.executor": "Run", 67 | "Flutter.main_develop.dart.executor": "Run", 68 | "RunOnceActivity.ShowReadmeOnStart": "true", 69 | "RunOnceActivity.cidr.known.project.marker": "true", 70 | "RunOnceActivity.readMode.enableVisualFormatting": "true", 71 | "SHARE_PROJECT_CONFIGURATION_FILES": "true", 72 | "cf.first.check.clang-format": "false", 73 | "cidr.known.project.marker": "true", 74 | "dart.analysis.tool.window.visible": "false", 75 | "git-widget-placeholder": "master", 76 | "io.flutter.reload.alreadyRun": "true", 77 | "kotlin-language-version-configured": "true", 78 | "last_opened_file_path": "/Users/hanxuguang/Documents/develop/technology_task/flutter/flutter_project/flutter_develop_template", 79 | "settings.editor.selected.configurable": "flutter.settings", 80 | "show.migrate.to.gradle.popup": "false" 81 | } 82 | } 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 104 | 105 | 108 | 109 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 1717291018554 123 | 127 | 128 | 129 | 130 | 139 | 140 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "launch main", 6 | "request": "launch", 7 | "type": "dart", 8 | "program": "lib/main.dart" 9 | }, 10 | { 11 | "name": "attach main", 12 | "request": "attach", 13 | "type": "dart", 14 | "program": "lib/main.dart" 15 | }, 16 | { 17 | "name": "launch dev", 18 | "request": "launch", 19 | "type": "dart", 20 | "program": "lib/main/main_dev.dart" 21 | }, 22 | { 23 | "name": "attach dev", 24 | "request": "attach", 25 | "type": "dart", 26 | "program": "lib/main/main_dev.dart" 27 | }, 28 | { 29 | "name": "launch pre", 30 | "request": "launch", 31 | "type": "dart", 32 | "program": "lib/main/main_pre.dart" 33 | }, 34 | { 35 | "name": "attach pre", 36 | "request": "attach", 37 | "type": "dart", 38 | "program": "lib/main/main_pre.dart" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/01.png -------------------------------------------------------------------------------- /02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/02.png -------------------------------------------------------------------------------- /03.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/03.gif -------------------------------------------------------------------------------- /04.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/04.gif -------------------------------------------------------------------------------- /05.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/05.gif -------------------------------------------------------------------------------- /06.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/06.gif -------------------------------------------------------------------------------- /07.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/07.gif -------------------------------------------------------------------------------- /08.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/08.gif -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | use_key_in_widget_constructors: false 25 | prefer_const_constructors: false 26 | package_names: null 27 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 28 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 29 | 30 | # Additional information about this file can be found at 31 | # https://dart.dev/guides/language/analysis-options 32 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/.idea/git_toolbox_blame.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /android/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 33 | 34 | -------------------------------------------------------------------------------- /android/.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | 49 | 50 | -------------------------------------------------------------------------------- /android/.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /android/.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /android/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /android/.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /android/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | def localProperties = new Properties() 9 | def localPropertiesFile = rootProject.file("local.properties") 10 | if (localPropertiesFile.exists()) { 11 | localPropertiesFile.withReader("UTF-8") { reader -> 12 | localProperties.load(reader) 13 | } 14 | } 15 | 16 | def flutterVersionCode = localProperties.getProperty("flutter.versionCode") 17 | if (flutterVersionCode == null) { 18 | flutterVersionCode = "1" 19 | } 20 | 21 | def flutterVersionName = localProperties.getProperty("flutter.versionName") 22 | if (flutterVersionName == null) { 23 | flutterVersionName = "1.0" 24 | } 25 | 26 | android { 27 | namespace = "com.example.flutter_develop_template" 28 | compileSdk = flutter.compileSdkVersion 29 | ndkVersion = flutter.ndkVersion 30 | 31 | compileOptions { 32 | sourceCompatibility = JavaVersion.VERSION_1_8 33 | targetCompatibility = JavaVersion.VERSION_1_8 34 | } 35 | 36 | defaultConfig { 37 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 38 | applicationId = "com.example.flutter_develop_template" 39 | // You can update the following values to match your application needs. 40 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 41 | minSdk = flutter.minSdkVersion 42 | targetSdk = flutter.targetSdkVersion 43 | versionCode = flutterVersionCode.toInteger() 44 | versionName = flutterVersionName 45 | } 46 | 47 | buildTypes { 48 | release { 49 | // TODO: Add your own signing config for the release build. 50 | // Signing with the debug keys for now, so `flutter run --release` works. 51 | signingConfig = signingConfigs.debug 52 | } 53 | } 54 | } 55 | 56 | flutter { 57 | source = "../.." 58 | } 59 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 19 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 37 | 38 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/flutter_develop_template/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.flutter_develop_template 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } 6 | maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter' } 7 | maven { url 'https://maven.aliyun.com/nexus/content/repositories/google' } 8 | maven { url 'https://maven.aliyun.com/nexus/content/repositories/gradle-plugin' } 9 | } 10 | } 11 | 12 | rootProject.buildDir = "../build" 13 | subprojects { 14 | project.buildDir = "${rootProject.buildDir}/${project.name}" 15 | } 16 | subprojects { 17 | project.evaluationDependsOn(":app") 18 | } 19 | 20 | tasks.register("clean", Delete) { 21 | delete rootProject.buildDir 22 | } 23 | -------------------------------------------------------------------------------- /android/flutter_develop_template_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.java.home=/Users/hanxuguang/Documents/develop/technology_task/java_server/java_profile/jdk/jdk-17.0.12.jdk/Contents/Home 2 | org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-7.6.3-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "7.3.0" apply false 22 | id "org.jetbrains.kotlin.android" version "1.7.10" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /flutter_develop_template.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ios/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/.DS_Store -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /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 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.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 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - package_info_plus (0.4.5): 4 | - Flutter 5 | - Sentry/HybridSDK (8.25.0) 6 | - sentry_flutter (7.20.2): 7 | - Flutter 8 | - FlutterMacOS 9 | - Sentry/HybridSDK (= 8.25.0) 10 | 11 | DEPENDENCIES: 12 | - Flutter (from `Flutter`) 13 | - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) 14 | - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) 15 | 16 | SPEC REPOS: 17 | trunk: 18 | - Sentry 19 | 20 | EXTERNAL SOURCES: 21 | Flutter: 22 | :path: Flutter 23 | package_info_plus: 24 | :path: ".symlinks/plugins/package_info_plus/ios" 25 | sentry_flutter: 26 | :path: ".symlinks/plugins/sentry_flutter/ios" 27 | 28 | SPEC CHECKSUMS: 29 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 30 | package_info_plus: 580e9a5f1b6ca5594e7c9ed5f92d1dfb2a66b5e1 31 | Sentry: cd86fc55628f5b7c572cabe66cc8f95a9d2f165a 32 | sentry_flutter: 99aff87df84da27d0c73bc58b8f9399ef73811bc 33 | 34 | PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 35 | 36 | COCOAPODS: 1.11.0 37 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flutter Develop Template 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutter_develop_template 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/lib/.DS_Store -------------------------------------------------------------------------------- /lib/common/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/lib/common/.DS_Store -------------------------------------------------------------------------------- /lib/common/mvvm/base_change_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BaseChangeNotifier extends ChangeNotifier { 4 | 5 | /// 刷新UI 6 | refreshState() => notifyListeners(); 7 | 8 | } -------------------------------------------------------------------------------- /lib/common/mvvm/base_model.dart: -------------------------------------------------------------------------------- 1 | import 'base_view_model.dart'; 2 | 3 | class BaseModel { 4 | 5 | VM? vm; 6 | 7 | void onDispose() { 8 | vm = null; 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /lib/common/mvvm/base_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/scheduler.dart'; 6 | import 'package:flutter_develop_template/common/mvvm/base_model.dart'; 7 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 8 | import 'package:flutter_develop_template/common/mvvm/base_view_model_widget.dart'; 9 | import 'package:flutter_develop_template/common/paging/base_paging_model.dart'; 10 | import 'package:flutter_develop_template/main/application.dart'; 11 | 12 | import '../../res/string/str_common.dart'; 13 | import '../widget/global_notification_widget.dart'; 14 | 15 | abstract class BaseStatefulPage extends BaseViewModelStatefulWidget { 16 | BaseStatefulPage({super.key}); 17 | 18 | @override 19 | BaseStatefulPageState createState(); 20 | } 21 | 22 | abstract class BaseStatefulPageState 23 | extends BaseViewModelStatefulWidgetState 24 | with AutomaticKeepAliveClientMixin { 25 | 26 | /// 定义对应的 viewModel 27 | VM? viewModel; 28 | 29 | /// 监听应用生命周期 30 | AppLifecycleListener? lifecycleListener; 31 | 32 | /// 获取应用状态 33 | AppLifecycleState? get lifecycleState => 34 | SchedulerBinding.instance.lifecycleState; 35 | 36 | /// 是否打印 监听应用生命周期的 日志 37 | bool debugPrintLifecycleLog = false; 38 | 39 | /// 进行初始化ViewModel相关操作 40 | @override 41 | void initState() { 42 | super.initState(); 43 | 44 | /// 初始化页面 属性、对象、绑定监听 45 | initAttribute(); 46 | initObserver(); 47 | 48 | /// 初始化ViewModel,并同步生命周期 49 | viewModel = viewBindingViewModel(); 50 | 51 | /// 调用viewModel的生命周期,比如 初始化 请求网络数据 等 52 | viewModel?.onCreate(); 53 | 54 | /// Flutter 低版本 使用 WidgetsBindingObserver,高版本 使用 AppLifecycleListener 55 | lifecycleListener = AppLifecycleListener( 56 | // 监听状态回调 57 | onStateChange: onStateChange, 58 | 59 | // 可见,并且可以响应用户操作时的回调 60 | onResume: onResume, 61 | 62 | // 可见,但无法响应用户操作时的回调 63 | onInactive: onInactive, 64 | 65 | // 隐藏时的回调 66 | onHide: onHide, 67 | 68 | // 显示时的回调 69 | onShow: onShow, 70 | 71 | // 暂停时的回调 72 | onPause: onPause, 73 | 74 | // 暂停后恢复时的回调 75 | onRestart: onRestart, 76 | 77 | // 当退出 并将所有视图与引擎分离时的回调(IOS 支持,Android 不支持) 78 | onDetach: onDetach, 79 | 80 | // 在退出程序时,发出询问的回调(IOS、Android 都不支持) 81 | onExitRequested: onExitRequested, 82 | ); 83 | 84 | /// 页面布局完成后的回调函数 85 | lifecycleListener?.binding.addPostFrameCallback((_) { 86 | assert(context != null, 'addPostFrameCallback throw Error context'); 87 | 88 | /// 初始化 需要context 的属性、对象、绑定监听 89 | initContextAttribute(context); 90 | initContextObserver(context); 91 | }); 92 | } 93 | 94 | @override 95 | void didChangeDependencies() { 96 | assert((){ 97 | debugPrint('BaseStatefulPage.didChangeDependencies --- ${GlobalOperateProvider.getGlobalOperate(context: context)}'); 98 | return true; 99 | }()); 100 | } 101 | 102 | /// 监听状态 103 | onStateChange(AppLifecycleState state) => mLog('app_state:$state'); 104 | 105 | /// =============================== 根据应用状态产生的各种回调 =============================== 106 | 107 | /// 可见,并且可以响应用户操作时的回调 108 | /// 比如从应用后台调度到前台时,在 onShow() 后面 执行 109 | onResume() => mLog('onResume'); 110 | 111 | /// 可见,但无法响应用户操作时的回调 112 | onInactive() => mLog('onInactive'); 113 | 114 | /// 隐藏时的回调 115 | onHide() => mLog('onHide'); 116 | 117 | /// 显示时的回调,从应用后台调度到前台时 118 | onShow() => mLog('onShow'); 119 | 120 | /// 暂停时的回调 121 | onPause() => mLog('onPause'); 122 | 123 | /// 暂停后恢复时的回调 124 | onRestart() => mLog('onRestart'); 125 | 126 | /// 这两个回调,不是所有平台都支持, 127 | 128 | /// 当退出 并将所有视图与引擎分离时的回调(IOS 支持,Android 不支持) 129 | onDetach() => mLog('onDetach'); 130 | 131 | /// 在退出程序时,发出询问的回调(IOS、Android 都不支持) 132 | /// 响应 [AppExitResponse.exit] 将继续终止,响应 [AppExitResponse.cancel] 将取消终止。 133 | Future onExitRequested() async { 134 | mLog('onExitRequested'); 135 | return AppExitResponse.exit; 136 | } 137 | 138 | /// BaseStatefulPageState的子类,重写 dispose() 139 | /// 一定要执行父类 dispose(),防止内存泄漏 140 | @override 141 | void dispose() { 142 | /// 销毁顺序 143 | 144 | /// 1、Model 销毁其持有的 ViewModel 145 | if(viewModel?.pageDataModel?.data is BaseModel?) { 146 | BaseModel? baseModel = viewModel?.pageDataModel?.data as BaseModel?; 147 | baseModel?.onDispose(); 148 | } 149 | if(viewModel?.pageDataModel?.data is BasePagingModel?) { 150 | BasePagingModel? basePagingModel = viewModel?.pageDataModel?.data as BasePagingModel?; 151 | basePagingModel?.onDispose(); 152 | } 153 | 154 | /// 2、ViewModel 销毁其持有的 View 155 | /// 3、ViewModel 销毁其持有的 Model 156 | viewModel?.onDispose(); 157 | 158 | /// 4、View 销毁其持有的 ViewModel 159 | viewModel = null; 160 | 161 | /// 5、销毁监听App生命周期方法 162 | lifecycleListener?.dispose(); 163 | super.dispose(); 164 | } 165 | 166 | /// 是否保持页面状态 167 | @override 168 | bool get wantKeepAlive => false; 169 | 170 | /// View 持有对应的 ViewModel 171 | VM viewBindingViewModel(); 172 | 173 | /// 子类重写,初始化 属性、对象 174 | /// 这里不是 网络请求操作,而是页面的初始化数据 175 | /// 网络请求操作,建议在viewModel.onCreate() 中实现 176 | void initAttribute(); 177 | 178 | /// 子类重写,初始化 需要 context 的属性、对象 179 | void initContextAttribute(BuildContext context) {} 180 | 181 | /// 子类重写,初始化绑定监听 182 | void initObserver(); 183 | 184 | /// 子类重写,初始化需要 context 的绑定监听 185 | void initContextObserver(BuildContext context) {} 186 | 187 | /// 输出日志 188 | void mLog(String info) { 189 | if (debugPrintLifecycleLog) { 190 | assert(() { 191 | debugPrint('--- $info'); 192 | return true; 193 | }()); 194 | } 195 | } 196 | 197 | /// 手机应用 198 | Widget appBuild(BuildContext context) => SizedBox(); 199 | 200 | /// Web 201 | Widget webBuild(BuildContext context) => SizedBox(); 202 | 203 | /// PC应用 204 | Widget pcBuild(BuildContext context) => SizedBox(); 205 | 206 | @override 207 | Widget build(BuildContext context) { 208 | /// 使用 AutomaticKeepAliveClientMixin 需要 super.build(context); 209 | /// 210 | /// 注意:AutomaticKeepAliveClientMixin 只是保存页面状态,并不影响 build 方法执行 211 | /// 比如 PageVie的 子页面 使用了AutomaticKeepAliveClientMixin 保存状态, 212 | /// PageView切换子页面时,子页面的build的还是会执行 213 | if(wantKeepAlive) { 214 | super.build(context); 215 | } 216 | 217 | /// 和 GlobalNotificationWidget,建立依赖关系 218 | if(EnvConfig.isGlobalNotification) { 219 | GlobalNotificationWidget.of(context); 220 | } 221 | 222 | switch (EnvConfig.platform) { 223 | case ApplicationPlatform.app: { 224 | if (Platform.isAndroid || Platform.isIOS) { 225 | // 如果,还想根据当前设备屏幕尺寸细分, 226 | // 使用MediaQuery,拿到当前设备信息,进一步适配 227 | return appBuild(context); 228 | } 229 | } 230 | case ApplicationPlatform.web: { 231 | return webBuild(context); 232 | } 233 | case ApplicationPlatform.pc: { 234 | if(Platform.isWindows || Platform.isMacOS) { 235 | return pcBuild(context); 236 | } 237 | } 238 | } 239 | return Center( 240 | child: Text(StrCommon.platformNotAdapted), 241 | ); 242 | } 243 | 244 | } 245 | -------------------------------------------------------------------------------- /lib/common/mvvm/base_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | import '../widget/notifier_widget.dart'; 4 | import 'base_page.dart'; 5 | 6 | /// 基类 7 | abstract class BaseViewModel { 8 | 9 | } 10 | 11 | /// 页面继承的ViewModel,不直接使用 BaseViewModel, 12 | /// 是因为BaseViewModel基类里代码,还是不要太多为好,扩展创建新的子类就好 13 | abstract class PageViewModel extends BaseViewModel { 14 | 15 | /// 定义对应的 view 16 | BaseStatefulPageState? viewState; 17 | 18 | /// 使用 直接拿它 19 | T get state => viewState as T; 20 | 21 | /// 页面数据model 22 | PageDataModel? pageDataModel; 23 | 24 | /// 尽量在onCreate方法中编写初始化逻辑 25 | void onCreate(); 26 | 27 | /// 对应的widget被销毁了,销毁相关引用对象,避免内存泄漏 28 | void onDispose() { 29 | viewState = null; 30 | pageDataModel = null; 31 | } 32 | 33 | /// 请求数据 34 | Future requestData({Map? params}); 35 | 36 | } -------------------------------------------------------------------------------- /lib/common/mvvm/base_view_model_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 3 | 4 | /// 定义ViewModelWidget基类 5 | abstract class BaseViewModelStatefulWidget extends StatefulWidget { 6 | BaseViewModelStatefulWidget({super.key}); 7 | 8 | @override 9 | BaseViewModelStatefulWidgetState createState(); 10 | } 11 | 12 | /// 定义ViewModelState 13 | abstract class BaseViewModelStatefulWidgetState extends State { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /lib/common/net/dio_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:dio/io.dart'; 5 | 6 | import '../../main/application.dart'; 7 | import 'dio_interceptor.dart'; 8 | 9 | /// 默认超时时间 10 | const defaultTimeout = 60 * 1000; 11 | 12 | class DioClient extends DioForNative { 13 | /// 单例 14 | static DioClient? _instance; 15 | 16 | factory DioClient() => _instance ??= DioClient._init(); 17 | 18 | /// 初始化方法 19 | DioClient._init() { 20 | options = BaseOptions( 21 | // 设置基础配置 22 | connectTimeout: Duration(milliseconds: defaultTimeout), // 连接超时时间 23 | receiveTimeout: Duration(milliseconds: defaultTimeout), // 接收超时时间 24 | sendTimeout: Duration(milliseconds: defaultTimeout), // 发送超时时间 25 | baseUrl: EnvConfig.baseUrl 26 | // ... ... 27 | ); 28 | // 拦截器 29 | interceptors.add(DioInterceptor()); // 拦截器 30 | 31 | // 代理抓包( 32 | // 注意: 33 | // 1、我使用的是Mac电脑,抓包工具是 Charles,建议正式上线时,关闭此功能 34 | // 2、如果开启了抓包,但没有启动 抓包工具,Dio 会报 连接异常 DioException [connection error] 35 | // https://juejin.cn/post/7131928652568231966 36 | // https://juejin.cn/post/7035652365826916366 37 | proxy(); 38 | } 39 | 40 | /// ResultFul API 风格 41 | // GET:从服务器获取一项或者多项数据 42 | // POST:在服务器新建一个资源 43 | // PUT:在服务器更新所有资源 44 | // PATCH:更新部分属性 45 | // DELETE:从服务器删除资源 46 | 47 | /// Get请求 48 | Future doGet( 49 | path, { 50 | String? baseUrl, 51 | Map? headers, 52 | ResponseType responseType = ResponseType.json, 53 | Object? data, 54 | Map? params, 55 | Options? cOptions, 56 | CancelToken? cancelToken, 57 | ProgressCallback? onReceiveProgress, 58 | }) { 59 | updateBaseOptions(baseUrl, headers, responseType); // 动态修改默认BaseOptions 60 | return get( 61 | path, 62 | data: data, 63 | queryParameters: params, 64 | options: cOptions, 65 | onReceiveProgress: onReceiveProgress, 66 | cancelToken: cancelToken, 67 | ); 68 | } 69 | 70 | /// Post 请求 71 | Future doPost( 72 | path, { 73 | String? baseUrl, 74 | Map? headers, 75 | ResponseType responseType = ResponseType.json, 76 | Object? data, 77 | Map? params, 78 | Options? cOptions, 79 | CancelToken? cancelToken, 80 | ProgressCallback? onSendProgress, 81 | ProgressCallback? onReceiveProgress, 82 | }) { 83 | updateBaseOptions(baseUrl, headers, responseType); // 动态修改默认BaseOptions 84 | return post(path, 85 | data: data, 86 | queryParameters: params, 87 | options: cOptions, 88 | cancelToken: cancelToken, 89 | onSendProgress: onSendProgress, 90 | onReceiveProgress: onReceiveProgress); 91 | } 92 | 93 | /// Put 请求 94 | Future doPut(path, 95 | {String? baseUrl, 96 | Map? headers, 97 | ResponseType responseType = ResponseType.json, 98 | Object? data, 99 | Map? params, 100 | Options? cOptions, 101 | CancelToken? cancelToken, 102 | ProgressCallback? onSendProgress, 103 | ProgressCallback? onReceiveProgress}) { 104 | updateBaseOptions(baseUrl, headers, responseType); // 动态修改默认BaseOptions 105 | return put(path, 106 | data: data, 107 | queryParameters: params, 108 | options: cOptions, 109 | cancelToken: cancelToken, 110 | onSendProgress: onSendProgress, 111 | onReceiveProgress: onReceiveProgress); 112 | } 113 | 114 | /// Patch 请求 115 | Future doPatch(path, 116 | {String? baseUrl, 117 | Map? headers, 118 | ResponseType responseType = ResponseType.json, 119 | Object? data, 120 | Map? params, 121 | Options? cOptions, 122 | CancelToken? cancelToken, 123 | ProgressCallback? onSendProgress, 124 | ProgressCallback? onReceiveProgress}) { 125 | updateBaseOptions(baseUrl, headers, responseType); // 动态修改默认BaseOptions 126 | return path(path, 127 | listData: data, 128 | queryParameters: params, 129 | options: cOptions, 130 | cancelToken: cancelToken, 131 | onSendProgress: onSendProgress, 132 | onReceiveProgress: onReceiveProgress); 133 | } 134 | 135 | /// Delete 请求 136 | Future doDelete(path, 137 | {String? baseUrl, 138 | Map? headers, 139 | ResponseType responseType = ResponseType.json, 140 | Object? data, 141 | Map? params, 142 | Options? cOptions, 143 | CancelToken? cancelToken}) { 144 | updateBaseOptions(baseUrl, headers, responseType); // 动态修改默认BaseOptions 145 | return delete( 146 | path, 147 | data: data, 148 | queryParameters: params, 149 | options: cOptions, 150 | cancelToken: cancelToken, 151 | ); 152 | } 153 | 154 | /// 上传文件 155 | Future uploadFile( 156 | path, { 157 | String? baseUrl, 158 | Map? headers, 159 | ResponseType responseType = ResponseType.json, 160 | Object? data, 161 | Map? params, 162 | Options? cOptions, 163 | CancelToken? cancelToken, 164 | ProgressCallback? onSendProgress, 165 | ProgressCallback? onReceiveProgress, 166 | }) { 167 | updateBaseOptions(baseUrl, headers, responseType); // 动态修改默认BaseOptions 168 | options.contentType = Headers.multipartFormDataContentType; 169 | return post(path, 170 | data: data, 171 | queryParameters: params, 172 | options: cOptions, 173 | cancelToken: cancelToken, 174 | onSendProgress: onSendProgress, 175 | onReceiveProgress: onReceiveProgress); 176 | } 177 | 178 | /// 动态修改 BaseOptions 179 | void updateBaseOptions( 180 | String? baseUrl, 181 | Map? headers, 182 | ResponseType responseType, 183 | ) { 184 | if (baseUrl != null) { 185 | options.baseUrl = baseUrl; 186 | } 187 | if (headers != null) { 188 | options.headers = headers; 189 | } 190 | if (responseType != ResponseType.json) { 191 | options.responseType = responseType; 192 | } 193 | } 194 | 195 | /// 代理抓包,测试阶段可能需要 196 | void proxy() { 197 | if (EnvConfig.proxyEnable) { 198 | if (EnvConfig.caughtAddress?.isNotEmpty ?? false) { 199 | (httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { 200 | final client = HttpClient(); 201 | client.findProxy = (uri) => 'PROXY ' + EnvConfig.caughtAddress!; 202 | 203 | client.badCertificateCallback = (cert, host, port) => true; 204 | return client; 205 | }; 206 | } 207 | } 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /lib/common/net/dio_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_develop_template/main/app.dart'; 4 | import 'package:flutter_develop_template/main/application.dart'; 5 | 6 | import '../widget/global_notification_widget.dart'; 7 | import 'dio_response.dart'; 8 | 9 | final DateTime dateTime = DateTime.now(); 10 | 11 | /// Dio拦截器 12 | class DioInterceptor extends InterceptorsWrapper { 13 | /// 请求发起前,调用的方法 14 | /// 可以在这里动态修改Header里信息,从options里获取原来的Header信息,进行修改 15 | /// 常见的场景有:弹出加载loading、添加Token 16 | @override 17 | void onRequest(RequestOptions options, RequestInterceptorHandler handler) { 18 | // options.headers['token'] = 'xxx'; // 设置 token 19 | 20 | // 打印请求日志信息 21 | assert(() { 22 | debugPrint( 23 | '${dateTime}:${options.method} request:${options.baseUrl + options.path}'); 24 | debugPrint('params:${options.queryParameters}'); 25 | return true; 26 | }()); 27 | 28 | // 或者刷新token时效 29 | 30 | /// 重置 全局操作状态 31 | if (EnvConfig.isGlobalNotification) { 32 | GlobalOperateProvider.runGlobalOperate( 33 | context: navigatorKey.currentContext, operate: GlobalOperate.idle); 34 | } 35 | 36 | /// 必须要写的代码,表示进入下一步 37 | handler.next(options); 38 | } 39 | 40 | /// 请求成功后,执行的响应方法 41 | @override 42 | void onResponse(Response response, ResponseInterceptorHandler handler) { 43 | // 打印响应日志信息 44 | 45 | // 打印请求日志信息 46 | assert(() { 47 | debugPrint('${dateTime}:response:${response.statusCode}'); 48 | // debugPrint('data:${response.data.toString()}'); 49 | return true; 50 | }()); 51 | 52 | if (response.statusCode == 200) { 53 | /// 访问成功 54 | 55 | /// 有返回值的情况,转实体 56 | if (response.data is Map) { 57 | ResponseData responseData = ResponseData.fromJson(response.data); 58 | 59 | /// 成功 60 | if (responseData.success) { 61 | response.statusCode = responseData.statusCode; 62 | response.data = responseData.data; 63 | } else { 64 | /// 走到这,说明访问成功,但业务不允许,比如没有权限 65 | response.statusCode = responseData.statusCode; 66 | response.statusMessage = responseData.statusMsgConversion; 67 | } 68 | 69 | } 70 | 71 | } 72 | 73 | /// 必须要写的代码,表示进入下一步 74 | return handler.resolve(response); 75 | } 76 | 77 | /// 这里的异常,属于Dio自身的异常 78 | @override 79 | void onError(DioException err, ErrorInterceptorHandler handler) { 80 | throw DioException( 81 | requestOptions: err.requestOptions, 82 | type: err.type, 83 | error: err, 84 | response: err.response); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /lib/common/net/dio_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | import '../../res/string/str_common.dart'; 4 | 5 | /// 响应的数据模型,根据你当前后台返回的数据来自定义,我这里只是做个例子 6 | abstract class BaseResponseData { 7 | int? statusCode; 8 | String? statusMessage; 9 | dynamic data; 10 | } 11 | 12 | const int REQUEST_SUCCESS = 0; 13 | class ResponseData extends BaseResponseData { 14 | 15 | bool get success => statusCode == REQUEST_SUCCESS; 16 | 17 | String? get statusMsgConversion => serviceFailureConversionText(statusCode); 18 | 19 | /// 这里是 业务逻辑不通过 20 | /// code 由后台人员自定义 21 | String? serviceFailureConversionText(int? errCode) { 22 | String str; 23 | switch (errCode) { 24 | case -1 : { 25 | str = statusMessage ?? '-1'; 26 | } break; 27 | default: { 28 | str = '未知错误'; 29 | } 30 | } 31 | return str; 32 | } 33 | 34 | ResponseData.fromJson(Map json) { 35 | statusCode = json['errorCode'] ?? json['code']; 36 | statusMessage = json['errorMsg'] ?? json['message']; 37 | data = json['data'] ?? json['data']; 38 | } 39 | 40 | } 41 | 42 | /// 根据Dio异常类型 转换为 文字 43 | String? dioErrorConversionText(DioException e) { 44 | String str; 45 | switch (e.type) { 46 | case DioExceptionType.connectionTimeout: 47 | str = '[DioExceptionType.connectionTimeout] ${StrCommon.connectionTimeout}'; 48 | break; 49 | case DioExceptionType.sendTimeout: 50 | str = '[DioExceptionType.sendTimeout] ${StrCommon.sendTimeout}'; 51 | break; 52 | case DioExceptionType.receiveTimeout: 53 | str = '[DioExceptionType.receiveTimeout] ${StrCommon.receiveTimeout}'; 54 | break; 55 | case DioExceptionType.badCertificate: 56 | str = '[DioExceptionType.badCertificate] ${StrCommon.accessCertificateError}'; 57 | break; 58 | case DioExceptionType.badResponse: 59 | str = '[DioExceptionType.badResponse] ${StrCommon.validationFailed}'; 60 | break; 61 | case DioExceptionType.connectionError: 62 | str = '[DioExceptionType.connectionError] ${StrCommon.connectionIsAbnormal}'; 63 | break; 64 | case DioExceptionType.unknown: 65 | default: 66 | str = '[DioExceptionType.unknown] ${StrCommon.unknownError}'; 67 | break; 68 | } 69 | return str; 70 | } 71 | 72 | 73 | /// 根据Dio异常code 转换为 文字 74 | String? codeConversionText(int? statusCode) { 75 | String str; 76 | if (statusCode != null) { 77 | switch (statusCode) { 78 | case 400: 79 | str = '[$statusCode] ${StrCommon.parameterIsIncorrect}'; 80 | break; 81 | case 402: 82 | str = '[$statusCode] ${StrCommon.illegalRequests}'; 83 | break; 84 | case 403: 85 | str = '[$statusCode] ${StrCommon.serverRejectsRequest}'; 86 | break; 87 | case 404: 88 | str = '[$statusCode] ${StrCommon.accessAddressDoesNotExist}'; 89 | break; 90 | case 405: 91 | str = '[$statusCode] ${StrCommon.requestIsMadeWrongWay}'; 92 | break; 93 | case 500: 94 | str = '[$statusCode] ${StrCommon.wasAnErrorInsideServer}'; 95 | break; 96 | case 502: 97 | str = '[$statusCode] ${StrCommon.invalidRequest}'; 98 | break; 99 | case 503: 100 | str = '[$statusCode] ${StrCommon.serverIsBusy}'; 101 | break; 102 | case 505: 103 | str = '[$statusCode] ${StrCommon.unsupportedHttpProtocol}'; 104 | break; 105 | default: 106 | str = '[$statusCode] ${StrCommon.unknownError}'; 107 | break; 108 | } 109 | } else { 110 | str = '[$statusCode] ${StrCommon.unknownError}'; 111 | } 112 | return str; 113 | } 114 | 115 | 116 | -------------------------------------------------------------------------------- /lib/common/paging/base_paging_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_develop_template/common/mvvm/base_model.dart'; 2 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 3 | 4 | /// 内部 有分页列表集合 的实体需要继承 BasePagingModel 5 | class BasePagingModel extends BaseModel{ 6 | int? curPage; 7 | List? datas; 8 | int? offset; 9 | bool? over; 10 | int? pageCount; 11 | int? size; 12 | int? total; 13 | 14 | BasePagingModel({this.curPage, this.datas, this.offset, this.over, 15 | this.pageCount, this.size, this.total}); 16 | 17 | } 18 | 19 | /// 是分页列表 集合子项 实体需要继承 BasePagingItem 20 | class BasePagingItem {} -------------------------------------------------------------------------------- /lib/common/paging/paging_data_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_develop_template/common/mvvm/base_change_notifier.dart'; 3 | 4 | import '../mvvm/base_view_model.dart'; 5 | import '../widget/notifier_widget.dart'; 6 | 7 | /// 分页数据相关 8 | 9 | /// 分页行为:下拉刷新/上拉加载更多 10 | enum PagingBehavior { 11 | /// 空闲,默认状态 12 | idle, 13 | 14 | /// 加载 15 | load, 16 | 17 | /// 刷新 18 | refresh; 19 | } 20 | 21 | /// 分页状态:执行完 下拉刷新/上拉加载更多后,得到的状态 22 | enum PagingState { 23 | /// 空闲,默认状态 24 | idle, 25 | 26 | /// 加载成功 27 | loadSuccess, 28 | 29 | /// 加载失败 30 | loadFail, 31 | 32 | /// 没有更多数据了 33 | loadNoData, 34 | 35 | /// 正在加载 36 | curLoading, 37 | 38 | /// 刷新成功 39 | refreshSuccess, 40 | 41 | /// 刷新失败 42 | refreshFail, 43 | 44 | /// 正在刷新 45 | curRefreshing, 46 | } 47 | 48 | /// 分页数据对象 49 | class PagingDataModel { 50 | // 当前页码 51 | int curPage; 52 | 53 | // 总共多少页 54 | int pageCount; 55 | 56 | // 总共 数据数量 57 | int total; 58 | 59 | // 当前页 数据数量 60 | int size; 61 | 62 | /// 响应的完整数据 63 | /// 你可能还需要,响应数据的 其他字段, 64 | /// 65 | /// assert((){ 66 | /// var mListModel = pageDataModel?.data as MessageListModel?; // 转化成对应的Model 67 | /// debugPrint('---pageCount:${mListModel?.pageCount}'); // 获取字段 68 | /// return true; 69 | /// }()); 70 | dynamic data; 71 | 72 | // 分页参数 字段,一般情况都是固定的,以防万一 73 | String? curPageField; 74 | 75 | // 数据列表 76 | List listData = []; 77 | 78 | // 当前的PageDataModel 79 | DM? pageDataModel; 80 | 81 | // 当前的PageViewModel 82 | VM? pageViewModel; 83 | 84 | PagingBehavior pagingBehavior = PagingBehavior.idle; 85 | 86 | PagingState pagingState = PagingState.idle; 87 | 88 | // 记录之前列表的长度 89 | int originalListDataLength = 0; 90 | 91 | PagingDataModel( 92 | {this.curPage = 1, 93 | this.pageCount = 0, 94 | this.total = 0, 95 | this.size = 0, 96 | this.data, 97 | this.curPageField = 'curPage', 98 | this.pageDataModel}) : 99 | listData = [], 100 | originalListDataLength = 0; 101 | 102 | /// 这两个方法,由 RefreshLoadWidget 组件调用 103 | 104 | /// 加载更多,追加数据 105 | Future loadListData() async { 106 | PagingState pagingState = PagingState.curLoading; 107 | pagingBehavior = PagingBehavior.load; 108 | Map? param = {curPageField!: ++curPage}; 109 | PageViewModel? currentPageViewModel = await pageViewModel?.requestData(params: param); 110 | if(currentPageViewModel?.pageDataModel?.type == NotifierResultType.success) { 111 | // 没有更多数据了 112 | if(currentPageViewModel?.pageDataModel?.total == listData.length || 113 | originalListDataLength == listData.length) { 114 | curPage = curPage > 1 ? --curPage : 1; 115 | pagingState = PagingState.loadNoData; 116 | } else { 117 | pagingState = PagingState.loadSuccess; 118 | } 119 | originalListDataLength = listData.length; 120 | } else { 121 | curPage = curPage > 1 ? --curPage : 1; 122 | pagingState = PagingState.loadFail; 123 | } 124 | return pagingState; 125 | } 126 | 127 | /// 下拉刷新数据 128 | Future refreshListData() async { 129 | PagingState pagingState = PagingState.curRefreshing; 130 | pagingBehavior = PagingBehavior.refresh; 131 | curPage = 1; 132 | listData.clear(); 133 | Map? param = {curPageField!: curPage}; 134 | PageViewModel? currentPageViewModel = await pageViewModel?.requestData(params: param); 135 | if(currentPageViewModel?.pageDataModel?.type == NotifierResultType.success) { 136 | originalListDataLength = listData.length; 137 | pagingState = PagingState.refreshSuccess; 138 | } else { 139 | originalListDataLength = 0; 140 | pagingState = PagingState.refreshFail; 141 | } 142 | 143 | return pagingState; 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /lib/common/repository/base_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_develop_template/common/mvvm/base_model.dart'; 5 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 6 | import 'package:flutter_develop_template/common/widget/notifier_widget.dart'; 7 | 8 | import '../net/dio_response.dart'; 9 | 10 | typedef JsonCoverEntity = T Function(Map json); 11 | 12 | class BaseRepository { 13 | 14 | /// 统一处理 响应数据,可以避免写 重复代码,但如果业务复杂,可能还是需要在原始写法上,扩展 15 | 16 | /// 普通页面(非分页)数据请求 统一处理 17 | Future httpPageRequest({ 18 | required PageViewModel pageViewModel, 19 | required Future Function() future, 20 | required JsonCoverEntity jsonCoverEntity, 21 | CancelToken? cancelToken, 22 | int curPage = 1, 23 | }) async { 24 | try { 25 | Response response = await future(); 26 | 27 | if (response.statusCode == REQUEST_SUCCESS) { 28 | /// 请求成功 29 | pageViewModel.pageDataModel?.type = NotifierResultType.success; 30 | 31 | /// ViewModel 和 Model 相互持有 32 | dynamic model = jsonCoverEntity(response.data); 33 | model.vm = pageViewModel; 34 | pageViewModel.pageDataModel?.data = model; 35 | } else { 36 | /// 请求成功,但业务不通过,比如没有权限 37 | pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized; 38 | pageViewModel.pageDataModel?.errorMsg = response.statusMessage; 39 | } 40 | } on DioException catch (dioEx) { 41 | /// 请求异常 42 | pageViewModel.pageDataModel?.type = NotifierResultType.dioError; 43 | pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx); 44 | } catch (e) { 45 | /// 未知异常 46 | pageViewModel.pageDataModel?.type = NotifierResultType.fail; 47 | pageViewModel.pageDataModel?.errorMsg = e.toString(); 48 | } 49 | 50 | return pageViewModel; 51 | } 52 | 53 | /// 分页数据请求 统一处理 54 | Future httpPagingRequest({ 55 | required PageViewModel pageViewModel, 56 | required Future Function() future, 57 | required JsonCoverEntity jsonCoverEntity, 58 | CancelToken? cancelToken, 59 | int curPage = 1, 60 | }) async { 61 | try { 62 | Response response = await future(); 63 | 64 | if (response.statusCode == REQUEST_SUCCESS) { 65 | /// 请求成功 66 | pageViewModel.pageDataModel?.type = NotifierResultType.success; 67 | 68 | /// 有分页 69 | pageViewModel.pageDataModel?.isPaging = true; 70 | 71 | /// 分页代码 72 | /// ViewModel 和 Model 相互持有代码,写着 correlationPaging() 里面 73 | pageViewModel.pageDataModel?.correlationPaging( 74 | pageViewModel, 75 | jsonCoverEntity(response.data) as dynamic, 76 | ); 77 | } else { 78 | /// 请求成功,但业务不通过,比如没有权限 79 | pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized; 80 | pageViewModel.pageDataModel?.errorMsg = response.statusMessage; 81 | } 82 | } on DioException catch (dioEx) { 83 | /// 请求异常 84 | pageViewModel.pageDataModel?.type = NotifierResultType.dioError; 85 | pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx); 86 | } catch (e) { 87 | /// 未知异常 88 | pageViewModel.pageDataModel?.type = NotifierResultType.fail; 89 | pageViewModel.pageDataModel?.errorMsg = e.toString(); 90 | } 91 | 92 | return pageViewModel; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /lib/common/util/global.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | import '../../res/style/color_styles.dart'; 5 | 6 | /// 状态栏样式 7 | SystemUiOverlayStyle _systemOverlayStyleForBrightness(Brightness brightness, 8 | [Color? backgroundColor]) { 9 | final SystemUiOverlayStyle style = brightness == Brightness.dark 10 | ? SystemUiOverlayStyle.light 11 | : SystemUiOverlayStyle.dark; 12 | // For backward compatibility, create an overlay style without system navigation bar settings. 13 | return SystemUiOverlayStyle( 14 | statusBarColor: backgroundColor, 15 | statusBarBrightness: style.statusBarBrightness, 16 | statusBarIconBrightness: style.statusBarIconBrightness, 17 | systemStatusBarContrastEnforced: style.systemStatusBarContrastEnforced, 18 | ); 19 | } 20 | 21 | /// 下面 注解 没写反!!!,给白色,图标/字体 实际情况是 黑色,反之 给黑色,图标/字体 是 白色 22 | 23 | /// 状态栏 背景透明,图标/字体 黑色 24 | SystemUiOverlayStyle get overlayBlackStyle => 25 | _systemOverlayStyleForBrightness( 26 | ThemeData.estimateBrightnessForColor(ColorStyles.color_FFFFFF), // 状态栏图标字体颜色 27 | ColorStyles.color_transparent, // 状态栏背景色 28 | ); 29 | 30 | /// 状态栏 背景透明,图标/字体 白色 31 | SystemUiOverlayStyle get overlayWhiteStyle => 32 | _systemOverlayStyleForBrightness( 33 | ThemeData.estimateBrightnessForColor(ColorStyles.color_000000), // 状态栏图标字体颜色 34 | ColorStyles.color_transparent, // 状态栏背景色 35 | ); -------------------------------------------------------------------------------- /lib/common/widget/global_notification_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 在执行全局操作后,所有继承 BaseStatefulPageState 的子页面, 4 | /// 都会执行 didChangeDependencies() 方法,然后执行 build() 方法 5 | /// 6 | /// 具体原理:是 InheritedWidget 的特性 7 | /// https://loveky.github.io/2018/07/18/how-flutter-inheritedwidget-works/ 8 | 9 | /// 全局操作类型 10 | enum GlobalOperate { 11 | /// 默认空闲 12 | idle, 13 | 14 | /// 切换登陆 15 | switchLogin, 16 | 17 | /// ... ... 18 | } 19 | 20 | /// 持有 全局操作状态 的 InheritedWidget 21 | class GlobalNotificationWidget extends InheritedWidget { 22 | GlobalNotificationWidget({ 23 | required this.globalOperate, 24 | required super.child}); 25 | 26 | final GlobalOperate globalOperate; 27 | 28 | static GlobalNotificationWidget? of(BuildContext context) { 29 | return context 30 | .dependOnInheritedWidgetOfExactType(); 31 | } 32 | 33 | /// 通知所有建立依赖的 子Widget 34 | @override 35 | bool updateShouldNotify(covariant GlobalNotificationWidget oldWidget) => 36 | oldWidget.globalOperate != globalOperate && 37 | globalOperate != GlobalOperate.idle; 38 | } 39 | 40 | /// 具体使用的 全局操作 Widget 41 | /// 42 | /// 执行全局操作: GlobalOperateProvider.runGlobalOperate(context: context, operate: GlobalOperate.switchLogin); 43 | /// 获取全局操作类型 GlobalOperateProvider.getGlobalOperate(context: context) 44 | class GlobalOperateProvider extends StatefulWidget { 45 | const GlobalOperateProvider({super.key, required this.child}); 46 | 47 | final Widget child; 48 | 49 | /// 执行全局操作 50 | static runGlobalOperate({ 51 | required BuildContext? context, 52 | required GlobalOperate operate, 53 | }) { 54 | context 55 | ?.findAncestorStateOfType<_GlobalOperateProviderState>() 56 | ?._runGlobalOperate(operate: operate); 57 | } 58 | 59 | /// 获取全局操作类型 60 | static GlobalOperate? getGlobalOperate({required BuildContext? context}) { 61 | return context 62 | ?.findAncestorStateOfType<_GlobalOperateProviderState>() 63 | ?.globalOperate; 64 | } 65 | 66 | @override 67 | State createState() => _GlobalOperateProviderState(); 68 | } 69 | 70 | class _GlobalOperateProviderState extends State { 71 | GlobalOperate globalOperate = GlobalOperate.idle; 72 | 73 | /// 执行全局操作 74 | _runGlobalOperate({required GlobalOperate operate}) { 75 | // 先重置 76 | globalOperate = GlobalOperate.idle; 77 | 78 | // 再赋值 79 | globalOperate = operate; 80 | 81 | /// 别忘了刷新,如果不刷新,子widget不会执行 didChangeDependencies 方法 82 | setState(() {}); 83 | } 84 | 85 | @override 86 | Widget build(BuildContext context) { 87 | return GlobalNotificationWidget( 88 | globalOperate: globalOperate, 89 | child: widget.child, 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/common/widget/notifier_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_develop_template/common/mvvm/base_change_notifier.dart'; 3 | import 'package:flutter_develop_template/common/paging/paging_data_model.dart'; 4 | import '../../res/string/str_common.dart'; 5 | import 'package:flutter_develop_template/main/application.dart'; 6 | 7 | import '../mvvm/base_view_model.dart'; 8 | import '../paging/base_paging_model.dart'; 9 | 10 | enum NotifierResultType { 11 | // 不检查 12 | notCheck, 13 | 14 | // 加载中 15 | loading, 16 | 17 | // 请求成功 18 | success, 19 | 20 | // 这种属于请求成功,但业务不通过,比如没有权限 21 | unauthorized, 22 | 23 | // 请求异常 24 | dioError, 25 | 26 | // 未知异常 27 | fail, 28 | } 29 | 30 | class PageDataModel extends BaseChangeNotifier { 31 | // 完整的数据 32 | T? data; 33 | 34 | // 列表数据 35 | L? datas; 36 | 37 | // 当前页码 38 | int? curPage; 39 | 40 | // 总共多少页 41 | int? pageCount; 42 | 43 | // 当前页 数据数量 44 | int? size; 45 | 46 | // 总共 数据数量 47 | int? total; 48 | 49 | PagingDataModel? pagingDataModel; 50 | 51 | NotifierResultType type; 52 | 53 | String? errorMsg; 54 | 55 | // 是否有分页 56 | bool isPaging; 57 | 58 | PageDataModel({ 59 | this.data, 60 | this.curPage = 1, 61 | this.pageCount = 0, 62 | this.size = 0, 63 | this.total = 0, 64 | this.pagingDataModel, 65 | this.type = NotifierResultType.loading, 66 | this.errorMsg, 67 | this.isPaging = false 68 | }); 69 | 70 | /// 分页代码 71 | 72 | /// 这块是在继承 PageViewModel类 中使用的 73 | /// 刷新/加载更多 后,重新赋值最新的 分页数据 74 | void bindingPaging( 75 | PageViewModel currentPageViewModel, 76 | PageDataModel originalPageDataModel, 77 | PagingDataModel originalPagingDataModel, 78 | PageViewModel originalPageViewModel, 79 | ) { 80 | originalPageDataModel.pagingDataModel = originalPagingDataModel; 81 | 82 | var list = (currentPageViewModel.pageDataModel?.data.datas as List?) ?? []; 83 | originalPagingDataModel 84 | ..pageCount = currentPageViewModel.pageDataModel?.pageCount ?? 0 85 | ..total = currentPageViewModel.pageDataModel?.total ?? 0 86 | ..size = currentPageViewModel.pageDataModel?.size ?? 0 87 | ..data = currentPageViewModel.pageDataModel 88 | ..listData.addAll(list) 89 | ..pageDataModel = originalPageDataModel 90 | ..pageViewModel = originalPageViewModel; 91 | } 92 | 93 | 94 | /// 这块是在 请求接口方法里 中使用的 95 | /// 将请求的数据 分页数据,赋值给 PageDataModel 96 | void correlationPaging( 97 | PageViewModel pageViewModel, 98 | BasePagingModel data, 99 | ) { 100 | 101 | /// ViewModel 和 Model 相互持有 102 | data.vm = pageViewModel; 103 | pageViewModel.pageDataModel?.data = data; 104 | 105 | pageViewModel.pageDataModel?.pageCount = data.pageCount ?? 0; 106 | pageViewModel.pageDataModel?.size = data.size ?? 0; 107 | pageViewModel.pageDataModel?.total = data.total ?? 0; 108 | } 109 | } 110 | 111 | typedef NotifierPageWidgetBuilder = Widget 112 | Function(BuildContext context, PageDataModel model); 113 | 114 | /// 这个是配合 PageDataModel 类使用的 115 | class NotifierPageWidget extends StatefulWidget { 116 | NotifierPageWidget({ 117 | super.key, 118 | required this.model, 119 | required this.builder, 120 | }); 121 | 122 | /// 需要监听的数据观察类 123 | final PageDataModel? model; 124 | 125 | final NotifierPageWidgetBuilder builder; 126 | 127 | @override 128 | _NotifierPageWidgetState createState() => _NotifierPageWidgetState(); 129 | } 130 | 131 | class _NotifierPageWidgetState 132 | extends State> { 133 | PageDataModel? model; 134 | 135 | /// 刷新UI 136 | refreshUI() => setState(() { 137 | model = widget.model; 138 | }); 139 | 140 | /// 对数据进行绑定监听 141 | @override 142 | void initState() { 143 | super.initState(); 144 | 145 | model = widget.model; 146 | 147 | // 先清空一次已注册的Listener,防止重复触发 148 | model?.removeListener(refreshUI); 149 | 150 | // 添加监听 151 | model?.addListener(refreshUI); 152 | } 153 | 154 | @override 155 | void didUpdateWidget(covariant NotifierPageWidget oldWidget) { 156 | super.didUpdateWidget(oldWidget); 157 | if (oldWidget.model != widget.model) { 158 | // 先清空一次已注册的Listener,防止重复触发 159 | oldWidget.model?.removeListener(refreshUI); 160 | 161 | model = widget.model; 162 | 163 | // 添加监听 164 | model?.addListener(refreshUI); 165 | } 166 | } 167 | 168 | @override 169 | Widget build(BuildContext context) { 170 | 171 | if (model?.type == NotifierResultType.notCheck) { 172 | return widget.builder(context, model!); 173 | } 174 | 175 | if (model?.type == NotifierResultType.loading) { 176 | return Center( 177 | child: Text(StrCommon.loading), 178 | ); 179 | } 180 | 181 | if (model?.type == NotifierResultType.success) { 182 | if (model?.data == null) { 183 | return Center( 184 | child: Text(StrCommon.dataIsEmpty), 185 | ); 186 | } 187 | if(model?.isPaging ?? false) { 188 | var lists = model?.data?.datas as List?; 189 | if(lists?.isEmpty ?? false){ 190 | return Center( 191 | child: Text(StrCommon.listDataIsEmpty), 192 | ); 193 | }; 194 | } 195 | return widget.builder(context, model!); 196 | } 197 | 198 | if (model?.type == NotifierResultType.unauthorized) { 199 | return Center( 200 | child: Text('${StrCommon.businessDoesNotPass}:${model?.errorMsg}'), 201 | ); 202 | } 203 | 204 | /// 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在, 205 | /// 但会阻断,后续代码执行,建议 非开发阶段 关闭 206 | if(EnvConfig.throwError) { 207 | throw Exception('${model?.errorMsg}'); 208 | } 209 | 210 | if (model?.type == NotifierResultType.dioError) { 211 | return Center( 212 | child: Text('${StrCommon.dioErrorAnomaly}:${model?.errorMsg}'), 213 | ); 214 | } 215 | 216 | if (model?.type == NotifierResultType.fail) { 217 | return Center( 218 | child: Text('${StrCommon.unknownAnomaly}:${model?.errorMsg}'), 219 | ); 220 | } 221 | 222 | return Center( 223 | child: Text('${StrCommon.pleaseService}:${model?.errorMsg}'), 224 | ); 225 | } 226 | 227 | @override 228 | void dispose() { 229 | widget.model?.removeListener(refreshUI); 230 | super.dispose(); 231 | } 232 | } 233 | 234 | typedef NotifierValueWidgetBuilder = Widget Function(BuildContext context, T? value); 235 | /// 这个是配合 继承 BaseChangeNotifier 类使用的 236 | class NotifierWidget extends StatefulWidget { 237 | const NotifierWidget({ 238 | super.key, 239 | required this.data, 240 | required this.builder, 241 | }); 242 | 243 | /// 需要监听的数据观察类 244 | final T? data; 245 | 246 | final Widget Function(BuildContext, T?) builder; 247 | 248 | @override 249 | State> createState() => _NotifierWidgetState(); 250 | } 251 | 252 | class _NotifierWidgetState 253 | extends State> { 254 | T? data; 255 | 256 | /// 刷新UI 257 | refreshState() => setState(() { 258 | data = widget.data; 259 | }); 260 | 261 | /// 对数据进行绑定监听 262 | @override 263 | void initState() { 264 | super.initState(); 265 | 266 | data = widget.data; 267 | 268 | // 先清空一次已注册的Listener,防止重复触发 269 | data?.removeListener(refreshState); 270 | 271 | // 添加监听 272 | data?.addListener(refreshState); 273 | } 274 | 275 | @override 276 | void didUpdateWidget(covariant NotifierWidget oldWidget) { 277 | super.didUpdateWidget(oldWidget); 278 | if (oldWidget.data != widget.data) { 279 | // 先清空一次已注册的Listener,防止重复触发 280 | oldWidget.data?.removeListener(refreshState); 281 | 282 | data = widget.data; 283 | 284 | // 添加监听 285 | data?.addListener(refreshState); 286 | } 287 | } 288 | 289 | @override 290 | Widget build(BuildContext context) { 291 | if (data == null) { 292 | return Center( 293 | child: Text(StrCommon.loading), 294 | ); 295 | } 296 | return widget.builder(context, data); 297 | } 298 | 299 | @override 300 | void dispose() { 301 | widget.data?.removeListener(refreshState); 302 | super.dispose(); 303 | } 304 | } -------------------------------------------------------------------------------- /lib/common/widget/refresh_load_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_develop_template/common/paging/paging_data_model.dart'; 3 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 4 | 5 | import '../../res/string/str_common.dart'; 6 | 7 | /// 封装 下拉刷新/上拉加载更多 组件 8 | class RefreshLoadWidget extends StatefulWidget { 9 | const RefreshLoadWidget({ 10 | super.key, 11 | required this.pagingDataModel, 12 | required this.scrollView, 13 | this.header, 14 | this.footer, 15 | this.enablePullDown = true, 16 | this.enablePullUp = true, 17 | }); 18 | 19 | final PagingDataModel pagingDataModel; 20 | final ScrollView scrollView; 21 | final Widget? header; 22 | final Widget? footer; 23 | 24 | // 开启/关闭 下拉刷新 25 | final bool enablePullDown; 26 | 27 | // 开启/关闭 上拉加载 28 | final bool enablePullUp; 29 | 30 | @override 31 | State createState() => _RefreshLoadWidgetState(); 32 | } 33 | 34 | class _RefreshLoadWidgetState extends State { 35 | late RefreshController refreshController; 36 | 37 | @override 38 | void initState() { 39 | super.initState(); 40 | refreshController = RefreshController(); 41 | } 42 | 43 | @override 44 | void dispose() { 45 | refreshController.dispose(); 46 | super.dispose(); 47 | } 48 | 49 | /// 下拉刷新 50 | onRefresh() async { 51 | // 正在刷新 52 | if (widget.pagingDataModel.pagingState == PagingState.curRefreshing) { 53 | return; 54 | } 55 | 56 | PagingState resultType = await widget.pagingDataModel.refreshListData(); 57 | if (resultType == PagingState.refreshSuccess) { 58 | refreshController.refreshCompleted(); 59 | } else { 60 | refreshController.refreshFailed(); 61 | } 62 | 63 | // 重置空闲状态 64 | widget.pagingDataModel.pagingState = PagingState.idle; 65 | widget.pagingDataModel.pagingBehavior = PagingBehavior.idle; 66 | } 67 | 68 | /// 上拉加载 69 | onLoading() async { 70 | // 正在加载 71 | if (widget.pagingDataModel.pagingState == PagingState.curLoading) { 72 | return; 73 | } 74 | 75 | PagingState resultType = await widget.pagingDataModel.loadListData(); 76 | if (resultType == PagingState.loadSuccess) { 77 | refreshController.loadComplete(); 78 | } else if (resultType == PagingState.loadNoData) { 79 | refreshController.loadNoData(); 80 | } else { 81 | refreshController.loadFailed(); 82 | } 83 | 84 | // 重置空闲状态 85 | widget.pagingDataModel.pagingState = PagingState.idle; 86 | widget.pagingDataModel.pagingBehavior = PagingBehavior.idle; 87 | } 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | return SmartRefresher( 92 | controller: refreshController, 93 | // 下拉刷新 94 | enablePullDown: true, 95 | // 上拉加载 96 | enablePullUp: true, 97 | header: widget.header ?? 98 | ClassicHeader( 99 | idleText: StrCommon.pullDownToRefresh, 100 | refreshingText: StrCommon.refreshing, 101 | completeText: StrCommon.loadedSuccess, 102 | releaseText: StrCommon.releaseToRefreshImmediately, 103 | failedText: StrCommon.refreshFailed, 104 | ), 105 | footer: widget.footer ?? 106 | ClassicFooter( 107 | idleText: StrCommon.pullUpLoad, 108 | loadingText: StrCommon.loading, 109 | canLoadingText: StrCommon.letGoLoading, 110 | failedText: StrCommon.failedLoad, 111 | noDataText: StrCommon.noMoreData, 112 | // 没有内容的文字 113 | noMoreIcon: Icon(Icons.data_array), // 没有内容的图标 114 | ), 115 | onRefresh: onRefresh, 116 | onLoading: onLoading, 117 | child: widget.scrollView, 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_develop_template/main/application.dart'; 2 | 3 | /// 生产环境 入口函数 4 | void main() => Application.runApplication( 5 | envTag: EnvTag.prod, // 生产环境 6 | platform: ApplicationPlatform.app, // 手机应用 7 | isGlobalNotification: true, // 是否有全局通知操作,比如切换用户 8 | baseUrl: 'https://www.wanandroid.com/', // 域名 9 | ); 10 | -------------------------------------------------------------------------------- /lib/main/app.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_develop_template/common/mvvm/base_page.dart'; 5 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 6 | import '../../router/page_route_observer.dart'; 7 | import '../../router/routers.dart'; 8 | import 'package:flutter_develop_template/common/widget/global_notification_widget.dart'; 9 | import 'package:flutter_develop_template/module/message/view/message_v.dart'; 10 | 11 | import '../../res/style/color_styles.dart'; 12 | import '../../res/style/theme_styles.dart'; 13 | import '../module/home/view/home_v.dart'; 14 | import '../module/order/view/order_v.dart'; 15 | import '../module/personal/view/personal_v.dart'; 16 | 17 | /// App初始化的第一个页面可能是 其他页面,比如 广告、引导页、登陆页面 18 | enum AppInitState { 19 | /// 是App主体页面 20 | isAppMainPage, 21 | 22 | /// 不是主体页面 23 | noAppMainPage 24 | } 25 | 26 | /// 全局key 27 | /// 获取全局context方式:navigatorKey.currentContext 28 | final GlobalKey navigatorKey = GlobalKey(); 29 | 30 | /// 这个对象 可以获取当前设配信息 31 | MediaQueryData? media; 32 | 33 | /// 监听全局路由,比如获取 当前路由栈里 页面总数 34 | PageRouteObserver? pageRouteObserver; 35 | 36 | class App extends StatelessWidget { 37 | const App({super.key}); 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | 42 | /// 初始化 MediaQuery、PageRouteObserver 43 | media ??= MediaQuery.of(context); 44 | pageRouteObserver ??= PageRouteObserver(); 45 | 46 | return GlobalOperateProvider( 47 | child: MaterialApp( 48 | title: 'Flutter Demo', 49 | debugShowCheckedModeBanner: false, 50 | navigatorObservers: [pageRouteObserver!], 51 | // 全局key 52 | navigatorKey: navigatorKey, 53 | // 使用路由找不到页面时,就会执行 onGenerateRoute 54 | onGenerateRoute: Routers.router.generator, 55 | theme: ThemeStyles.defaultTheme, 56 | // PopScope:监听返回键 57 | home: PopScope( 58 | // WillPopScope 3.16中过时,使用PopScope替换 59 | canPop: false, 60 | onPopInvoked: (bool didPop) { 61 | if (didPop) { 62 | return; 63 | } 64 | 65 | // 这里使用 Navigator.of(context).pop(); 是无效的 66 | // 使用SystemNavigator.pop() 或者 exit() 67 | 68 | // SystemNavigator.pop():用于在导航堆栈中弹出最顶层的页面,并在导航堆栈为空时退出应用程序 69 | // 平台兼容性:如果你只用Flutter做Android应用,优先使用 SystemNavigator.pop() 来退出应用程序。 70 | 71 | // exit():直接终止应用程序的运行。 72 | // 平台兼容性:适用于所有平台(Android、iOS等) 73 | 74 | // 写逻辑代码 75 | // ... 76 | 77 | exit(0); // 退出应用 78 | }, 79 | child: AppTransfer( 80 | initState: AppInitState.isAppMainPage, 81 | )), 82 | ), 83 | ); 84 | } 85 | } 86 | 87 | /// 这个是用来中转的,比如初始化第一个启动的页面 可能是 广告、引导页、登陆页面,之后再从这些页面进入 App主体页面 88 | class AppTransfer extends StatelessWidget { 89 | const AppTransfer({super.key, required this.initState}); 90 | 91 | final AppInitState initState; 92 | 93 | @override 94 | Widget build(BuildContext context) { 95 | Widget child; // MaterialApp 96 | switch (initState) { 97 | case AppInitState.isAppMainPage: 98 | { 99 | // 先判断是否登陆 100 | // ... ... 101 | child = AppMainPage(); 102 | } 103 | break; 104 | default: 105 | // 进入 广告、引导页 等等,再从这些页面进入 App首页 106 | child = AppMainPage(); 107 | } 108 | return child; 109 | } 110 | } 111 | 112 | /// 这是App主体页面,主要是 PageView + BottomNavigationBar 113 | class AppMainPage extends BaseStatefulPage { 114 | AppMainPage({super.key}); 115 | 116 | @override 117 | AppMainPageState createState() => AppMainPageState(); 118 | } 119 | 120 | class AppMainPageState extends BaseStatefulPageState { 121 | 122 | PageController? pageController; 123 | 124 | @override 125 | void initAttribute() { 126 | pageController ??= PageController( 127 | initialPage: 0, 128 | keepPage: true, 129 | ); 130 | } 131 | 132 | @override 133 | void initObserver() { 134 | 135 | } 136 | 137 | @override 138 | AppMainPageViewModel viewBindingViewModel() { 139 | /// ViewModel 和 View 相互持有 140 | return AppMainPageViewModel()..viewState = this; 141 | } 142 | 143 | @override 144 | void dispose() { 145 | pageController?.dispose(); 146 | super.dispose(); 147 | } 148 | 149 | void pageChanged(int index) { 150 | bottomSelectedIndex = index; 151 | } 152 | 153 | void bottomTap(int index) { 154 | bottomSelectedIndex = index; 155 | pageController?.jumpToPage(index); 156 | setState(() {}); 157 | } 158 | 159 | int bottomSelectedIndex = 0; 160 | 161 | List buildBottomNavBarItems() { 162 | return [ 163 | BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), 164 | BottomNavigationBarItem(icon: Icon(Icons.message), label: 'Message'), 165 | BottomNavigationBarItem(icon: Icon(Icons.add_chart), label: 'Order'), 166 | BottomNavigationBarItem(icon: Icon(Icons.person_rounded), label: 'Personal'), 167 | ]; 168 | } 169 | 170 | Widget buildPageView() { 171 | return PageView( 172 | physics: NeverScrollableScrollPhysics(), // 禁止滑动 173 | controller: pageController, 174 | onPageChanged: pageChanged, 175 | children: [ 176 | HomeView(), 177 | MessageView(), 178 | OrderView(), 179 | PersonalView(), 180 | ], 181 | ); 182 | } 183 | 184 | @override 185 | Widget appBuild(BuildContext context) { 186 | return Scaffold( 187 | body: buildPageView(), 188 | bottomNavigationBar: BottomNavigationBar( 189 | backgroundColor: ColorStyles.color_FFFFFF, 190 | type: BottomNavigationBarType.fixed, // 自适应宽度,但同时会失去,图标/文字 缩放效果 191 | currentIndex: bottomSelectedIndex, 192 | onTap: bottomTap, 193 | items: buildBottomNavBarItems(), 194 | unselectedItemColor: ColorStyles.color_1E88E5, // 未选中状态下的颜色 195 | unselectedFontSize: 14, // 未选中状态下的字体大小 196 | selectedItemColor: ColorStyles.color_EA5034, // 选中状态下的颜色 197 | selectedFontSize: 14, // 选中状态下的字体大小 198 | ), 199 | ); 200 | } 201 | 202 | } 203 | 204 | class AppMainPageViewModel extends PageViewModel { 205 | 206 | @override 207 | onCreate() { 208 | 209 | } 210 | 211 | @override 212 | Future requestData({Map? params}) { 213 | return Future.value(null); 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /lib/main/application.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:sentry_flutter/sentry_flutter.dart'; 5 | import '../../res/string/str_common.dart'; 6 | 7 | import '../../router/routers.dart'; 8 | import 'app.dart'; 9 | 10 | enum ApplicationPlatform { 11 | /// 手机应用 12 | app, 13 | 14 | /// Web 15 | web, 16 | 17 | /// PC应用 18 | pc 19 | } 20 | 21 | enum EnvTag { 22 | /// 开发 23 | dev, 24 | 25 | /// 预发 26 | pre, 27 | 28 | /// 生产 29 | prod, 30 | } 31 | 32 | /// 环境配置实体 33 | class EnvConfig { 34 | /// 域名 35 | static String baseUrl = ''; 36 | 37 | /// 开发环境 38 | static EnvTag envTag = EnvTag.dev; 39 | 40 | /// 是否开启抓包 41 | static bool proxyEnable = false; 42 | 43 | /// 抓包工具的代理地址 + 端口 44 | static String? caughtAddress; 45 | 46 | /// 平台 47 | static ApplicationPlatform platform = ApplicationPlatform.app; 48 | 49 | /// 是否有全局通知操作,比如切换用户 50 | static bool isGlobalNotification = false; 51 | 52 | /// 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在 53 | /// 但会阻断,后续代码执行,建议 非开发阶段 关闭 54 | static bool throwError = false; 55 | 56 | /// 是否开启 异常上报到 Sentry 57 | static bool pushErrToSentry = false; 58 | 59 | /// Sentry DNS 标识 60 | static String? sentryDNS; 61 | } 62 | 63 | class Application { 64 | Application.runApplication( 65 | {required EnvTag envTag, // 开发环境 66 | required String baseUrl, // 域名 67 | required ApplicationPlatform platform, // 平台 68 | bool proxyEnable = false, // 是否开启抓包 69 | String? caughtAddress, // 抓包工具的代理地址 + 端口 70 | bool isGlobalNotification = false, // 是否有全局通知操作,比如切换用户 71 | bool throwError = false, // 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在,但会阻断,后续代码执行 72 | bool pushErrToSentry = false, // 是否开启 异常上报到 Sentry 73 | String? sentryDNS // Sentry DNS 标识 74 | }) { 75 | EnvConfig.envTag = envTag; 76 | EnvConfig.baseUrl = baseUrl; 77 | EnvConfig.platform = platform; 78 | EnvConfig.proxyEnable = proxyEnable; 79 | EnvConfig.caughtAddress = caughtAddress; 80 | EnvConfig.isGlobalNotification = isGlobalNotification; 81 | EnvConfig.throwError = throwError; 82 | EnvConfig.pushErrToSentry = pushErrToSentry; 83 | EnvConfig.sentryDNS = sentryDNS; 84 | 85 | /// 确保一些依赖,全部初始化 86 | WidgetsFlutterBinding.ensureInitialized(); 87 | 88 | /// 初始化路由 89 | Routers.configureRouters(); 90 | 91 | /// Flutter 框架中 Widget显示错误时,替换为当前组件 92 | ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails) { 93 | return Material( 94 | child: Center( 95 | child: EnvConfig.envTag == EnvTag.dev 96 | ? Text(flutterErrorDetails.exceptionAsString()) 97 | : Text(StrCommon.pleaseService), 98 | ), 99 | ); 100 | }; 101 | 102 | /// FlutterError.onError 是 Flutter 提供的一个全局错误处理回调, 103 | /// 用于捕获框架内发生的未处理异常。无论是同步还是异步异常, 104 | /// 只要它们发生在 Flutter 框架的上下文中,都会触发这个回调。 105 | FlutterError.onError = (FlutterErrorDetails flutterErrorDetails) async { 106 | if (EnvConfig.pushErrToSentry) { 107 | 108 | assert((){ 109 | debugPrint('执行了异常 上报:${flutterErrorDetails.exception}'); 110 | return true; 111 | }()); 112 | 113 | /// 使用第三方服务(例如Sentry)上报错误 114 | /// Sentry.captureException(error, stackTrace: stackTrace); 115 | await Sentry.captureException( 116 | flutterErrorDetails.exception, 117 | stackTrace: flutterErrorDetails.stack, 118 | ); 119 | } 120 | }; 121 | 122 | /// runZonedGuarded 捕获 Flutter 框架中的 异步异常 123 | /// 注意:它不是全局的,只能捕获指定范围 124 | runZonedGuarded(() async { 125 | if (EnvConfig.pushErrToSentry) { 126 | /// 初始化 Sentry 127 | await SentryFlutter.init( 128 | (options) { 129 | options.dsn = EnvConfig.sentryDNS; 130 | options.tracesSampleRate = 1.0; 131 | 132 | if (EnvConfig.envTag == EnvTag.dev) { 133 | /// 是否打印输出 Sentry 日志 134 | options.debug = true; 135 | } 136 | }, 137 | appRunner: () => runApp(App()), 138 | ); 139 | } else { 140 | runApp(App()); 141 | } 142 | }, (Object error, StackTrace stack) async { 143 | if (EnvConfig.pushErrToSentry) { 144 | 145 | assert((){ 146 | debugPrint('执行了异常 上报:$error'); 147 | return true; 148 | }()); 149 | 150 | /// 使用第三方服务(例如Sentry)上报错误 151 | /// Sentry.captureException(error, stackTrace: stackTrace); 152 | await Sentry.captureException( 153 | error, 154 | stackTrace: stack, 155 | ); 156 | } 157 | }); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /lib/main/main_dev.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_develop_template/main/application.dart'; 2 | 3 | /// 开发环境 入口函数 4 | void main() => Application.runApplication( 5 | envTag: EnvTag.dev, // 开发环境 6 | platform: ApplicationPlatform.app, // 手机应用 7 | baseUrl: 'https://www.wanandroid.com/', // 域名 8 | proxyEnable: false, // 是否开启抓包 9 | caughtAddress: '192.168.1.3:8888', // 抓包工具的代理地址 + 端口 10 | isGlobalNotification: true, // 是否有全局通知操作,比如切换用户 11 | /// 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在, 12 | /// 但会阻断,后续代码执行,建议 非开发阶段 关闭 13 | throwError: true, 14 | pushErrToSentry: false, // 是否开启 异常上报到 Sentry 15 | sentryDNS: 'https://123456789191111@xxx.com/2' // Sentry DNS 标识 16 | 17 | /// AndroidManifest.xml 中 18 | /// 开启网络权限 19 | /// 20 | /// 21 | /// 如果使用 http,还需要配置 22 | /// Application.runApplication( 5 | envTag: EnvTag.pre, // 预发布环境 6 | platform: ApplicationPlatform.app, // 手机应用 7 | isGlobalNotification: true, // 是否有全局通知操作,比如切换用户 8 | baseUrl: 'https://www.wanandroid.com/', // 域名 9 | ); 10 | -------------------------------------------------------------------------------- /lib/module/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/lib/module/.DS_Store -------------------------------------------------------------------------------- /lib/module/home/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/lib/module/home/.DS_Store -------------------------------------------------------------------------------- /lib/module/home/api/home_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 5 | import 'package:flutter_develop_template/common/net/dio_client.dart'; 6 | 7 | import '../../../common/repository/base_repository.dart'; 8 | import '../model/home_list_m.dart'; 9 | 10 | class HomeRepository extends BaseRepository { 11 | /// 获取首页数据 12 | Future getHomeData({ 13 | required PageViewModel pageViewModel, 14 | CancelToken? cancelToken, 15 | int curPage = 1, 16 | }) async => 17 | httpPageRequest( 18 | pageViewModel: pageViewModel, 19 | jsonCoverEntity: HomeListModel.fromJson, 20 | future: () => DioClient().doGet('project/list/$curPage/json?cid=294', cancelToken: cancelToken), 21 | cancelToken: cancelToken, 22 | curPage: curPage); 23 | 24 | /// 这是不使用 httpPageRequest 的原始写法,如果业务复杂,可能还是需要在原始写法上,扩展 25 | /// 获取首页数据 26 | // Future getHomeData({ 27 | // required PageViewModel pageViewModel, 28 | // CancelToken? cancelToken, 29 | // int curPage = 1, 30 | // }) async { 31 | // try { 32 | // Response response = await DioClient().doGet( 33 | // 'project/list/$curPage/json?cid=294', 34 | // cancelToken: cancelToken); 35 | // 36 | // if (response.statusCode == REQUEST_SUCCESS) { 37 | // /// 请求成功 38 | // pageViewModel.pageDataModel?.type = NotifierResultType.success; 39 | // 40 | // /// ViewModel 和 Model 相互持有 41 | // HomeListModel model = HomeListModel.fromJson(response.data); 42 | // model.vm = pageViewModel; 43 | // pageViewModel.pageDataModel?.data = model; 44 | // } else { 45 | // /// 请求成功,但业务不通过,比如没有权限 46 | // pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized; 47 | // pageViewModel.pageDataModel?.errorMsg = response.statusMessage; 48 | // } 49 | // } on DioException catch (dioEx) { 50 | // /// 请求异常 51 | // pageViewModel.pageDataModel?.type = NotifierResultType.dioError; 52 | // pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx); 53 | // } catch (e) { 54 | // /// 未知异常 55 | // pageViewModel.pageDataModel?.type = NotifierResultType.fail; 56 | // pageViewModel.pageDataModel?.errorMsg = e.toString(); 57 | // } 58 | // 59 | // return pageViewModel; 60 | // } 61 | } 62 | -------------------------------------------------------------------------------- /lib/module/home/model/home_list_m.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_develop_template/common/mvvm/base_model.dart'; 3 | 4 | class HomeListModel extends BaseModel { 5 | int? curPage; 6 | List? datas; 7 | int? offset; 8 | bool? over; 9 | int? pageCount; 10 | int? size; 11 | int? total; 12 | 13 | ValueNotifier tapNum = ValueNotifier(0); // 点击次数 14 | 15 | @override 16 | void onDispose() { 17 | tapNum.dispose(); 18 | super.onDispose(); 19 | } 20 | 21 | HomeListModel( 22 | {this.curPage, 23 | this.datas, 24 | this.offset, 25 | this.over, 26 | this.pageCount, 27 | this.size, 28 | this.total}); 29 | 30 | HomeListModel.fromJson(Map json) { 31 | curPage = json['curPage']; 32 | if (json['datas'] != null) { 33 | datas = []; 34 | json['datas'].forEach((v) { 35 | datas!.add(new Datas.fromJson(v)); 36 | }); 37 | } 38 | offset = json['offset']; 39 | over = json['over']; 40 | pageCount = json['pageCount']; 41 | size = json['size']; 42 | total = json['total']; 43 | } 44 | 45 | Map toJson() { 46 | final Map data = new Map(); 47 | data['curPage'] = this.curPage; 48 | if (this.datas != null) { 49 | data['datas'] = this.datas!.map((v) => v.toJson()).toList(); 50 | } 51 | data['offset'] = this.offset; 52 | data['over'] = this.over; 53 | data['pageCount'] = this.pageCount; 54 | data['size'] = this.size; 55 | data['total'] = this.total; 56 | return data; 57 | } 58 | } 59 | 60 | class Datas { 61 | bool? adminAdd; 62 | String? apkLink; 63 | int? audit; 64 | String? author; 65 | bool? canEdit; 66 | int? chapterId; 67 | String? chapterName; 68 | bool? collect; 69 | int? courseId; 70 | String? desc; 71 | String? descMd; 72 | String? envelopePic; 73 | bool? fresh; 74 | String? host; 75 | int? id; 76 | bool? isAdminAdd; 77 | String? link; 78 | String? niceDate; 79 | String? niceShareDate; 80 | String? origin; 81 | String? prefix; 82 | String? projectLink; 83 | int? publishTime; 84 | int? realSuperChapterId; 85 | int? selfVisible; 86 | int? shareDate; 87 | String? shareUser; 88 | int? superChapterId; 89 | String? superChapterName; 90 | List? tags; 91 | String? title; 92 | int? type; 93 | int? userId; 94 | int? visible; 95 | int? zan; 96 | 97 | Datas( 98 | {this.adminAdd, 99 | this.apkLink, 100 | this.audit, 101 | this.author, 102 | this.canEdit, 103 | this.chapterId, 104 | this.chapterName, 105 | this.collect, 106 | this.courseId, 107 | this.desc, 108 | this.descMd, 109 | this.envelopePic, 110 | this.fresh, 111 | this.host, 112 | this.id, 113 | this.isAdminAdd, 114 | this.link, 115 | this.niceDate, 116 | this.niceShareDate, 117 | this.origin, 118 | this.prefix, 119 | this.projectLink, 120 | this.publishTime, 121 | this.realSuperChapterId, 122 | this.selfVisible, 123 | this.shareDate, 124 | this.shareUser, 125 | this.superChapterId, 126 | this.superChapterName, 127 | this.tags, 128 | this.title, 129 | this.type, 130 | this.userId, 131 | this.visible, 132 | this.zan}); 133 | 134 | Datas.fromJson(Map json) { 135 | adminAdd = json['adminAdd']; 136 | apkLink = json['apkLink']; 137 | audit = json['audit']; 138 | author = json['author']; 139 | canEdit = json['canEdit']; 140 | chapterId = json['chapterId']; 141 | chapterName = json['chapterName']; 142 | collect = json['collect']; 143 | courseId = json['courseId']; 144 | desc = json['desc']; 145 | descMd = json['descMd']; 146 | envelopePic = json['envelopePic']; 147 | fresh = json['fresh']; 148 | host = json['host']; 149 | id = json['id']; 150 | isAdminAdd = json['isAdminAdd']; 151 | link = json['link']; 152 | niceDate = json['niceDate']; 153 | niceShareDate = json['niceShareDate']; 154 | origin = json['origin']; 155 | prefix = json['prefix']; 156 | projectLink = json['projectLink']; 157 | publishTime = json['publishTime']; 158 | realSuperChapterId = json['realSuperChapterId']; 159 | selfVisible = json['selfVisible']; 160 | shareDate = json['shareDate']; 161 | shareUser = json['shareUser']; 162 | superChapterId = json['superChapterId']; 163 | superChapterName = json['superChapterName']; 164 | if (json['tags'] != null) { 165 | tags = []; 166 | json['tags'].forEach((v) { 167 | tags!.add(new Tags.fromJson(v)); 168 | }); 169 | } 170 | title = json['title']; 171 | type = json['type']; 172 | userId = json['userId']; 173 | visible = json['visible']; 174 | zan = json['zan']; 175 | } 176 | 177 | Map toJson() { 178 | final Map data = new Map(); 179 | data['adminAdd'] = this.adminAdd; 180 | data['apkLink'] = this.apkLink; 181 | data['audit'] = this.audit; 182 | data['author'] = this.author; 183 | data['canEdit'] = this.canEdit; 184 | data['chapterId'] = this.chapterId; 185 | data['chapterName'] = this.chapterName; 186 | data['collect'] = this.collect; 187 | data['courseId'] = this.courseId; 188 | data['desc'] = this.desc; 189 | data['descMd'] = this.descMd; 190 | data['envelopePic'] = this.envelopePic; 191 | data['fresh'] = this.fresh; 192 | data['host'] = this.host; 193 | data['id'] = this.id; 194 | data['isAdminAdd'] = this.isAdminAdd; 195 | data['link'] = this.link; 196 | data['niceDate'] = this.niceDate; 197 | data['niceShareDate'] = this.niceShareDate; 198 | data['origin'] = this.origin; 199 | data['prefix'] = this.prefix; 200 | data['projectLink'] = this.projectLink; 201 | data['publishTime'] = this.publishTime; 202 | data['realSuperChapterId'] = this.realSuperChapterId; 203 | data['selfVisible'] = this.selfVisible; 204 | data['shareDate'] = this.shareDate; 205 | data['shareUser'] = this.shareUser; 206 | data['superChapterId'] = this.superChapterId; 207 | data['superChapterName'] = this.superChapterName; 208 | if (this.tags != null) { 209 | data['tags'] = this.tags!.map((v) => v.toJson()).toList(); 210 | } 211 | data['title'] = this.title; 212 | data['type'] = this.type; 213 | data['userId'] = this.userId; 214 | data['visible'] = this.visible; 215 | data['zan'] = this.zan; 216 | return data; 217 | } 218 | } 219 | 220 | class Tags { 221 | String? name; 222 | String? url; 223 | 224 | Tags({this.name, this.url}); 225 | 226 | Tags.fromJson(Map json) { 227 | name = json['name']; 228 | url = json['url']; 229 | } 230 | 231 | Map toJson() { 232 | final Map data = new Map(); 233 | data['name'] = this.name; 234 | data['url'] = this.url; 235 | return data; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /lib/module/home/view/home_v.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_develop_template/common/mvvm/base_page.dart'; 5 | import '../../../../res/string/str_home.dart'; 6 | import 'package:flutter_develop_template/common/widget/notifier_widget.dart'; 7 | import 'package:flutter_develop_template/module/home/view_model/home_vm.dart'; 8 | 9 | import '../../../../res/string/str_common.dart'; 10 | import '../../../../res/style/color_styles.dart'; 11 | import '../../../../res/style/text_styles.dart'; 12 | import '../../../common/widget/global_notification_widget.dart'; 13 | import '../model/home_list_m.dart'; 14 | 15 | class HomeView extends BaseStatefulPage { 16 | HomeView({super.key}); 17 | 18 | @override 19 | HomeViewState createState() => HomeViewState(); 20 | } 21 | 22 | class HomeViewState extends BaseStatefulPageState { 23 | 24 | @override 25 | HomeViewModel viewBindingViewModel() { 26 | /// ViewModel 和 View 相互持有 27 | return HomeViewModel()..viewState = this; 28 | } 29 | 30 | @override 31 | void initAttribute() {} 32 | 33 | @override 34 | void initObserver() {} 35 | 36 | @override 37 | void dispose() { 38 | assert((){ 39 | debugPrint('HomeView.onDispose()'); 40 | return true; 41 | }()); 42 | 43 | /// BaseStatefulPageState的子类,重写 dispose() 44 | /// 一定要执行父类 dispose(),防止内存泄漏 45 | super.dispose(); 46 | } 47 | 48 | bool executeSwitchLogin = false; 49 | 50 | @override 51 | void didChangeDependencies() { 52 | var operate = GlobalOperateProvider.getGlobalOperate(context: context); 53 | 54 | assert((){ 55 | debugPrint('HomeView.didChangeDependencies --- $operate'); 56 | return true; 57 | }()); 58 | 59 | // 切换用户 60 | // 正常业务流程是:从本地存储,拿到当前最新的用户ID,请求接口,我这里偷了个懒 😄 61 | // 直接使用随机数,模拟 不同用户ID 62 | if (operate == GlobalOperate.switchLogin) { 63 | executeSwitchLogin = true; 64 | 65 | // 重新请求数据 66 | // 如果你想刷新的时候,显示loading,加上这两行 67 | viewModel?.pageDataModel?.type = NotifierResultType.loading; 68 | viewModel?.pageDataModel?.refreshState(); 69 | 70 | viewModel?.requestData(params: {'curPage': Random().nextInt(20)}); 71 | } 72 | } 73 | 74 | ValueNotifier tapNum = ValueNotifier(0); 75 | 76 | @override 77 | Widget appBuild(BuildContext context) { 78 | return Scaffold( 79 | appBar: AppBar( 80 | backgroundColor: AppBarTheme.of(context).backgroundColor, 81 | /// 局部刷新 82 | title: ValueListenableBuilder( 83 | valueListenable: tapNum, 84 | builder: (context, value, _) { 85 | return Text( 86 | '${StrHome.home}:$value', 87 | style: TextStyles.style_222222_20, 88 | ); 89 | }, 90 | ), 91 | actions: [ 92 | IconButton( 93 | onPressed: () { 94 | tapNum.value += 1; 95 | }, 96 | icon: Icon(Icons.add)), 97 | IconButton( 98 | onPressed: () { 99 | viewModel?.requestData(params: {'curPage': Random().nextInt(20)}); 100 | }, 101 | icon: Icon(Icons.refresh)), 102 | IconButton( 103 | onPressed: () { 104 | // 如果你想刷新的时候,显示loading,加上这两行 105 | viewModel?.pageDataModel?.type = NotifierResultType.loading; 106 | viewModel?.pageDataModel?.refreshState(); 107 | 108 | viewModel?.requestData(params: {'curPage': Random().nextInt(20)}); 109 | }, 110 | icon: Icon(Icons.refresh_sharp)) 111 | ], 112 | ), 113 | body: NotifierPageWidget( 114 | model: viewModel?.pageDataModel, 115 | builder: (context, dataModel) { 116 | final data = dataModel.data as HomeListModel?; 117 | if(data != null) { 118 | /// 延迟一帧 119 | WidgetsBinding.instance.addPostFrameCallback((_){ 120 | /// 赋值、并替换 HomeListModel 内的tapNum,建立联系 121 | tapNum.value = data.pageCount ?? 0; 122 | data.tapNum = tapNum; 123 | }); 124 | } 125 | return Stack( 126 | children: [ 127 | ListView.builder( 128 | padding: EdgeInsets.zero, 129 | itemCount: data?.datas?.length ?? 0, 130 | itemBuilder: (context, index) { 131 | return Container( 132 | width: MediaQuery.of(context).size.width, 133 | height: 50, 134 | alignment: Alignment.center, 135 | child: Text('${data?.datas?[index].title}'), 136 | ); 137 | }), 138 | Container( 139 | color: ColorStyles.color_388E3C, 140 | child: executeSwitchLogin 141 | ? Row( 142 | mainAxisAlignment: MainAxisAlignment.center, 143 | children: [ 144 | Text(StrCommon.executeSwitchUser), 145 | IconButton( 146 | onPressed: () { 147 | executeSwitchLogin = false; 148 | setState(() {}); 149 | }, 150 | icon: Icon(Icons.close)) 151 | ], 152 | ) 153 | : SizedBox(), 154 | ), 155 | ], 156 | ); 157 | } 158 | ), 159 | ); 160 | } 161 | 162 | /// 是否保存页面状态 163 | @override 164 | bool get wantKeepAlive => true; 165 | 166 | } 167 | -------------------------------------------------------------------------------- /lib/module/home/view_model/home_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 4 | import 'package:flutter_develop_template/common/widget/notifier_widget.dart'; 5 | import 'package:flutter_develop_template/module/home/view/home_v.dart'; 6 | 7 | import '../api/home_repository.dart'; 8 | 9 | class HomeViewModel extends PageViewModel { 10 | CancelToken? cancelToken; 11 | 12 | @override 13 | onCreate() { 14 | 15 | assert((){ 16 | /// 拿到 页面状态里的 对象、属性 等等 17 | debugPrint('---executeSwitchLogin:${state.executeSwitchLogin}'); 18 | return true; 19 | }()); 20 | 21 | cancelToken = CancelToken(); 22 | pageDataModel = PageDataModel(); 23 | requestData(); 24 | } 25 | 26 | @override 27 | onDispose() { 28 | if (!(cancelToken?.isCancelled ?? true)) { 29 | cancelToken?.cancel(); 30 | } 31 | assert((){ 32 | debugPrint('HomeViewModel.onDispose()'); 33 | return true; 34 | }()); 35 | 36 | /// 别忘了执行父类的 onDispose 37 | super.onDispose(); 38 | } 39 | 40 | /// 请求数据 41 | @override 42 | Future requestData({Map? params}) async { 43 | PageViewModel viewModel = await HomeRepository().getHomeData( 44 | pageViewModel: this, 45 | cancelToken: cancelToken, 46 | curPage: params?['curPage'] ?? 1 47 | ); 48 | pageDataModel = viewModel.pageDataModel; 49 | pageDataModel?.refreshState(); 50 | return Future.value(viewModel); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/module/message/api/message_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_develop_template/common/repository/base_repository.dart'; 3 | import 'package:flutter_develop_template/common/net/dio_client.dart'; 4 | 5 | import '../../../common/mvvm/base_view_model.dart'; 6 | import '../model/message_list_m.dart'; 7 | 8 | class MessageRepository extends BaseRepository { 9 | 10 | /// 分页列表 11 | Future getMessageData({ 12 | required PageViewModel pageViewModel, 13 | CancelToken? cancelToken, 14 | int curPage = 1, 15 | }) async => httpPagingRequest( 16 | pageViewModel: pageViewModel, 17 | cancelToken: cancelToken, 18 | jsonCoverEntity: MessageListModel.fromJson, 19 | curPage: curPage, 20 | future: () => DioClient().doGet('article/list/$curPage/json', cancelToken: cancelToken)); 21 | 22 | 23 | /// 这是不使用 httpPagingRequest 的原始写法,如果业务复杂,可能还是需要在原始写法上,扩展 24 | // /// 分页列表 25 | // Future getMessageData({ 26 | // required PageViewModel pageViewModel, 27 | // CancelToken? cancelToken, 28 | // int curPage = 1, 29 | // }) async { 30 | // try { 31 | // Response response = await DioClient().doGet('article/list/$curPage/json', cancelToken: cancelToken); 32 | // 33 | // if(response.statusCode == REQUEST_SUCCESS) { 34 | // /// 请求成功 35 | // pageViewModel.pageDataModel?.type = NotifierResultType.success; 36 | // 37 | // /// 有分页 38 | // pageViewModel.pageDataModel?.isPaging = true; 39 | // 40 | // /// 分页代码 41 | // /// ViewModel 和 Model 相互持有代码,写着 correlationPaging() 里面 42 | // pageViewModel.pageDataModel?.correlationPaging(pageViewModel, MessageListModel.fromJson(response.data)); 43 | // } else { 44 | // 45 | // /// 请求成功,但业务不通过,比如没有权限 46 | // pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized; 47 | // pageViewModel.pageDataModel?.errorMsg = response.statusMessage; 48 | // } 49 | // 50 | // } on DioException catch (dioEx) { 51 | // /// 请求异常 52 | // pageViewModel.pageDataModel?.type = NotifierResultType.dioError; 53 | // pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx); 54 | // } catch (e) { 55 | // /// 未知异常 56 | // pageViewModel.pageDataModel?.type = NotifierResultType.fail; 57 | // pageViewModel.pageDataModel?.errorMsg = e.toString(); 58 | // } 59 | // 60 | // return pageViewModel; 61 | // } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /lib/module/message/model/message_list_m.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_develop_template/common/paging/base_paging_model.dart'; 2 | 3 | /// 这个实体有 分页列表数据集合,继承BasePagingModel 4 | class MessageListModel extends BasePagingModel { 5 | int? curPage; 6 | List? datas; 7 | int? offset; 8 | bool? over; 9 | int? pageCount; 10 | int? size; 11 | int? total; 12 | 13 | /// 别忘了这里,和父类相同的参数,传给父类 14 | /// 这种是语法糖写法,等同于下面的传统写法 15 | MessageListModel( 16 | {super.curPage, 17 | super.datas, 18 | super.offset, 19 | super.over, 20 | super.pageCount, 21 | super.size, 22 | super.total}); 23 | 24 | /// 传统写法 25 | // MessageListModel({curPage, datas, offset, over, pageCount, size, total}) 26 | // : super( 27 | // curPage: curPage, 28 | // datas: datas, 29 | // offset: offset, 30 | // over: over, 31 | // pageCount: pageCount, 32 | // size: size, 33 | // total: total); 34 | 35 | MessageListModel.fromJson(Map json) { 36 | curPage = json['curPage']; 37 | if (json['datas'] != null) { 38 | datas = []; 39 | json['datas'].forEach((v) { 40 | datas!.add(new Datas.fromJson(v)); 41 | }); 42 | } 43 | offset = json['offset']; 44 | over = json['over']; 45 | pageCount = json['pageCount']; 46 | size = json['size']; 47 | total = json['total']; 48 | } 49 | 50 | Map toJson() { 51 | final Map data = new Map(); 52 | data['curPage'] = this.curPage; 53 | if (this.datas != null) { 54 | /// 这个实体是分页的列表项,继承BasePagingItem,要转化一下 55 | // data['datas'] = this.datas!.map((v) => v.toJson()).toList(); 56 | data['datas'] = this.datas!.map((v) => (v as Datas).toJson()).toList(); 57 | } 58 | data['offset'] = this.offset; 59 | data['over'] = this.over; 60 | data['pageCount'] = this.pageCount; 61 | data['size'] = this.size; 62 | data['total'] = this.total; 63 | return data; 64 | } 65 | } 66 | 67 | /// 这个实体是分页的列表项,继承BasePagingItem 68 | class Datas extends BasePagingItem { 69 | bool? adminAdd; 70 | String? apkLink; 71 | int? audit; 72 | String? author; 73 | bool? canEdit; 74 | int? chapterId; 75 | String? chapterName; 76 | bool? collect; 77 | int? courseId; 78 | String? desc; 79 | String? descMd; 80 | String? envelopePic; 81 | bool? fresh; 82 | String? host; 83 | int? id; 84 | bool? isAdminAdd; 85 | String? link; 86 | String? niceDate; 87 | String? niceShareDate; 88 | String? origin; 89 | String? prefix; 90 | String? projectLink; 91 | int? publishTime; 92 | int? realSuperChapterId; 93 | int? selfVisible; 94 | int? shareDate; 95 | String? shareUser; 96 | int? superChapterId; 97 | String? superChapterName; 98 | List? tags; 99 | String? title; 100 | int? type; 101 | int? userId; 102 | int? visible; 103 | int? zan; 104 | 105 | Datas( 106 | {this.adminAdd, 107 | this.apkLink, 108 | this.audit, 109 | this.author, 110 | this.canEdit, 111 | this.chapterId, 112 | this.chapterName, 113 | this.collect, 114 | this.courseId, 115 | this.desc, 116 | this.descMd, 117 | this.envelopePic, 118 | this.fresh, 119 | this.host, 120 | this.id, 121 | this.isAdminAdd, 122 | this.link, 123 | this.niceDate, 124 | this.niceShareDate, 125 | this.origin, 126 | this.prefix, 127 | this.projectLink, 128 | this.publishTime, 129 | this.realSuperChapterId, 130 | this.selfVisible, 131 | this.shareDate, 132 | this.shareUser, 133 | this.superChapterId, 134 | this.superChapterName, 135 | this.tags, 136 | this.title, 137 | this.type, 138 | this.userId, 139 | this.visible, 140 | this.zan}); 141 | 142 | Datas.fromJson(Map json) { 143 | adminAdd = json['adminAdd']; 144 | apkLink = json['apkLink']; 145 | audit = json['audit']; 146 | author = json['author']; 147 | canEdit = json['canEdit']; 148 | chapterId = json['chapterId']; 149 | chapterName = json['chapterName']; 150 | collect = json['collect']; 151 | courseId = json['courseId']; 152 | desc = json['desc']; 153 | descMd = json['descMd']; 154 | envelopePic = json['envelopePic']; 155 | fresh = json['fresh']; 156 | host = json['host']; 157 | id = json['id']; 158 | isAdminAdd = json['isAdminAdd']; 159 | link = json['link']; 160 | niceDate = json['niceDate']; 161 | niceShareDate = json['niceShareDate']; 162 | origin = json['origin']; 163 | prefix = json['prefix']; 164 | projectLink = json['projectLink']; 165 | publishTime = json['publishTime']; 166 | realSuperChapterId = json['realSuperChapterId']; 167 | selfVisible = json['selfVisible']; 168 | shareDate = json['shareDate']; 169 | shareUser = json['shareUser']; 170 | superChapterId = json['superChapterId']; 171 | superChapterName = json['superChapterName']; 172 | if (json['tags'] != null) { 173 | tags = []; 174 | json['tags'].forEach((v) { 175 | tags!.add(new Tags.fromJson(v)); 176 | }); 177 | } 178 | title = json['title']; 179 | type = json['type']; 180 | userId = json['userId']; 181 | visible = json['visible']; 182 | zan = json['zan']; 183 | } 184 | 185 | Map toJson() { 186 | final Map data = new Map(); 187 | data['adminAdd'] = this.adminAdd; 188 | data['apkLink'] = this.apkLink; 189 | data['audit'] = this.audit; 190 | data['author'] = this.author; 191 | data['canEdit'] = this.canEdit; 192 | data['chapterId'] = this.chapterId; 193 | data['chapterName'] = this.chapterName; 194 | data['collect'] = this.collect; 195 | data['courseId'] = this.courseId; 196 | data['desc'] = this.desc; 197 | data['descMd'] = this.descMd; 198 | data['envelopePic'] = this.envelopePic; 199 | data['fresh'] = this.fresh; 200 | data['host'] = this.host; 201 | data['id'] = this.id; 202 | data['isAdminAdd'] = this.isAdminAdd; 203 | data['link'] = this.link; 204 | data['niceDate'] = this.niceDate; 205 | data['niceShareDate'] = this.niceShareDate; 206 | data['origin'] = this.origin; 207 | data['prefix'] = this.prefix; 208 | data['projectLink'] = this.projectLink; 209 | data['publishTime'] = this.publishTime; 210 | data['realSuperChapterId'] = this.realSuperChapterId; 211 | data['selfVisible'] = this.selfVisible; 212 | data['shareDate'] = this.shareDate; 213 | data['shareUser'] = this.shareUser; 214 | data['superChapterId'] = this.superChapterId; 215 | data['superChapterName'] = this.superChapterName; 216 | if (this.tags != null) { 217 | data['tags'] = this.tags!.map((v) => v.toJson()).toList(); 218 | } 219 | data['title'] = this.title; 220 | data['type'] = this.type; 221 | data['userId'] = this.userId; 222 | data['visible'] = this.visible; 223 | data['zan'] = this.zan; 224 | return data; 225 | } 226 | } 227 | 228 | class Tags { 229 | String? name; 230 | String? url; 231 | 232 | Tags({this.name, this.url}); 233 | 234 | Tags.fromJson(Map json) { 235 | name = json['name']; 236 | url = json['url']; 237 | } 238 | 239 | Map toJson() { 240 | final Map data = new Map(); 241 | data['name'] = this.name; 242 | data['url'] = this.url; 243 | return data; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /lib/module/message/view/message_v.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_develop_template/common/mvvm/base_page.dart'; 5 | import '../../../../res/string/str_common.dart'; 6 | import '../../../../res/string/str_message.dart'; 7 | import 'package:flutter_develop_template/common/widget/refresh_load_widget.dart'; 8 | import 'package:flutter_develop_template/module/message/model/message_list_m.dart'; 9 | import 'package:flutter_develop_template/module/message/view_model/message_vm.dart'; 10 | 11 | import '../../../../res/style/color_styles.dart'; 12 | import '../../../../res/style/text_styles.dart'; 13 | import '../../../common/widget/global_notification_widget.dart'; 14 | import '../../../common/widget/notifier_widget.dart'; 15 | 16 | class MessageView extends BaseStatefulPage { 17 | MessageView({super.key}); 18 | 19 | @override 20 | MessageViewState createState() => MessageViewState(); 21 | } 22 | 23 | class MessageViewState extends BaseStatefulPageState { 24 | 25 | @override 26 | MessageViewModel viewBindingViewModel() { 27 | /// ViewModel 和 View 相互持有 28 | return MessageViewModel()..viewState = this; 29 | } 30 | 31 | @override 32 | void initAttribute() { 33 | 34 | } 35 | 36 | @override 37 | void initObserver() {} 38 | 39 | @override 40 | void dispose() { 41 | assert((){ 42 | debugPrint('MessageView.onDispose()'); 43 | return true; 44 | }()); 45 | super.dispose(); 46 | } 47 | 48 | bool executeSwitchLogin = false; 49 | 50 | @override 51 | void didChangeDependencies() { 52 | var operate = GlobalOperateProvider.getGlobalOperate(context: context); 53 | 54 | assert((){ 55 | debugPrint('MessageView.didChangeDependencies --- $operate'); 56 | return true; 57 | }()); 58 | 59 | // 切换用户 60 | // 正常业务流程是:从本地存储,拿到当前最新的用户ID,请求接口,我这里偷了个懒 😄 61 | // 直接使用随机数,模拟 不同用户ID 62 | if (operate == GlobalOperate.switchLogin) { 63 | executeSwitchLogin = true; 64 | 65 | // 重新请求数据 66 | // 如果你想刷新的时候,显示loading,加上这两行 67 | viewModel?.pageDataModel?.type = NotifierResultType.loading; 68 | viewModel?.pageDataModel?.refreshState(); 69 | 70 | viewModel?.pagingDataModel?.listData.clear(); 71 | viewModel?.requestData(params: {'curPage': Random().nextInt(20)}); 72 | } 73 | } 74 | 75 | @override 76 | Widget appBuild(BuildContext context) { 77 | return Scaffold( 78 | appBar: AppBar( 79 | backgroundColor: AppBarTheme.of(context).backgroundColor, 80 | title: Text( 81 | StrMessage.message, 82 | style: TextStyles.style_222222_20, 83 | )), 84 | body: NotifierPageWidget( 85 | model: viewModel?.pageDataModel, 86 | builder: (context, dataModel) { 87 | final dataList = dataModel.pagingDataModel?.listData; 88 | return Stack( 89 | children: [ 90 | RefreshLoadWidget( 91 | pagingDataModel: dataModel.pagingDataModel!, 92 | scrollView: ListView.builder( 93 | padding: EdgeInsets.zero, 94 | itemCount: dataList?.length ?? 0, 95 | itemBuilder: (context, index) { 96 | var data = dataList?[index] as Datas; 97 | return Container( 98 | decoration: BoxDecoration( 99 | border: Border( 100 | bottom: BorderSide( 101 | width: 0.5, 102 | color: ColorStyles.color_000000 103 | ) 104 | ) 105 | ), 106 | width: MediaQuery.of(context).size.width, 107 | height: 50, 108 | alignment: Alignment.center, 109 | child: Text('${data.title}'), 110 | ); 111 | }), 112 | ), 113 | Container( 114 | color: ColorStyles.color_388E3C, 115 | child: executeSwitchLogin ? Row( 116 | mainAxisAlignment: MainAxisAlignment.center, 117 | children: [ 118 | Text(StrCommon.executeSwitchUser), 119 | IconButton(onPressed: (){ 120 | executeSwitchLogin = false; 121 | setState(() {}); 122 | }, icon: Icon(Icons.close)) 123 | ], 124 | ) : SizedBox(), 125 | ), 126 | ], 127 | ); 128 | }, 129 | ), 130 | ); 131 | } 132 | 133 | @override 134 | bool get wantKeepAlive => true; 135 | 136 | } 137 | -------------------------------------------------------------------------------- /lib/module/message/view_model/message_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_develop_template/module/message/api/message_repository.dart'; 4 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 5 | import 'package:flutter_develop_template/common/paging/paging_data_model.dart'; 6 | import 'package:flutter_develop_template/module/message/model/message_list_m.dart'; 7 | 8 | import '../../../common/widget/notifier_widget.dart'; 9 | import '../view/message_v.dart'; 10 | 11 | class MessageViewModel extends PageViewModel { 12 | 13 | CancelToken? cancelToken; 14 | PagingDataModel? pagingDataModel; 15 | 16 | @override 17 | onCreate() { 18 | 19 | assert((){ 20 | /// 拿到 页面状态里的 对象、属性 等等 21 | debugPrint('---executeSwitchLogin:${state.executeSwitchLogin}'); 22 | return true; 23 | }()); 24 | 25 | cancelToken = CancelToken(); 26 | pageDataModel = PageDataModel(); 27 | pagingDataModel = PagingDataModel(); 28 | requestData(); 29 | } 30 | 31 | @override 32 | onDispose() { 33 | if(!(cancelToken?.isCancelled ?? true)) { 34 | cancelToken?.cancel(); 35 | } 36 | 37 | assert((){ 38 | debugPrint('MessageViewModel.onDispose()'); 39 | return true; 40 | }()); 41 | 42 | /// 别忘了执行父类的 onDispose 43 | super.onDispose(); 44 | } 45 | 46 | /// 记录是否是第一次请求数据 47 | bool initRequest = true; 48 | 49 | @override 50 | Future requestData({Map? params}) async { 51 | PageViewModel viewModel = await MessageRepository().getMessageData( 52 | pageViewModel: this, 53 | cancelToken: cancelToken, 54 | curPage: params?['curPage'] ?? 1, 55 | ); 56 | 57 | /// 第一次请求数据,不会触发 下拉刷新 和 上拉加载更多方法,通过标识,初始化一些Paging参数 58 | if(initRequest) { 59 | pagingDataModel?.originalListDataLength = (viewModel.pageDataModel?.data as MessageListModel).datas?.length ?? 0; 60 | } 61 | 62 | initRequest = false; 63 | 64 | pageDataModel = viewModel.pageDataModel; 65 | 66 | /// 分页代码 67 | pageDataModel?.bindingPaging( 68 | viewModel, 69 | pageDataModel!, 70 | pagingDataModel!, 71 | this); 72 | 73 | pageDataModel?.refreshState(); 74 | 75 | /// 注意:使用需要返回 PageDataModel对象,它和 76 | return Future.value(viewModel); 77 | } 78 | 79 | } 80 | 81 | -------------------------------------------------------------------------------- /lib/module/order/view_model/order_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 3 | 4 | import '../view/order_v.dart'; 5 | 6 | class OrderViewModel extends PageViewModel { 7 | 8 | @override 9 | onCreate() { 10 | 11 | assert((){ 12 | /// 拿到 页面状态里的 对象、属性 等等 13 | debugPrint('state: --- ${state.paramsModel}'); 14 | return true; 15 | }()); 16 | 17 | } 18 | 19 | @override 20 | Future requestData({Map? params}) { 21 | return Future.value(null); 22 | } 23 | } -------------------------------------------------------------------------------- /lib/module/personal/api/personal_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_develop_template/common/repository/base_repository.dart'; 3 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 4 | import 'package:flutter_develop_template/common/net/dio_client.dart'; 5 | import 'package:flutter_develop_template/module/personal/model/user_info_m.dart'; 6 | 7 | class PersonalRepository extends BaseRepository { 8 | 9 | /// 注册 10 | Future registerUser({ 11 | required PageViewModel pageViewModel, 12 | Map? params, 13 | CancelToken? cancelToken, 14 | }) async => 15 | httpPageRequest( 16 | pageViewModel: pageViewModel, 17 | cancelToken: cancelToken, 18 | jsonCoverEntity: UserInfoModel.fromJson, 19 | future: () => DioClient().doPost( 20 | 'user/register', 21 | params: params, 22 | cancelToken: cancelToken, 23 | )); 24 | 25 | /// 登陆 26 | Future loginUser({ 27 | required PageViewModel pageViewModel, 28 | Map? params, 29 | CancelToken? cancelToken, 30 | }) async => 31 | httpPageRequest( 32 | pageViewModel: pageViewModel, 33 | cancelToken: cancelToken, 34 | jsonCoverEntity: UserInfoModel.fromJson, 35 | future: () => DioClient().doPost( 36 | 'user/login', 37 | params: params, 38 | cancelToken: cancelToken, 39 | )); 40 | 41 | 42 | /// 这是不使用 httpPageRequest 的原始写法,如果业务复杂,可能还是需要在原始写法上,扩展 43 | // /// 注册 44 | // Future registerUser({ 45 | // required PageViewModel pageViewModel, 46 | // Map? params, 47 | // CancelToken? cancelToken, 48 | // }) async { 49 | // 50 | // try { 51 | // Response response = await DioClient().doPost( 52 | // 'user/register', 53 | // params: params, 54 | // cancelToken: cancelToken, 55 | // ); 56 | // 57 | // if(response.statusCode == REQUEST_SUCCESS) { 58 | // /// 请求成功 59 | // pageViewModel.pageDataModel?.type = NotifierResultType.success; // 请求成功 60 | // 61 | // /// ViewModel 和 Model 相互持有 62 | // UserInfoModel model = UserInfoModel.fromJson(response.data); 63 | // model.vm = pageViewModel; 64 | // pageViewModel.pageDataModel?.data = model; 65 | // } else { 66 | // 67 | // /// 请求成功,但业务不通过,比如没有权限 68 | // pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized; 69 | // pageViewModel.pageDataModel?.errorMsg = response.statusMessage; 70 | // } 71 | // 72 | // } on DioException catch (dioEx) { 73 | // /// 请求异常 74 | // pageViewModel.pageDataModel?.type = NotifierResultType.dioError; 75 | // pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx); 76 | // 77 | // } catch (e) { 78 | // /// 未知异常 79 | // pageViewModel.pageDataModel?.type = NotifierResultType.fail; 80 | // pageViewModel.pageDataModel?.errorMsg = e.toString(); 81 | // } 82 | // 83 | // return pageViewModel; 84 | // } 85 | // 86 | // /// 登陆 87 | // Future loginUser({ 88 | // required PageViewModel pageViewModel, 89 | // Map? params, 90 | // CancelToken? cancelToken, 91 | // }) async { 92 | // 93 | // try { 94 | // Response response = await DioClient().doPost( 95 | // 'user/login', 96 | // params: params, 97 | // cancelToken: cancelToken, 98 | // ); 99 | // 100 | // if(response.statusCode == REQUEST_SUCCESS) { 101 | // /// 请求成功 102 | // pageViewModel.pageDataModel?.type = NotifierResultType.success; 103 | // 104 | // /// ViewModel 和 Model 相互持有 105 | // UserInfoModel model = UserInfoModel.fromJson(response.data); 106 | // model.vm = pageViewModel; 107 | // pageViewModel.pageDataModel?.data = model; 108 | // } else { 109 | // 110 | // /// 请求成功,但业务不通过,比如没有权限 111 | // pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized; 112 | // pageViewModel.pageDataModel?.errorMsg = response.statusMessage; 113 | // } 114 | // 115 | // } on DioException catch (dioEx) { 116 | // /// 请求异常 117 | // pageViewModel.pageDataModel?.type = NotifierResultType.dioError; 118 | // pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx); 119 | // 120 | // } catch (e) { 121 | // /// 未知异常 122 | // pageViewModel.pageDataModel?.type = NotifierResultType.fail; 123 | // pageViewModel.pageDataModel?.errorMsg = e.toString(); 124 | // } 125 | // 126 | // return pageViewModel; 127 | // } 128 | } 129 | -------------------------------------------------------------------------------- /lib/module/personal/model/user_info_m.dart: -------------------------------------------------------------------------------- 1 | import '../../../common/mvvm/base_model.dart'; 2 | 3 | class UserInfoModel extends BaseModel { 4 | bool? admin; 5 | int? coinCount; 6 | List? collectIds; 7 | String? email; 8 | String? icon; 9 | int? id; 10 | String? nickname; 11 | String? password; 12 | String? publicName; 13 | String? token; 14 | int? type; 15 | String? username; 16 | 17 | bool isLogin = false; 18 | 19 | UserInfoModel( 20 | {this.admin, 21 | this.coinCount, 22 | this.collectIds, 23 | this.email, 24 | this.icon, 25 | this.id, 26 | this.nickname, 27 | this.password, 28 | this.publicName, 29 | this.token, 30 | this.type, 31 | this.username}); 32 | 33 | UserInfoModel.fromJson(Map json) { 34 | admin = json['admin']; 35 | coinCount = json['coinCount']; 36 | collectIds = json['collectIds'].cast(); 37 | email = json['email']; 38 | icon = json['icon']; 39 | id = json['id']; 40 | nickname = json['nickname']; 41 | password = json['password']; 42 | publicName = json['publicName']; 43 | token = json['token']; 44 | type = json['type']; 45 | username = json['username']; 46 | } 47 | 48 | Map toJson() { 49 | final Map data = new Map(); 50 | data['admin'] = this.admin; 51 | data['coinCount'] = this.coinCount; 52 | data['collectIds'] = this.collectIds; 53 | data['email'] = this.email; 54 | data['icon'] = this.icon; 55 | data['id'] = this.id; 56 | data['nickname'] = this.nickname; 57 | data['password'] = this.password; 58 | data['publicName'] = this.publicName; 59 | data['token'] = this.token; 60 | data['type'] = this.type; 61 | data['username'] = this.username; 62 | return data; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/module/personal/view/personal_v.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:flutter_develop_template/common/mvvm/base_page.dart'; 6 | import '../../../../res/string/str_personal.dart'; 7 | import 'package:flutter_develop_template/common/widget/global_notification_widget.dart'; 8 | import 'package:flutter_develop_template/common/widget/notifier_widget.dart'; 9 | import 'package:flutter_develop_template/main/app.dart'; 10 | import 'package:flutter_develop_template/module/personal/model/user_info_m.dart'; 11 | 12 | import '../../../../res/style/color_styles.dart'; 13 | import '../../../../res/style/text_styles.dart'; 14 | import '../../../common/util/global.dart'; 15 | import '../view_model/personal_vm.dart'; 16 | 17 | class PersonalView extends BaseStatefulPage { 18 | PersonalView({super.key}); 19 | 20 | @override 21 | PersonalViewState createState() => PersonalViewState(); 22 | } 23 | 24 | class PersonalViewState extends BaseStatefulPageState { 25 | @override 26 | void initAttribute() {} 27 | 28 | @override 29 | void initObserver() {} 30 | 31 | @override 32 | viewBindingViewModel() { 33 | /// ViewModel 和 View 相互持有 34 | return PersonalViewModel()..viewState = this; 35 | } 36 | 37 | bool executeSwitchLogin = false; 38 | 39 | @override 40 | void didChangeDependencies() { 41 | var operate = GlobalOperateProvider.getGlobalOperate(context: context); 42 | 43 | assert((){ 44 | debugPrint('OrderView.didChangeDependencies --- $operate'); 45 | return true; 46 | }()); 47 | 48 | if (operate == GlobalOperate.switchLogin) { 49 | executeSwitchLogin = true; 50 | // 重新请求数据 51 | // viewModel.requestData(); 52 | } 53 | } 54 | 55 | @override 56 | Widget appBuild(BuildContext context) { 57 | return AnnotatedRegion( 58 | value: overlayBlackStyle, 59 | child: Material( 60 | child: Stack( 61 | children: [ 62 | Align( 63 | alignment: Alignment.center, 64 | child: Container( 65 | margin: EdgeInsets.only(top: kToolbarHeight + media!.padding.top), 66 | alignment: Alignment.center, 67 | child: ElevatedButton( 68 | child: Text(StrPersonal.register), 69 | onPressed: () { 70 | // 如果你想刷新的时候,显示loading,加上这两行 71 | viewModel?.pageDataModel?.type = NotifierResultType.loading; 72 | viewModel?.pageDataModel?.refreshState(); 73 | 74 | var str1 = Random().nextInt(9); 75 | var str2 = Random().nextInt(9); 76 | var str3 = Random().nextInt(9); 77 | var str4 = Random().nextInt(9); 78 | var str5 = Random().nextInt(9); 79 | viewModel?.registerUser(params: { 80 | 'username': '${str1}${str2}${str3}${str4}${str5}', 81 | 'password': '123456', 82 | 'repassword': '123456' 83 | }); 84 | }, 85 | ), 86 | ), 87 | ), 88 | Align( 89 | alignment: Alignment.center, 90 | child: Container( 91 | margin: EdgeInsets.only(top: kToolbarHeight + media!.padding.top + 100), 92 | alignment: Alignment.center, 93 | child: ElevatedButton( 94 | child: Text(StrPersonal.login), 95 | onPressed: () { 96 | // 如果你想刷新的时候,显示loading,加上这两行 97 | viewModel?.pageDataModel?.type = NotifierResultType.loading; 98 | viewModel?.pageDataModel?.refreshState(); 99 | 100 | viewModel?.loginUser(params: { 101 | 'username': 'aaaaaa', 102 | 'password': '123456', 103 | }); 104 | }, 105 | ), 106 | ), 107 | ), 108 | Align( 109 | alignment: Alignment.center, 110 | child: Container( 111 | margin: EdgeInsets.only(top: kToolbarHeight + media!.padding.top + 200), 112 | alignment: Alignment.center, 113 | child: ElevatedButton( 114 | child: Text(StrPersonal.switchUser), 115 | onPressed: () { 116 | // 更新本地存储的用户ID,(常用的本地存储库:shared_preferences) 117 | // ... ... 118 | 119 | // 通知所有继承 BaseStatefulPageState 的子页面 120 | GlobalOperateProvider.runGlobalOperate(context: context, operate: GlobalOperate.switchLogin); 121 | }, 122 | ), 123 | ), 124 | ), 125 | Container( 126 | margin: EdgeInsets.only(top: kToolbarHeight + media!.padding.top), 127 | color: ColorStyles.color_388E3C, 128 | child: executeSwitchLogin 129 | ? Row( 130 | mainAxisAlignment: MainAxisAlignment.center, 131 | children: [ 132 | Text(StrPersonal.switchUser), 133 | IconButton( 134 | onPressed: () { 135 | executeSwitchLogin = false; 136 | setState(() {}); 137 | }, 138 | icon: Icon(Icons.close)) 139 | ], 140 | ) 141 | : SizedBox(), 142 | ), 143 | _myAppBar(), 144 | ], 145 | ), 146 | ), 147 | ); 148 | } 149 | 150 | _myAppBar() { 151 | return Container( 152 | width: media!.size.width, 153 | height: kToolbarHeight + media!.padding.top, 154 | padding: EdgeInsets.only(top: media!.padding.top,left: 16), 155 | color: AppBarTheme.of(context).backgroundColor, 156 | alignment: Alignment.centerLeft, 157 | child: Builder( 158 | builder: (context) { 159 | /// 初始化状态设置为 不检查,不然会 返回 loading 组件 160 | viewModel?.pageDataModel?.type = NotifierResultType.notCheck; 161 | return NotifierPageWidget( 162 | model: viewModel?.pageDataModel, 163 | builder: (context, dataModel) { 164 | final data = dataModel.data as UserInfoModel?; 165 | String title = (data?.isLogin ?? false) ? '${StrPersonal.loginSuccess}:${data?.username} ${StrPersonal.welcome}' : '${StrPersonal.registerSuccess}:${data?.username} ${StrPersonal.welcome}'; 166 | return Text( 167 | (data?.username?.isEmpty ?? true) ? StrPersonal.personal : title, 168 | style: TextStyles.style_222222_20, 169 | ); 170 | } 171 | ); 172 | } 173 | ), 174 | ); 175 | } 176 | 177 | @override 178 | bool get wantKeepAlive => true; 179 | 180 | } 181 | -------------------------------------------------------------------------------- /lib/module/personal/view_model/personal_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter_develop_template/module/personal/api/personal_repository.dart'; 4 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 5 | 6 | import '../../../common/widget/notifier_widget.dart'; 7 | import '../model/user_info_m.dart'; 8 | import '../view/personal_v.dart'; 9 | 10 | class PersonalViewModel extends PageViewModel { 11 | CancelToken? cancelToken; 12 | 13 | @override 14 | onCreate() { 15 | 16 | assert((){ 17 | /// 拿到 页面状态里的 对象、属性 等等 18 | debugPrint('---executeSwitchLogin:${state.executeSwitchLogin}'); 19 | return true; 20 | }()); 21 | 22 | cancelToken = CancelToken(); 23 | pageDataModel = PageDataModel(); 24 | } 25 | 26 | @override 27 | onDispose() { 28 | if (!(cancelToken?.isCancelled ?? true)) { 29 | cancelToken?.cancel(); 30 | } 31 | assert((){ 32 | debugPrint('PersonalViewModel.onDispose()'); 33 | return true; 34 | }()); 35 | 36 | /// 别忘了执行父类的 onDispose 37 | super.onDispose(); 38 | } 39 | 40 | /// 注册 41 | Future registerUser({Map? params}) async { 42 | PageViewModel viewModel = await PersonalRepository().registerUser( 43 | pageViewModel: this, 44 | cancelToken: cancelToken, 45 | params: params 46 | ); 47 | 48 | (viewModel.pageDataModel?.data as UserInfoModel?)?.isLogin = false; 49 | pageDataModel = viewModel.pageDataModel; 50 | pageDataModel?.refreshState(); 51 | return Future.value(viewModel); 52 | } 53 | 54 | /// 登陆 55 | Future loginUser({Map? params}) async { 56 | PageViewModel viewModel = await PersonalRepository().loginUser( 57 | pageViewModel: this, 58 | cancelToken: cancelToken, 59 | params: params 60 | ); 61 | (viewModel.pageDataModel?.data as UserInfoModel?)?.isLogin = true; 62 | pageDataModel = viewModel.pageDataModel; 63 | pageDataModel?.refreshState(); 64 | return Future.value(viewModel); 65 | } 66 | 67 | @override 68 | Future requestData({Map? params}) { 69 | return Future.value(null); 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /lib/module/test_fluro/page_a.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_develop_template/common/mvvm/base_page.dart'; 3 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 4 | import '../../../../res/string/str_common.dart'; 5 | import '../../../../router/navigator_util.dart'; 6 | import 'package:flutter_develop_template/main/app.dart'; 7 | 8 | import '../../../../router/routers.dart'; 9 | 10 | class PageAView extends BaseStatefulPage { 11 | PageAView( 12 | {super.key, 13 | this.name, 14 | this.title, 15 | this.url, 16 | this.age, 17 | this.price, 18 | this.flag}); 19 | 20 | final String? name; 21 | final String? title; 22 | final String? url; 23 | final int? age; 24 | final double? price; 25 | final bool? flag; 26 | 27 | @override 28 | PageAViewState createState() => PageAViewState(); 29 | } 30 | 31 | class PageAViewState extends BaseStatefulPageState { 32 | 33 | @override 34 | void initAttribute() {} 35 | 36 | @override 37 | void initObserver() {} 38 | 39 | @override 40 | PageAViewModel viewBindingViewModel() { 41 | /// ViewModel 和 View 相互持有 42 | return PageAViewModel()..viewState = this; 43 | } 44 | 45 | @override 46 | Widget appBuild(BuildContext context) { 47 | return Scaffold( 48 | appBar: AppBar( 49 | title: Text(StrCommon.pageA), 50 | ), 51 | body: SizedBox( 52 | width: media!.size.width, 53 | height: media!.size.height, 54 | child: Column( 55 | mainAxisAlignment: MainAxisAlignment.center, 56 | crossAxisAlignment: CrossAxisAlignment.center, 57 | children: [ 58 | Text('name:${widget.name}'), 59 | Text('title:${widget.title}'), 60 | Text('url:${widget.url}'), 61 | Text('age:${widget.age}'), 62 | Text('price:${widget.price}'), 63 | Text('flag:${widget.flag}'), 64 | SizedBox(height: 20), 65 | ElevatedButton( 66 | onPressed: () { 67 | NavigatorUtil.push(context, Routers.pageC, replace: true); 68 | }, 69 | child: Text(StrCommon.toPageCDestroyCurrent), 70 | ), 71 | ElevatedButton( 72 | onPressed: () { 73 | NavigatorUtil.push(context, Routers.pageD); 74 | }, 75 | child: Text(StrCommon.routeInterceptFromPageAtoPageD), 76 | ), 77 | ElevatedButton( 78 | onPressed: () { 79 | NavigatorUtil.back(context, arguments: '我是PageA页的Pop返回值'); 80 | }, 81 | child: Text(StrCommon.backPreviousPage), 82 | ), 83 | ], 84 | ), 85 | ), 86 | ); 87 | } 88 | 89 | } 90 | 91 | class PageAViewModel extends PageViewModel { 92 | 93 | @override 94 | onCreate() { 95 | 96 | } 97 | 98 | @override 99 | Future requestData({Map? params}) { 100 | return Future.value(null); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/module/test_fluro/page_a2.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_develop_template/common/mvvm/base_page.dart'; 3 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 4 | import 'package:flutter_develop_template/main/app.dart'; 5 | 6 | import '../../../../res/string/str_common.dart'; 7 | import '../../../../router/navigator_util.dart'; 8 | 9 | class PageA2View extends BaseStatefulPage { 10 | PageA2View( 11 | {super.key, 12 | this.name, 13 | this.title, 14 | this.url, 15 | this.age, 16 | this.price, 17 | this.flag}); 18 | 19 | final String? name; 20 | final String? title; 21 | final String? url; 22 | final int? age; 23 | final double? price; 24 | final bool? flag; 25 | 26 | @override 27 | PageA2ViewState createState() => PageA2ViewState(); 28 | } 29 | 30 | class PageA2ViewState extends BaseStatefulPageState { 31 | 32 | @override 33 | void initAttribute() {} 34 | 35 | @override 36 | void initObserver() {} 37 | 38 | @override 39 | PageA2ViewModel viewBindingViewModel() { 40 | /// ViewModel 和 View 相互持有 41 | return PageA2ViewModel()..viewState = this; 42 | } 43 | 44 | @override 45 | Widget appBuild(BuildContext context) { 46 | return Scaffold( 47 | appBar: AppBar( 48 | title: Text(StrCommon.pageA2), 49 | ), 50 | body: SizedBox( 51 | width: media!.size.width, 52 | height: media!.size.height, 53 | child: Column( 54 | mainAxisAlignment: MainAxisAlignment.center, 55 | crossAxisAlignment: CrossAxisAlignment.center, 56 | children: [ 57 | Text('name:${widget.name}'), 58 | Text('title:${widget.title}'), 59 | Text('url:${widget.url}'), 60 | Text('age:${widget.age}'), 61 | Text('price:${widget.price}'), 62 | Text('flag:${widget.flag}'), 63 | SizedBox(height: 20), 64 | ElevatedButton( 65 | onPressed: () { 66 | NavigatorUtil.back(context, arguments: '我是PageA2页的Pop返回值'); 67 | }, 68 | child: Text(StrCommon.backPreviousPage), 69 | ), 70 | ], 71 | ), 72 | ), 73 | ); 74 | } 75 | 76 | } 77 | 78 | class PageA2ViewModel extends PageViewModel { 79 | 80 | @override 81 | onCreate() { 82 | 83 | } 84 | 85 | @override 86 | Future requestData({Map? params}) { 87 | return Future.value(null); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/module/test_fluro/page_b.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_develop_template/common/mvvm/base_page.dart'; 3 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 4 | import 'package:flutter_develop_template/main/app.dart'; 5 | import '../../../../res/string/str_common.dart'; 6 | import '../../../../router/navigator_util.dart'; 7 | import '../../../../router/routers.dart'; 8 | import '../order/view/order_v.dart'; 9 | 10 | class PageBView extends BaseStatefulPage { 11 | PageBView({super.key, this.paramsModel}); 12 | 13 | final TestParamsModel? paramsModel; 14 | 15 | @override 16 | PageBViewState createState() => PageBViewState(); 17 | 18 | } 19 | 20 | class PageBViewState extends BaseStatefulPageState { 21 | 22 | @override 23 | void initAttribute() { 24 | 25 | } 26 | 27 | @override 28 | void initObserver() { 29 | 30 | } 31 | 32 | @override 33 | PageBViewModel viewBindingViewModel() { 34 | /// ViewModel 和 View 相互持有 35 | return PageBViewModel()..viewState = this; 36 | } 37 | 38 | @override 39 | Widget appBuild(BuildContext context) { 40 | return Scaffold( 41 | appBar: AppBar( 42 | title: Text(StrCommon.pageB), 43 | ), 44 | body: SizedBox( 45 | width: media!.size.width, 46 | height: media!.size.height, 47 | child: Column( 48 | mainAxisAlignment: MainAxisAlignment.center, 49 | crossAxisAlignment: CrossAxisAlignment.center, 50 | children: [ 51 | Text('name:${widget.paramsModel?.name}'), 52 | Text('title:${widget.paramsModel?.title}'), 53 | Text('url:${widget.paramsModel?.url}'), 54 | Text('age:${widget.paramsModel?.age}'), 55 | Text('price:${widget.paramsModel?.price}'), 56 | Text('flag:${widget.paramsModel?.flag}'), 57 | SizedBox(height: 20), 58 | ElevatedButton( 59 | onPressed: () { 60 | NavigatorUtil.push(context,Routers.pageD); 61 | }, 62 | child: Text(StrCommon.toPageD), 63 | ), 64 | ElevatedButton( 65 | onPressed: () { 66 | NavigatorUtil.back(context,arguments: widget.paramsModel); 67 | }, 68 | child: Text(StrCommon.backPreviousPage), 69 | ), 70 | ], 71 | ), 72 | ), 73 | ); 74 | } 75 | 76 | } 77 | 78 | class PageBViewModel extends PageViewModel { 79 | 80 | @override 81 | onCreate() { 82 | 83 | } 84 | 85 | @override 86 | Future requestData({Map? params}) { 87 | return Future.value(null); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /lib/module/test_fluro/page_c.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_develop_template/common/mvvm/base_page.dart'; 3 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 4 | 5 | import '../../../../res/string/str_common.dart'; 6 | import '../../../../router/navigator_util.dart'; 7 | import '../../../../router/routers.dart'; 8 | import '../../main/app.dart'; 9 | 10 | class PageCView extends BaseStatefulPage { 11 | PageCView({super.key}); 12 | 13 | @override 14 | PageCViewState createState() => PageCViewState(); 15 | } 16 | 17 | class PageCViewState extends BaseStatefulPageState { 18 | 19 | @override 20 | void initAttribute() { 21 | 22 | } 23 | 24 | @override 25 | void initObserver() { 26 | 27 | } 28 | 29 | @override 30 | PageCViewModel viewBindingViewModel() { 31 | /// ViewModel 和 View 相互持有 32 | return PageCViewModel()..viewState = this; 33 | } 34 | 35 | @override 36 | Widget appBuild(BuildContext context) { 37 | return Scaffold( 38 | appBar: AppBar( 39 | title: Text(StrCommon.pageC), 40 | ), 41 | body: SizedBox( 42 | width: media!.size.width, 43 | height: media!.size.height, 44 | child: Column( 45 | mainAxisAlignment: MainAxisAlignment.center, 46 | crossAxisAlignment: CrossAxisAlignment.center, 47 | children: [ 48 | ElevatedButton( 49 | onPressed: () { 50 | NavigatorUtil.push(context,Routers.pageD,replace:true); 51 | }, 52 | child: Text(StrCommon.toPageD), 53 | ), 54 | ], 55 | ), 56 | ), 57 | ); 58 | } 59 | 60 | } 61 | 62 | class PageCViewModel extends PageViewModel { 63 | 64 | @override 65 | onCreate() { 66 | 67 | } 68 | 69 | @override 70 | Future requestData({Map? params}) { 71 | return Future.value(null); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /lib/module/test_fluro/page_d.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_develop_template/common/mvvm/base_page.dart'; 3 | import 'package:flutter_develop_template/common/mvvm/base_view_model.dart'; 4 | 5 | import '../../../../res/string/str_common.dart'; 6 | import '../../../../router/navigator_util.dart'; 7 | import '../../main/app.dart'; 8 | 9 | class PageDView extends BaseStatefulPage { 10 | PageDView({super.key}); 11 | 12 | @override 13 | PageDViewState createState() => PageDViewState(); 14 | } 15 | 16 | class PageDViewState extends BaseStatefulPageState { 17 | 18 | @override 19 | void initAttribute() { 20 | 21 | } 22 | 23 | @override 24 | void initObserver() { 25 | 26 | } 27 | 28 | @override 29 | PageDViewModel viewBindingViewModel() { 30 | /// ViewModel 和 View 相互持有 31 | return PageDViewModel()..viewState = this; 32 | } 33 | 34 | @override 35 | Widget appBuild(BuildContext context) { 36 | return Scaffold( 37 | appBar: AppBar( 38 | title: Text(StrCommon.pageD), 39 | ), 40 | body: SizedBox( 41 | width: media!.size.width, 42 | height: media!.size.height, 43 | child: Column( 44 | mainAxisAlignment: MainAxisAlignment.center, 45 | crossAxisAlignment: CrossAxisAlignment.center, 46 | children: [ 47 | ElevatedButton( 48 | onPressed: () { 49 | /// 这种是,先销毁所有路由,再新建 50 | // NavigatorUtil.push( 51 | // context, 52 | // Routers.root, 53 | // clearStack: true, 54 | // transition: TransitionType.none, 55 | // ); 56 | 57 | /// 这种是 按照 栈 先进后出的原则,回退到根页面 58 | NavigatorUtil.back(context,toRoot: true); 59 | }, 60 | child: Text(StrCommon.toHomeDestroyAll), 61 | ), 62 | ], 63 | ), 64 | ), 65 | ); 66 | } 67 | 68 | } 69 | 70 | class PageDViewModel extends PageViewModel { 71 | 72 | @override 73 | onCreate() { 74 | 75 | } 76 | 77 | @override 78 | Future requestData({Map? params}) { 79 | return Future.value(null); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /lib/res/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxia-mrhan/flutter_develop_template/26f279bc9e0dc6e3c7e76be4b727ef241af09bd1/lib/res/.DS_Store -------------------------------------------------------------------------------- /lib/res/string/str_common.dart: -------------------------------------------------------------------------------- 1 | /// 这里统一写全局,非动态、固定的 字符串文本 2 | /// 如果哪一天 项目突然要求 使用 国际化 3 | /// 直接将这里的内容,拷贝到 国际化插件生成的 映射文件中,再 对照中文 翻译成 指定语言 4 | /// 5 | /// 通用 6 | class StrCommon { 7 | 8 | static const String pleaseService = '请联系客服'; 9 | static const String platformNotAdapted = '当前平台未适配'; 10 | static const String executeSwitchUser = '执行了切换用户操作'; 11 | 12 | static const String loading = '加载中...'; 13 | static const String dataIsEmpty = '数据为空'; 14 | static const String listDataIsEmpty = '列表数据为空'; 15 | static const String businessDoesNotPass = '业务不通过'; 16 | static const String dioErrorAnomaly = 'dioError异常'; 17 | static const String unknownAnomaly = '未知异常'; 18 | 19 | static const String pullDownToRefresh = '下拉刷新'; 20 | static const String refreshing = '刷新中...'; 21 | static const String loadedSuccess = '加载成功'; 22 | static const String releaseToRefreshImmediately = '松开立即刷新'; 23 | static const String refreshFailed = '刷新失败'; 24 | static const String pullUpLoad = '上拉加载'; 25 | static const String letGoLoading = '松手开始加载数据'; 26 | static const String failedLoad = '加载失败'; 27 | static const String noMoreData = '没有更多数据了'; 28 | 29 | static const String connectionTimeout = '连接超时'; 30 | static const String sendTimeout = '发送超时'; 31 | static const String receiveTimeout = '接收超时'; 32 | static const String accessCertificateError = '访问证书错误'; 33 | static const String validationFailed = '验证失败'; 34 | static const String connectionIsAbnormal = '连接异常'; 35 | static const String unknownError = '未知错误'; 36 | 37 | static const String parameterIsIncorrect = '参数有误'; 38 | static const String illegalRequests = '非法请求'; 39 | static const String serverRejectsRequest = '服务器拒绝请求'; 40 | static const String accessAddressDoesNotExist = '访问地址不存在'; 41 | static const String requestIsMadeWrongWay = '请求方式错误'; 42 | static const String wasAnErrorInsideServer = '服务器内部出错了'; 43 | static const String invalidRequest = '无效的请求'; 44 | static const String serverIsBusy = '服务器繁忙'; 45 | static const String unsupportedHttpProtocol = '不支持的HTTP协议'; 46 | 47 | 48 | ///--------------------------- 测试路由 ------------------------------- 49 | static const String pageA = 'PageA'; 50 | static const String pageA2 = 'PageA2'; 51 | static const String pageB = 'PageB'; 52 | static const String pageC = 'PageC'; 53 | static const String pageD = 'PageD'; 54 | 55 | static const String toPageCDestroyCurrent = '前往PageC,并销毁当前页面'; 56 | static const String routeInterceptFromPageAtoPageD = '路由拦截 从PageA前往PageD'; 57 | static const String backPreviousPage = '返回上一页'; 58 | static const String toPageD = '前往PageD'; 59 | static const String toHomeDestroyAll = '前往首页,并销毁所有页面'; 60 | 61 | } -------------------------------------------------------------------------------- /lib/res/string/str_home.dart: -------------------------------------------------------------------------------- 1 | /// 这里统一写全局,非动态、固定的 字符串文本 2 | /// 如果哪一天 项目突然要求 使用 国际化 3 | /// 直接将这里的内容,拷贝到 国际化插件生成的 映射文件中,再 对照中文 翻译成 指定语言 4 | /// 5 | /// 首页 6 | class StrHome { 7 | 8 | static const String home = 'Home'; 9 | 10 | } -------------------------------------------------------------------------------- /lib/res/string/str_message.dart: -------------------------------------------------------------------------------- 1 | /// 这里统一写全局,非动态、固定的 字符串文本 2 | /// 如果哪一天 项目突然要求 使用 国际化 3 | /// 直接将这里的内容,拷贝到 国际化插件生成的 映射文件中,再 对照中文 翻译成 指定语言 4 | /// 5 | /// 消息 6 | class StrMessage { 7 | 8 | static const String message = 'Message'; 9 | 10 | } -------------------------------------------------------------------------------- /lib/res/string/str_order.dart: -------------------------------------------------------------------------------- 1 | /// 这里统一写全局,非动态、固定的 字符串文本 2 | /// 如果哪一天 项目突然要求 使用 国际化 3 | /// 直接将这里的内容,拷贝到 国际化插件生成的 映射文件中,再 对照中文 翻译成 指定语言 4 | /// 5 | /// 订单 6 | class StrOrder { 7 | 8 | static const String order = 'Order'; 9 | static const String noObjectToPageA = '携带 非对象类型 前往PageA(拼接方式)'; 10 | static const String noObjectToPageA2 = '携带 非对象类型 前往PageA2(arguments方式)'; 11 | static const String objectToPageB = '携带 对象类型 前往PageB'; 12 | 13 | } -------------------------------------------------------------------------------- /lib/res/string/str_personal.dart: -------------------------------------------------------------------------------- 1 | /// 这里统一写全局,非动态、固定的 字符串文本 2 | /// 如果哪一天 项目突然要求 使用 国际化 3 | /// 直接将这里的内容,拷贝到 国际化插件生成的 映射文件中,再 对照中文 翻译成 指定语言 4 | /// 5 | /// 个人 6 | class StrPersonal { 7 | 8 | static const String personal = 'Personal'; 9 | static const String register = '注册'; 10 | static const String switchUser = '切换用户'; 11 | static const String loginSuccess = '登陆成功'; 12 | static const String registerSuccess = '注册成功'; 13 | static const String welcome = '欢迎您'; 14 | static const String login = '登陆'; 15 | 16 | } -------------------------------------------------------------------------------- /lib/res/style/color_styles.dart: -------------------------------------------------------------------------------- 1 | /// 错误的包,这种在Widget使用时,Widget找不到 样式实例 2 | /// import 'dart:ui'; 3 | 4 | /// 导入正确的包 5 | import 'package:flutter/material.dart'; 6 | /// 或者 import 'package:flutter/cupertino.dart'; 7 | 8 | /// 全局颜色样式 统一放置的地方 9 | /// 根据颜色大概种类,进行归类 10 | class ColorStyles { 11 | 12 | static const Color color_transparent = Color(0x00000000); 13 | 14 | static const Color color_FFFFFF = Color(0xFFFFFFFF); 15 | 16 | static const Color color_000000 = Color(0xFF000000); 17 | static const Color color_222222 = Color(0xFF222222); 18 | static const Color color_474747 = Color(0xff474747); 19 | 20 | static const Color color_F94B30 = Color(0xFFF94B30); 21 | static const Color color_EA5034 = Color(0xFFEA5034); 22 | static const Color color_FF4D4F = Color(0xFFFF4D4F); 23 | 24 | static const Color color_3A65E6 = Color(0xFF3A65E6); 25 | static const Color color_456FEF = Color(0xFF456FEF); 26 | static const Color color_0E6ED9 = Color(0xFF0E6ED9); 27 | static const Color color_1E88E5 = Color(0xFF1E88E5); 28 | 29 | static const Color color_388E3C = Color(0xFF388E3C); 30 | static const Color color_2E7D32 = Color(0xFF2E7D32); 31 | 32 | } -------------------------------------------------------------------------------- /lib/res/style/text_styles.dart: -------------------------------------------------------------------------------- 1 | /// 错误的包,这种在Widget使用时,Widget找不到 样式实例 2 | /// import 'dart:ui'; 3 | 4 | /// 导入正确的包 5 | import 'package:flutter/material.dart'; 6 | /// 或者 import 'package:flutter/cupertino.dart'; 7 | 8 | import 'color_styles.dart'; 9 | 10 | /// 全局字体样式 统一放置的地方 11 | /// 根据字体是否是加粗,进行归类 12 | class TextStyles { 13 | static final TextStyle style_000000_16 = TextStyle( 14 | color: ColorStyles.color_000000, 15 | fontSize: 16, 16 | ); 17 | 18 | static final TextStyle style_222222_16 = TextStyle( 19 | color: ColorStyles.color_222222, 20 | fontSize: 16, 21 | ); 22 | 23 | static final TextStyle style_222222_20 = TextStyle( 24 | color: ColorStyles.color_222222, 25 | fontSize: 20, 26 | ); 27 | 28 | ///--------------------------- 字体加粗 ------------------------------- 29 | static final TextStyle style_bold_000000_16 = TextStyle( 30 | color: ColorStyles.color_000000, 31 | fontSize: 16, 32 | fontWeight: FontWeight.w700, 33 | ); 34 | 35 | static final TextStyle style_bold_222222_16 = TextStyle( 36 | color: ColorStyles.color_222222, 37 | fontSize: 16, 38 | fontWeight: FontWeight.w700, 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /lib/res/style/theme_styles.dart: -------------------------------------------------------------------------------- 1 | /// 错误的包,这种在Widget使用时,Widget找不到 样式实例 2 | /// import 'dart:ui'; 3 | 4 | /// 导入正确的包 5 | import 'package:flutter/material.dart'; 6 | 7 | import 'color_styles.dart'; 8 | /// 或者 import 'package:flutter/cupertino.dart'; 9 | 10 | /// 全局主题样式 统一放置的地方 11 | class ThemeStyles { 12 | 13 | static ThemeData defaultTheme = ThemeData( 14 | // appBar主题 15 | appBarTheme: AppBarTheme(backgroundColor: ColorStyles.color_0E6ED9), 16 | // 去除水波纹效果 17 | splashColor: ColorStyles.color_transparent, 18 | // 去除长按效果 19 | highlightColor: ColorStyles.color_transparent, 20 | useMaterial3: true, 21 | ); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /lib/router/navigator_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluro/fluro.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import '../../../../router/routers.dart'; 4 | 5 | import '../../main/app.dart'; 6 | 7 | /// 路由工具类 8 | /// 如果所在类没有 context,可以使用 全局context:navigatorKey.currentContext 9 | class NavigatorUtil { 10 | 11 | /// 前往页面 12 | static Future push( 13 | BuildContext context, 14 | path, { 15 | bool replace = false, // 替换,进入新的页面时,将当前页面销毁 16 | bool clearStack = false, // 清空路由栈,进入新的页面时,其他实例的页面全部销毁 17 | Object? arguments, // 传递的参数 18 | TransitionType? transition, // 页面跳转的动画 类型 19 | }) { 20 | return Routers.router.navigateTo( 21 | context, 22 | path, 23 | replace: replace, 24 | clearStack: clearStack, 25 | transition: transition ?? TransitionType.inFromRight, // 默认跳转动画,从右侧进入 26 | routeSettings: RouteSettings( 27 | arguments: arguments, 28 | ), 29 | ); 30 | } 31 | 32 | /// 返回 33 | static void back( 34 | BuildContext context, { 35 | int count = 1, // 指定返回多少层 36 | bool toRoot = false, // 直接返回根目录 37 | Object? arguments, 38 | }) { 39 | if(toRoot) { 40 | /// 获取路由栈里,页面总数量 41 | int? routeStackLength = pageRouteObserver?.getRouteStackLength() ?? 1; 42 | count = routeStackLength; 43 | if(count == 1) { 44 | /// 路由栈里,只有1个页面 45 | return; 46 | } else { 47 | /// 减1,保留 根页面 48 | count--; 49 | } 50 | } 51 | NavigatorState state = Navigator.of(context); 52 | while(count-- > 0) { 53 | state = state..pop(arguments); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /lib/router/page_route_observer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../../../router/routers.dart'; 3 | 4 | import '../../main/app.dart'; 5 | import 'navigator_util.dart'; 6 | 7 | /// 监听路由栈状态 8 | class PageRouteObserver extends NavigatorObserver { 9 | static List> routeStack = []; 10 | 11 | @override 12 | void didPush(Route route, Route? previousRoute) { 13 | super.didPush(route, previousRoute); 14 | 15 | /// 当前所在页面 Path 16 | String? currentRoutePath = getOriginalPath(previousRoute); 17 | 18 | /// 要前往的页面 Path 19 | String? newRoutePath = getOriginalPath(route); 20 | 21 | /// 拦截指定页面 22 | /// 如果从 PageA 页面,跳转到 PageD,将其拦截 23 | if(currentRoutePath == Routers.pageA) { 24 | 25 | if(newRoutePath == Routers.pageD) { 26 | assert((){ 27 | debugPrint('准备从 PageA页面 进入 pageD页面,进行登陆信息验证'); 28 | return true; 29 | }()); 30 | 31 | // if(验证不通过) { 32 | /// 注意:要延迟一帧 33 | WidgetsBinding.instance.addPostFrameCallback((_){ 34 | // 我这里是pop,视觉上达到无法进入新页面的效果, 35 | // 正常业务是跳转到 登陆页面 36 | NavigatorUtil.back(navigatorKey.currentContext!); 37 | }); 38 | // } 39 | } 40 | 41 | } 42 | 43 | routeStack.add(route); 44 | printRouteStack(); 45 | } 46 | 47 | @override 48 | void didPop(Route route, Route? previousRoute) { 49 | super.didPop(route, previousRoute); 50 | routeStack.remove(route); 51 | printRouteStack(); 52 | } 53 | 54 | @override 55 | void didRemove(Route route, Route? previousRoute) { 56 | super.didRemove(route, previousRoute); 57 | routeStack.remove(route); 58 | printRouteStack(); 59 | } 60 | 61 | @override 62 | void didReplace({Route? newRoute, Route? oldRoute}) { 63 | super.didReplace(newRoute: newRoute, oldRoute: oldRoute); 64 | if (oldRoute != null) { 65 | routeStack.remove(oldRoute); 66 | } 67 | if (newRoute != null) { 68 | routeStack.add(newRoute); 69 | } 70 | printRouteStack(); 71 | } 72 | 73 | void printRouteStack() { 74 | assert((){ 75 | debugPrint('当前路由栈: ${routeStack.map((route) => route.settings.name).toList()}'); 76 | debugPrint('当前路由栈长度: ${routeStack.length}'); 77 | return true; 78 | }()); 79 | } 80 | 81 | /// 返回当前路由栈的长度 82 | int getRouteStackLength() { 83 | return routeStack.length; 84 | } 85 | } 86 | 87 | /// 获取原生路径 88 | /// 使用 path拼接方式 传递 参数,会改变原来的 路由页面 Path 89 | /// 90 | /// 比如:NavigatorUtil.push(context,'${Routers.pageA}?name=$name&title=$title&url=$url&age=$age&price=$price&flag=$flag'); 91 | /// path会变成:/pageA?name=jk&title=%E5%BC%A0%E4%B8%89&url=https%3A%2F%2Fwww.baidu.com&age=99&price=9.9&flag=true 92 | /// 所以再次匹配pageA,找不到,需要还原一下,getOriginalPath(path) 93 | String? getOriginalPath(Route? route) { 94 | // 获取原始的路由路径 95 | String? fullPath = route?.settings.name; 96 | 97 | if(fullPath != null) { 98 | // 使用正则表达式去除查询参数 99 | return fullPath.split('?')[0]; 100 | } 101 | 102 | return fullPath; 103 | } -------------------------------------------------------------------------------- /lib/router/routers.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluro/fluro.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_develop_template/main/app.dart'; 4 | import 'package:flutter_develop_template/module/test_fluro/page_a.dart'; 5 | import 'package:flutter_develop_template/module/test_fluro/page_a2.dart'; 6 | import 'package:flutter_develop_template/module/test_fluro/page_b.dart'; 7 | import 'package:flutter_develop_template/module/test_fluro/page_c.dart'; 8 | 9 | import '../../module/order/view/order_v.dart'; 10 | import '../../module/test_fluro/page_d.dart'; 11 | 12 | class Routers { 13 | static FluroRouter router = FluroRouter(); 14 | 15 | // 配置路由 16 | static void configureRouters() { 17 | router.notFoundHandler = Handler(handlerFunc: (_, __) { 18 | // 找不到路由时,返回指定提示页面 19 | return Scaffold( 20 | body: const Center( 21 | child: Text('404'), 22 | ), 23 | ); 24 | }); 25 | 26 | // 初始化路由 27 | _initRouter(); 28 | } 29 | 30 | // 设置页面 31 | 32 | // 页面标识 33 | static String root = '/'; 34 | 35 | // 页面A 36 | static String pageA = '/pageA'; 37 | 38 | // 页面A2 39 | static String pageA2 = '/pageA2'; 40 | 41 | // 页面B 42 | static String pageB = '/pageB'; 43 | 44 | // 页面C 45 | static String pageC = '/pageC'; 46 | 47 | // 页面D 48 | static String pageD = '/pageD'; 49 | 50 | // 注册路由 51 | static _initRouter() { 52 | 53 | // 根页面 54 | router.define( 55 | root, 56 | handler: Handler( 57 | handlerFunc: (_, __) => AppMainPage(), 58 | ), 59 | ); 60 | 61 | // 页面A 需要 非对象类型 参数(通过 拼接 传参数) 62 | router.define( 63 | pageA, 64 | handler: Handler( 65 | handlerFunc: (_, Map> params) { 66 | 67 | // 获取路由参数 68 | String? name = params['name']?.first; 69 | String? title = params['title']?.first; 70 | String? url = params['url']?.first; 71 | String? age = params['age']?.first ?? '-1'; 72 | String? price = params['price']?.first ?? '-1'; 73 | String? flag = params['flag']?.first ?? 'false'; 74 | 75 | return PageAView( 76 | name: name, 77 | title: title, 78 | url: url, 79 | age: int.parse(age), 80 | price: double.parse(price), 81 | flag: bool.parse(flag) 82 | ); 83 | 84 | }, 85 | ), 86 | ); 87 | 88 | // 页面A2 需要 非对象类型 参数(通过 arguments 传参数) 89 | router.define( 90 | pageA2, 91 | handler: Handler( 92 | handlerFunc: (context, _) { 93 | 94 | // 获取路由参数 95 | final arguments = context?.settings?.arguments as Map; 96 | 97 | String? name = arguments['name'] as String?; 98 | String? title = arguments['title'] as String?; 99 | String? url = arguments['url'] as String?; 100 | int? age = arguments['age'] as int?; 101 | double? price = arguments['price'] as double?; 102 | bool? flag = arguments['flag'] as bool?; 103 | 104 | return PageA2View( 105 | name: name, 106 | title: title, 107 | url: url, 108 | age: age, 109 | price: price, 110 | flag: flag ?? false 111 | ); 112 | 113 | }, 114 | ), 115 | ); 116 | 117 | // 页面B 需要 对象类型 参数 118 | router.define( 119 | pageB, 120 | handler: Handler( 121 | handlerFunc: (context, Map> params) { 122 | // 获取路由参数 123 | TestParamsModel? paramsModel = context?.settings?.arguments as TestParamsModel?; 124 | return PageBView(paramsModel: paramsModel); 125 | }, 126 | ), 127 | ); 128 | 129 | // 页面C 无参数 130 | router.define( 131 | pageC, 132 | handler: Handler( 133 | handlerFunc: (_, __) => PageCView(), 134 | ), 135 | ); 136 | 137 | // 页面D 无参数 138 | router.define( 139 | pageD, 140 | handler: Handler( 141 | handlerFunc: (_, __) => PageDView(), 142 | ), 143 | ); 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_develop_template 2 | description: "A new Flutter project." 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 1.0.0+1 20 | 21 | environment: 22 | sdk: '>=3.4.1 <4.0.0' 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | 34 | 35 | # The following adds the Cupertino Icons font to your application. 36 | # Use with the CupertinoIcons class for iOS style icons. 37 | cupertino_icons: ^1.0.6 38 | dio: ^5.4.3+1 39 | fluro: ^2.0.5 40 | pull_to_refresh: ^2.0.0 41 | sentry_flutter: ^7.8.0 42 | 43 | dev_dependencies: 44 | flutter_test: 45 | sdk: flutter 46 | 47 | # The "flutter_lints" package below contains a set of recommended lints to 48 | # encourage good coding practices. The lint set provided by the package is 49 | # activated in the `analysis_options.yaml` file located at the root of your 50 | # package. See that file for information about deactivating specific lint 51 | # rules and activating additional ones. 52 | flutter_lints: ^3.0.0 53 | 54 | # For information on the generic Dart part of this file, see the 55 | # following page: https://dart.dev/tools/pub/pubspec 56 | 57 | # The following section is specific to Flutter packages. 58 | flutter: 59 | 60 | # The following line ensures that the Material Icons font is 61 | # included with your application, so that you can use the icons in 62 | # the material Icons class. 63 | uses-material-design: true 64 | 65 | # To add assets to your application, add an assets section, like this: 66 | # assets: 67 | # - images/a_dot_burr.jpeg 68 | # - images/a_dot_ham.jpeg 69 | 70 | # An image asset can refer to one or more resolution-specific "variants", see 71 | # https://flutter.dev/assets-and-images/#resolution-aware 72 | 73 | # For details regarding adding assets from package dependencies, see 74 | # https://flutter.dev/assets-and-images/#from-packages 75 | 76 | # To add custom fonts to your application, add a fonts section here, 77 | # in this "flutter" section. Each entry in this list should have a 78 | # "family" key with the font family name, and a "fonts" key with a 79 | # list giving the asset and other descriptors for the font. For 80 | # example: 81 | # fonts: 82 | # - family: Schyler 83 | # fonts: 84 | # - asset: fonts/Schyler-Regular.ttf 85 | # - asset: fonts/Schyler-Italic.ttf 86 | # style: italic 87 | # - family: Trajan Pro 88 | # fonts: 89 | # - asset: fonts/TrajanPro.ttf 90 | # - asset: fonts/TrajanPro_Bold.ttf 91 | # weight: 700 92 | # 93 | # For details regarding fonts from package dependencies, 94 | # see https://flutter.dev/custom-fonts/#from-packages 95 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_develop_template/main/app.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(const App()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('0'), findsOneWidget); 19 | expect(find.text('1'), findsNothing); 20 | 21 | // Tap the '+' icon and trigger a frame. 22 | await tester.tap(find.byIcon(Icons.add)); 23 | await tester.pump(); 24 | 25 | // Verify that our counter has incremented. 26 | expect(find.text('0'), findsNothing); 27 | expect(find.text('1'), findsOneWidget); 28 | }); 29 | } 30 | --------------------------------------------------------------------------------