This commit is contained in:
2026-06-03 14:07:10 +08:00
parent 3bdece45c3
commit 9eb8d1cc37
118 changed files with 5689 additions and 2 deletions

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

33
.metadata Normal file
View File

@@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "05db9689081f091050f01aed79f04dce0c750154"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
- platform: android
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
- platform: ios
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

94
README.en.md Normal file
View File

@@ -0,0 +1,94 @@
[![中文](https://img.shields.io/badge/Lang-中文-red)](README.md)
# Flutter Template
A production-ready Flutter quick-start template extracted from real-world projects. Ships with a proven architecture, curated dependency stack, and ready-to-use infrastructure — start writing business features on day one.
## Goals
- Android & iOS support
- Zero business residue — no business pages, API models, assets, copy, or private configuration
- Production infrastructure included: routing, theming, networking, caching, logging, permissions, utilities, state management, and common UI components
- Demo page as default home — delete or replace with your own
## Tech Stack
| Category | Package | Purpose |
|---|---|---|
| State Management | flutter_riverpod | Compile-safe, testable state management |
| Networking | dio | HTTP client with interceptor chain |
| Local Cache | shared_preferences | Key-value persistence |
| Network Monitor | connectivity_plus | Real-time connectivity tracking |
| Permissions | permission_handler | Runtime permission requests |
| Screen Adaptation | flutter_screenutil | Design-dimension-based layout |
| Image Loading | cached_network_image | Network image caching |
| SVG | flutter_svg | SVG rendering |
| Pull to Refresh | pull_to_refresh | Refresh and load-more |
| Loading HUD | flutter_easyloading | Toast and loading indicator |
| Device Info | device_info_plus | Device metadata |
| Package Info | package_info_plus | App version info |
| URL Launcher | url_launcher | Open external URLs |
| Linting | flutter_lints | Recommended Dart lint rules |
## Directory Structure
```
lib/
├── app/ # Application shell
│ ├── app.dart # MaterialApp, theme, localization, global config
│ ├── bootstrap.dart # Startup initialization
│ ├── config/ # Multi-environment config (dev/staging/prod)
│ ├── router/ # Navigator utilities and route stack tracking
│ └── theme/ # Light/dark themes, colors, spacing
├── core/ # Framework-level infrastructure
│ ├── cache/ # SharedPreferences wrapper and common keys
│ ├── extensions/ # BuildContext extensions
│ ├── logging/ # Logger
│ ├── mixins/ # Reusable mixins
│ ├── network/ # Dio client, response wrapper, exceptions,
│ │ └── offline_queue/ # interceptors, network monitor, offline queue
│ ├── permission/ # Permission request abstraction
│ └── utils/ # Date, form validation, debounce/throttle,
│ # device info, URL utilities
├── shared/
│ └── widgets/ # Reusable UI components
├── features/
│ └── demo/ # Capability demo page — removable
└── main.dart # Entry point
```
## Built-in Capabilities
- **Networking** — ApiClient (Dio) + ApiResponse + ApiException + header interceptor
- **Network Monitoring** — Real-time connectivity via connectivity_plus
- **Offline Queue** — Request queuing, persistence, and auto-replay on reconnect
- **Local Cache** — AppStorage wrapping SharedPreferences
- **State Management** — Riverpod provider system
- **Theming** — Material 3 light/dark themes + screen adaptation
- **UI Components** — AppButton, AppTextField, AppCard, AppDialog, AppToast, AppEmptyView, AppErrorView, AppLoadingView, AppStatusView, AppAvatar, AppTag, AppSearchBar, AppRefreshList, AppNetworkImage, SafeAreaWrapper
- **Utilities** — Date formatting, form validation, debounce/throttle, device info, URL utils
- **Project Config** — flutter_lints, Android/iOS platform projects
## Getting Started
```bash
cd flutter-template
flutter pub get
flutter analyze
flutter test
flutter run
```
```bash
flutter run -d <device-id>
flutter build apk --debug
flutter build ios --debug --no-codesign
```
## Onboarding a New Project
1. Update API base URLs in `lib/app/config/app_config.dart`
2. Add feature modules under `lib/features/`
3. Use ApiClient via `core/network/providers/dio_providers.dart`
4. Keep business-specific cache keys in your feature module; `core/cache/storage_keys.dart` for truly shared keys only
5. Delete `features/demo/` or replace it with your own home page

View File

@@ -1,3 +1,93 @@
# record-tool
[![English](https://img.shields.io/badge/Lang-English-blue)](README.en.md)
录制助手 flutter
# Flutter Template
一个从实际项目中提炼的 Flutter 通用快速开发模板,开箱即用,避免重复搭建工程基础设施。
## 目标
- 支持 Android 和 iOS
- 不包含原业务页面、接口、Model、资源、文案和私有配置
- 保留通用工程化能力:路由、主题、网络、缓存、日志、权限、工具类、状态管理和常用 UI 组件
- 默认首页为 demo 页面,可直接替换为新业务首页
## 技术栈
| 类别 | 依赖 | 用途 |
|---|---|---|
| 状态管理 | flutter_riverpod | 编译安全、可测试的状态管理 |
| 网络请求 | dio | HTTP 客户端,支持拦截器链 |
| 本地缓存 | shared_preferences | KV 持久化存储 |
| 网络监听 | connectivity_plus | 实时网络状态监测 |
| 权限申请 | permission_handler | 运行时权限请求 |
| 屏幕适配 | flutter_screenutil | 设计稿尺寸适配 |
| 图片加载 | cached_network_image | 网络图片缓存 |
| SVG | flutter_svg | SVG 渲染 |
| 下拉刷新 | pull_to_refresh | 下拉刷新 / 上拉加载 |
| 加载提示 | flutter_easyloading | Toast 和 loading |
| 设备信息 | device_info_plus | 设备元数据 |
| 应用信息 | package_info_plus | 版本号等应用信息 |
| 链接跳转 | url_launcher | 外部 URL 打开 |
| 代码规范 | flutter_lints | Dart 推荐 lint 规则 |
## 目录结构
```
lib/
├── app/ # 应用壳
│ ├── app.dart # MaterialApp、主题、国际化、全局配置
│ ├── bootstrap.dart # 启动初始化
│ ├── config/ # 多环境配置 (dev/staging/prod)
│ ├── router/ # Navigator 工具和路由栈跟踪
│ └── theme/ # 亮色/暗色主题、颜色、间距
├── core/ # 框架层基础设施
│ ├── cache/ # SharedPreferences 封装和通用 key
│ ├── extensions/ # BuildContext 扩展
│ ├── logging/ # 日志工具
│ ├── mixins/ # 通用 mixin
│ ├── network/ # Dio 封装、响应包装、异常处理
│ │ └── offline_queue/ # 拦截器、网络监听、离线请求队列
│ ├── permission/ # 权限请求封装
│ └── utils/ # 日期、表单校验、防抖节流、设备、URL 工具
├── shared/
│ └── widgets/ # 通用 UI 组件库
├── features/
│ └── demo/ # 模板能力演示页,可删除
└── main.dart # 入口
```
## 内置能力
- **网络层** — ApiClient (Dio) + ApiResponse + ApiException + 请求头拦截器
- **网络监听** — 基于 connectivity_plus 的实时状态追踪
- **离线队列** — 请求入队、持久化、网络恢复自动重放
- **本地缓存** — AppStorage 封装 SharedPreferences
- **状态管理** — Riverpod Provider 体系
- **主题系统** — Material 3 亮/暗双主题 + 屏幕适配
- **UI 组件** — AppButton、AppTextField、AppCard、AppDialog、AppToast、AppEmptyView、AppErrorView、AppLoadingView、AppStatusView、AppAvatar、AppTag、AppSearchBar、AppRefreshList、AppNetworkImage、SafeAreaWrapper
- **工具类** — 日期格式化、表单校验、防抖节流、设备信息、URL 工具
- **工程配置** — flutter_lints、Android/iOS 平台工程
## 快速开始
```bash
cd flutter-template
flutter pub get
flutter analyze
flutter test
flutter run
```
```bash
flutter run -d <device-id>
flutter build apk --debug
flutter build ios --debug --no-codesign
```
## 新业务接入指引
1. 修改 `lib/app/config/app_config.dart` 中的环境地址
2.`lib/features/` 下新增业务模块
3. 通过 `core/network/providers/dio_providers.dart` 获取 ApiClient
4. 业务缓存 key 放在业务模块内,`core/cache/storage_keys.dart` 只放真正通用的 key
5. 删除 `features/demo/` 或替换为业务首页

28
analysis_options.yaml Normal file
View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
prefer_single_quotes: true
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.flutter_template"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.flutter_template"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="flutter_template"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.example.flutter_template
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

24
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

34
ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
ios/Podfile Normal file
View File

@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

67
ios/Podfile.lock Normal file
View File

@@ -0,0 +1,67 @@
PODS:
- connectivity_plus (0.0.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
COCOAPODS: 1.16.2

View File

@@ -0,0 +1,749 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
64BC3B3C75FABAA128C4FE7A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D84231833EC084C45F76D9A0 /* Pods_RunnerTests.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
87DA76CA5231F693C9392B80 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6AA1C86CE710449CC3E8FCD /* Pods_Runner.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2B235D76031429175C8F5973 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
69561E7442E8CC939DB2B482 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
95C6AC1536EA3DBA2D369C55 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
99DBE6263F70650B13C33EAC /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
B6AA1C86CE710449CC3E8FCD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C2D68FA11B326EC41ECE81C8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
D84231833EC084C45F76D9A0 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DFA822DC4965C5AFAFF6BB41 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
969A7C920B7FA8B096ABE9CA /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
64BC3B3C75FABAA128C4FE7A /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
87DA76CA5231F693C9392B80 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0B59A02A3C32BFCA7B12CCCB /* Pods */ = {
isa = PBXGroup;
children = (
69561E7442E8CC939DB2B482 /* Pods-Runner.debug.xcconfig */,
C2D68FA11B326EC41ECE81C8 /* Pods-Runner.release.xcconfig */,
2B235D76031429175C8F5973 /* Pods-Runner.profile.xcconfig */,
95C6AC1536EA3DBA2D369C55 /* Pods-RunnerTests.debug.xcconfig */,
DFA822DC4965C5AFAFF6BB41 /* Pods-RunnerTests.release.xcconfig */,
99DBE6263F70650B13C33EAC /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
292109AD45B1136C0FCAE383 /* Frameworks */ = {
isa = PBXGroup;
children = (
B6AA1C86CE710449CC3E8FCD /* Pods_Runner.framework */,
D84231833EC084C45F76D9A0 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
0B59A02A3C32BFCA7B12CCCB /* Pods */,
292109AD45B1136C0FCAE383 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
310E52B4148392E9B8E18DEE /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
969A7C920B7FA8B096ABE9CA /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
473E31C28AE1D9633F1F84FD /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
CD126DA202245CE8BB749787 /* [CP] Embed Pods Frameworks */,
62D3774184B6C76B8E34C8C2 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
310E52B4148392E9B8E18DEE /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
473E31C28AE1D9633F1F84FD /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
62D3774184B6C76B8E34C8C2 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
CD126DA202245CE8BB749787 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 35634V629S;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterTemplate;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 95C6AC1536EA3DBA2D369C55 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterTemplate.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = DFA822DC4965C5AFAFF6BB41 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterTemplate.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 99DBE6263F70650B13C33EAC /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterTemplate.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 35634V629S;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterTemplate;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 35634V629S;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterTemplate;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
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.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
ios/Runner/Info.plist Normal file
View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Flutter Template</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>flutter_template</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

53
lib/app/app.dart Normal file
View File

@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_template/app/config/app_config.dart';
import 'package:flutter_template/app/router/app_navigator.dart';
import 'package:flutter_template/app/theme/app_theme.dart';
import 'package:flutter_template/features/demo/demo_page.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
class FlutterTemplateApp extends StatelessWidget {
const FlutterTemplateApp({super.key});
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
splitScreenMode: true,
builder: (context, child) {
return MaterialApp(
title: AppConfig.appName,
navigatorKey: AppNavigator.navigatorKey,
navigatorObservers: [RouteTracker()],
debugShowCheckedModeBanner: false,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
supportedLocales: const [Locale('zh', 'CN'), Locale('en', 'US')],
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
builder: EasyLoading.init(
builder: (context, child) {
return MediaQuery(
data: MediaQuery.of(
context,
).copyWith(textScaler: const TextScaler.linear(1)),
child: child ?? const SizedBox.shrink(),
);
},
),
home: RefreshConfiguration(
enableLoadingWhenNoData: false,
headerTriggerDistance: 80,
child: const DemoPage(),
),
);
},
);
}
}

29
lib/app/bootstrap.dart Normal file
View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/app/app.dart';
import 'package:flutter_template/app/config/app_config.dart';
import 'package:flutter_template/core/cache/app_storage.dart';
import 'package:flutter_template/core/logging/app_logger.dart';
import 'package:package_info_plus/package_info_plus.dart';
class AppBootstrapper {
AppBootstrapper._();
static Future<void> bootstrap({
AppEnvironment environment = AppEnvironment.dev,
}) async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await AppStorage.init();
final packageInfo = await PackageInfo.fromPlatform();
AppConfig.configure(environment: environment, packageInfo: packageInfo);
AppLogger.debug('App started in ${AppConfig.current.environment.name}');
runApp(const ProviderScope(child: FlutterTemplateApp()));
}
}

View File

@@ -0,0 +1,48 @@
import 'package:package_info_plus/package_info_plus.dart';
enum AppEnvironment { dev, staging, prod }
class EnvironmentValues {
const EnvironmentValues({
required this.environment,
required this.baseUrl,
required this.enableNetworkLog,
});
final AppEnvironment environment;
final String baseUrl;
final bool enableNetworkLog;
}
class AppConfig {
AppConfig._();
static late EnvironmentValues current;
static PackageInfo? packageInfo;
static const appName = 'Flutter Template';
static void configure({
required AppEnvironment environment,
PackageInfo? packageInfo,
}) {
AppConfig.packageInfo = packageInfo;
current = switch (environment) {
AppEnvironment.dev => const EnvironmentValues(
environment: AppEnvironment.dev,
baseUrl: 'https://example.com/api',
enableNetworkLog: true,
),
AppEnvironment.staging => const EnvironmentValues(
environment: AppEnvironment.staging,
baseUrl: 'https://staging.example.com/api',
enableNetworkLog: true,
),
AppEnvironment.prod => const EnvironmentValues(
environment: AppEnvironment.prod,
baseUrl: 'https://api.example.com',
enableNetworkLog: false,
),
};
}
}

View File

@@ -0,0 +1,189 @@
import 'dart:io';
import 'package:flutter/material.dart';
class AppNavigator {
AppNavigator._();
static final navigatorKey = GlobalKey<NavigatorState>();
static final Set<String> _pushingRoutes = <String>{};
static BuildContext? get context => navigatorKey.currentContext;
static Future<T?> push<T>(
Widget page, {
BuildContext? context,
Object? arguments,
String? name,
bool preventDuplicate = true,
}) async {
final routeName = name ?? page.runtimeType.toString();
if (preventDuplicate && RouteTracker.contains(routeName)) {
return null;
}
if (_pushingRoutes.contains(routeName)) {
return null;
}
_pushingRoutes.add(routeName);
final nav = Navigator.of(
context ?? AppNavigator.context!,
rootNavigator: true,
);
try {
return await nav.push<T>(
SlidePageRoute(
builder: (_) => page,
settings: RouteSettings(name: routeName, arguments: arguments),
),
);
} finally {
WidgetsBinding.instance.addPostFrameCallback((_) {
_pushingRoutes.remove(routeName);
});
}
}
static Future<T?> pushReplacement<T, TO>(
Widget page, {
BuildContext? context,
Object? arguments,
String? name,
}) {
return Navigator.of(
context ?? AppNavigator.context!,
rootNavigator: true,
).pushReplacement<T, TO>(
SlidePageRoute(
builder: (_) => page,
settings: RouteSettings(name: name, arguments: arguments),
),
);
}
static Future<T?> pushAndRemoveUntil<T>(
Widget page, {
BuildContext? context,
RoutePredicate? predicate,
Object? arguments,
String? name,
}) {
return Navigator.of(
context ?? AppNavigator.context!,
rootNavigator: true,
).pushAndRemoveUntil<T>(
SlidePageRoute(
builder: (_) => page,
settings: RouteSettings(name: name, arguments: arguments),
),
predicate ?? (_) => false,
);
}
static Future<T?> pushTransparent<T>(
Widget page, {
BuildContext? context,
Color barrierColor = Colors.black54,
Duration duration = const Duration(milliseconds: 200),
bool dismissible = true,
}) {
return Navigator.of(context ?? AppNavigator.context!).push<T>(
PageRouteBuilder<T>(
opaque: false,
barrierColor: barrierColor,
barrierDismissible: dismissible,
transitionDuration: duration,
reverseTransitionDuration: duration,
pageBuilder: (_, __, ___) => page,
transitionsBuilder: (_, animation, __, child) {
return FadeTransition(opacity: animation, child: child);
},
),
);
}
static void pop<T extends Object?>({BuildContext? context, T? result}) {
Navigator.of(
context ?? AppNavigator.context!,
rootNavigator: true,
).pop<T>(result);
}
static void popTimes({BuildContext? context, int count = 1}) {
var popped = 0;
Navigator.of(context ?? AppNavigator.context!).popUntil((route) {
if (popped < count) {
popped++;
return false;
}
return true;
});
}
}
class SlidePageRoute<T> extends MaterialPageRoute<T> {
SlidePageRoute({required super.builder, super.settings});
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
if (Platform.isAndroid) {
final tween = Tween(
begin: const Offset(1, 0),
end: Offset.zero,
).chain(CurveTween(curve: Curves.easeOutCubic));
return SlideTransition(position: animation.drive(tween), child: child);
}
return Theme.of(context).pageTransitionsTheme.buildTransitions<T>(
this,
context,
animation,
secondaryAnimation,
child,
);
}
}
class RouteTracker extends NavigatorObserver {
static final List<String?> pageStack = [];
bool _isPage(Route<dynamic> route) => route is PageRoute;
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (_isPage(route)) {
pageStack.add(route.settings.name);
}
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (_isPage(route) && pageStack.isNotEmpty) {
pageStack.removeLast();
}
}
@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (_isPage(route)) {
pageStack.remove(route.settings.name);
}
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
if (oldRoute != null && _isPage(oldRoute) && pageStack.isNotEmpty) {
pageStack.removeLast();
}
if (newRoute != null && _isPage(newRoute)) {
pageStack.add(newRoute.settings.name);
}
}
static bool contains(String name) => pageStack.contains(name);
}

View File

@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
class AppTheme {
AppTheme._();
static const seedColor = Color(0xFF2563EB);
static const background = Color(0xFFF6F7FB);
static const surface = Colors.white;
static const textPrimary = Color(0xFF111827);
static const textSecondary = Color(0xFF6B7280);
static const border = Color(0xFFE5E7EB);
static const success = Color(0xFF16A34A);
static const warning = Color(0xFFF59E0B);
static const danger = Color(0xFFDC2626);
static ThemeData get light {
final scheme = ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.light,
surface: surface,
);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
scaffoldBackgroundColor: background,
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
scrolledUnderElevation: 0,
backgroundColor: surface,
foregroundColor: textPrimary,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(88, 44),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size(88, 44),
side: const BorderSide(color: border),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
);
}
static ThemeData get dark {
final scheme = ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.dark,
);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
scrolledUnderElevation: 0,
),
);
}
}
class AppSpacing {
AppSpacing._();
static const double xs = 4;
static const double sm = 8;
static const double md = 12;
static const double lg = 16;
static const double xl = 24;
static const double xxl = 32;
}

68
lib/core/cache/app_storage.dart vendored Normal file
View File

@@ -0,0 +1,68 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
class AppStorage {
AppStorage._();
static SharedPreferences? _prefs;
static Future<void> init() async {
_prefs ??= await SharedPreferences.getInstance();
}
static SharedPreferences get instance {
final prefs = _prefs;
if (prefs == null) {
throw StateError('AppStorage.init() must be called before use.');
}
return prefs;
}
static String? getString(String key) => instance.getString(key);
static Future<bool> setString(String key, String value) {
return instance.setString(key, value);
}
static int getInt(String key, {int defaultValue = 0}) {
return instance.getInt(key) ?? defaultValue;
}
static Future<bool> setInt(String key, int value) {
return instance.setInt(key, value);
}
static bool getBool(String key, {bool defaultValue = false}) {
return instance.getBool(key) ?? defaultValue;
}
static Future<bool> setBool(String key, bool value) {
return instance.setBool(key, value);
}
static List<String> getStringList(String key) {
return instance.getStringList(key) ?? const [];
}
static Future<bool> setStringList(String key, List<String> value) {
return instance.setStringList(key, value);
}
static Map<String, dynamic>? getJson(String key) {
final raw = instance.getString(key);
if (raw == null || raw.isEmpty) return null;
final decoded = jsonDecode(raw);
return decoded is Map<String, dynamic> ? decoded : null;
}
static Future<bool> setJson(String key, Map<String, dynamic> value) {
return instance.setString(key, jsonEncode(value));
}
static bool containsKey(String key) => instance.containsKey(key);
static Future<bool> remove(String key) => instance.remove(key);
static Future<bool> clear() => instance.clear();
}

9
lib/core/cache/storage_keys.dart vendored Normal file
View File

@@ -0,0 +1,9 @@
class StorageKeys {
StorageKeys._();
static const authToken = 'auth_token';
static const locale = 'locale';
static const themeMode = 'theme_mode';
static const offlineQueue = 'offline_queue';
static const offlineDeadQueue = 'offline_dead_queue';
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
extension BuildContextX on BuildContext {
ThemeData get theme => Theme.of(this);
ColorScheme get colors => theme.colorScheme;
TextTheme get textTheme => theme.textTheme;
Size get screenSize => MediaQuery.sizeOf(this);
EdgeInsets get safePadding => MediaQuery.paddingOf(this);
bool get isDarkMode => theme.brightness == Brightness.dark;
bool get isKeyboardVisible => MediaQuery.viewInsetsOf(this).bottom > 0;
void hideKeyboard() {
FocusManager.instance.primaryFocus?.unfocus();
}
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter/foundation.dart';
class AppLogger {
AppLogger._();
static void debug(Object? message, {Object? error, StackTrace? stackTrace}) {
if (!kDebugMode) return;
debugPrint('[DEBUG] $message');
if (error != null) debugPrint('[ERROR] $error');
if (stackTrace != null) debugPrint(stackTrace.toString());
}
static void info(Object? message) {
if (kDebugMode) debugPrint('[INFO] $message');
}
static void warning(Object? message) {
if (kDebugMode) debugPrint('[WARN] $message');
}
}

View File

@@ -0,0 +1,20 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
mixin StreamSubscriptionMixin<T extends StatefulWidget> on State<T> {
final List<StreamSubscription<dynamic>> _subscriptions = [];
void addSubscription(StreamSubscription<dynamic> subscription) {
_subscriptions.add(subscription);
}
@override
void dispose() {
for (final subscription in _subscriptions) {
subscription.cancel();
}
_subscriptions.clear();
super.dispose();
}
}

View File

@@ -0,0 +1,126 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter_template/core/network/api_exception.dart';
import 'package:flutter_template/core/network/api_response.dart';
import 'package:flutter_template/core/network/http_method.dart';
typedef JsonParser<T> = T Function(dynamic json);
class ApiClient {
const ApiClient(this._dio);
final Dio _dio;
Future<T> request<T>(
String path, {
HttpMethod method = HttpMethod.get,
Map<String, dynamic>? queryParameters,
Object? data,
Map<String, dynamic>? headers,
JsonParser<T>? parser,
bool wrapResponse = true,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
}) async {
try {
final response = await _dio.request<dynamic>(
path,
queryParameters: queryParameters,
data: data,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
options: Options(method: method.value, headers: headers),
);
final raw = _decodeIfNeeded(response.data);
if (!wrapResponse) {
return _parseData<T>(raw, parser);
}
if (raw is! Map<String, dynamic>) {
return _parseData<T>(raw, parser);
}
final wrapped = ApiResponse<T>.fromJson(raw, fromJsonT: parser);
if (!wrapped.isSuccess) {
throw ApiException(
code: wrapped.code,
statusCode: response.statusCode,
message: wrapped.message.isEmpty ? '请求失败' : wrapped.message,
details: raw,
);
}
return wrapped.data as T;
} on DioException catch (error) {
throw _mapDioException(error);
}
}
Future<T> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
JsonParser<T>? parser,
bool wrapResponse = true,
}) {
return request<T>(
path,
method: HttpMethod.get,
queryParameters: queryParameters,
parser: parser,
wrapResponse: wrapResponse,
);
}
Future<T> post<T>(
String path, {
Object? data,
JsonParser<T>? parser,
bool wrapResponse = true,
}) {
return request<T>(
path,
method: HttpMethod.post,
data: data,
parser: parser,
wrapResponse: wrapResponse,
);
}
dynamic _decodeIfNeeded(dynamic data) {
if (data is String) {
try {
return jsonDecode(data);
} catch (_) {
return data;
}
}
return data;
}
T _parseData<T>(dynamic data, JsonParser<T>? parser) {
if (parser != null) return parser(data);
return data as T;
}
ApiException _mapDioException(DioException error) {
final statusCode = error.response?.statusCode;
final message = switch (error.type) {
DioExceptionType.connectionTimeout => '网络连接超时',
DioExceptionType.sendTimeout => '请求发送超时',
DioExceptionType.receiveTimeout => '响应接收超时',
DioExceptionType.badCertificate => '证书校验失败',
DioExceptionType.badResponse => '服务器响应异常',
DioExceptionType.cancel => '请求已取消',
DioExceptionType.connectionError => '网络连接不可用',
DioExceptionType.unknown => '网络请求失败',
};
return ApiException(
message: message,
statusCode: statusCode,
details: error,
);
}
}

View File

@@ -0,0 +1,23 @@
class ApiException implements Exception {
const ApiException({
required this.message,
this.code,
this.statusCode,
this.details,
});
final String message;
final int? code;
final int? statusCode;
final Object? details;
@override
String toString() {
final parts = <String>[
if (statusCode != null) 'status=$statusCode',
if (code != null) 'code=$code',
message,
];
return 'ApiException(${parts.join(', ')})';
}
}

View File

@@ -0,0 +1,49 @@
class ApiResponse<T> {
const ApiResponse({required this.code, required this.message, this.data});
final int code;
final String message;
final T? data;
bool get isSuccess => code >= 200 && code < 300;
factory ApiResponse.fromJson(
Map<String, dynamic> json, {
T Function(dynamic json)? fromJsonT,
}) {
return ApiResponse<T>(
code: (json['code'] as num?)?.toInt() ?? 200,
message: (json['message'] ?? json['msg'] ?? '').toString(),
data: fromJsonT == null ? json['data'] as T? : fromJsonT(json['data']),
);
}
}
class PageResponse<T> {
const PageResponse({
required this.items,
required this.total,
this.page,
this.pageSize,
});
final List<T> items;
final int total;
final int? page;
final int? pageSize;
bool get isEmpty => items.isEmpty;
factory PageResponse.fromJson(
Map<String, dynamic> json, {
required T Function(dynamic json) fromJsonT,
}) {
final rawItems = json['items'] ?? json['rows'] ?? json['list'] ?? const [];
return PageResponse<T>(
items: rawItems is List ? rawItems.map(fromJsonT).toList() : <T>[],
total: (json['total'] as num?)?.toInt() ?? 0,
page: (json['page'] as num?)?.toInt(),
pageSize: (json['pageSize'] as num?)?.toInt(),
);
}
}

View File

@@ -0,0 +1,31 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter_template/app/config/app_config.dart';
import 'package:flutter_template/core/cache/app_storage.dart';
import 'package:flutter_template/core/cache/storage_keys.dart';
import 'package:flutter_template/core/utils/device_utils.dart';
class HeaderInterceptor extends Interceptor {
@override
Future<void> onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final token = AppStorage.getString(StorageKeys.authToken);
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
final packageInfo = AppConfig.packageInfo;
final device = await DeviceUtils.deviceInfo();
options.headers.addAll({
'platform': Platform.operatingSystem,
if (packageInfo != null) 'appVersion': packageInfo.version,
'environment': AppConfig.current.environment.name,
...device,
});
handler.next(options);
}
}

View File

@@ -0,0 +1,11 @@
enum HttpMethod {
get('GET'),
post('POST'),
put('PUT'),
patch('PATCH'),
delete('DELETE');
const HttpMethod(this.value);
final String value;
}

View File

@@ -0,0 +1,58 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_template/core/network/network_state.dart';
class NetworkMonitor {
final _controller = StreamController<NetworkState>.broadcast();
final _connectivity = Connectivity();
StreamSubscription<List<ConnectivityResult>>? _subscription;
NetworkState _state = const NetworkState(Reachability.unknown);
Stream<NetworkState> get stream => _controller.stream;
NetworkState get current => _state;
void start() {
try {
_subscription = _connectivity.onConnectivityChanged.listen(
(results) => _setOnline(_isOnline(results)),
onError: (Object error, StackTrace stackTrace) {
debugPrint('[NetworkMonitor] connectivity error: $error');
},
);
_connectivity.checkConnectivity().then(
(results) => _setOnline(_isOnline(results)),
onError: (Object error, StackTrace stackTrace) {
debugPrint('[NetworkMonitor] check error: $error');
},
);
} on MissingPluginException catch (error) {
debugPrint('[NetworkMonitor] plugin unavailable: $error');
}
}
bool _isOnline(List<ConnectivityResult> results) {
if (results.isEmpty) return false;
if (results.contains(ConnectivityResult.none)) return false;
return true;
}
void _setOnline(bool online) {
final next = NetworkState(
online ? Reachability.online : Reachability.offline,
);
if (next.reachability == _state.reachability) return;
_state = next;
_controller.add(_state);
}
void dispose() {
_subscription?.cancel();
_controller.close();
}
}

View File

@@ -0,0 +1,11 @@
enum Reachability { unknown, online, offline }
class NetworkState {
const NetworkState(this.reachability);
final Reachability reachability;
bool get isOnline => reachability == Reachability.online;
bool get isOffline => reachability == Reachability.offline;
bool get isUnknown => reachability == Reachability.unknown;
}

View File

@@ -0,0 +1,61 @@
import 'package:dio/dio.dart';
import 'package:flutter_template/core/network/network_monitor.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_manager.dart';
import 'package:flutter_template/core/network/offline_queue/offline_request.dart';
import 'package:uuid/uuid.dart';
class OfflineQueueInterceptor extends Interceptor {
OfflineQueueInterceptor({
required this.monitor,
required this.manager,
this.enabled = false,
});
final NetworkMonitor monitor;
final OfflineQueueManager manager;
final bool enabled;
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (!enabled || options.extra['offline'] != true) {
handler.next(options);
return;
}
if (options.extra['replay'] == true || monitor.current.isUnknown) {
handler.next(options);
return;
}
if (monitor.current.isOffline) {
final extra = options.extra;
final request = OfflineRequest(
id: const Uuid().v4(),
idempotencyKey: (extra['idempotencyKey'] ?? const Uuid().v4())
.toString(),
method: options.method,
path: options.path,
query: options.queryParameters,
body: options.data,
headers: Map<String, dynamic>.from(options.headers),
priority: QueuePriority
.values[(extra['priority'] as int?) ?? QueuePriority.normal.index],
category:
QueueCategory.values[(extra['category'] as int?) ??
QueueCategory.userAction.index],
);
manager.enqueue(request);
handler.reject(
DioException(
requestOptions: options,
type: DioExceptionType.cancel,
message: 'OFFLINE_QUEUED',
),
);
return;
}
handler.next(options);
}
}

View File

@@ -0,0 +1,141 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_template/core/logging/app_logger.dart';
import 'package:flutter_template/core/network/network_monitor.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_state.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_storage.dart';
import 'package:flutter_template/core/network/offline_queue/offline_request.dart';
class OfflineQueueManager {
OfflineQueueManager({
required this.dio,
required this.monitor,
required this.storage,
});
final Dio dio;
final NetworkMonitor monitor;
final OfflineQueueStorage storage;
final _queue = <OfflineRequest>[];
final _dead = <OfflineRequest>[];
final _stateController = StreamController<OfflineQueueState>.broadcast();
StreamSubscription<dynamic>? _networkSubscription;
Timer? _replayDebounceTimer;
bool _replaying = false;
Stream<OfflineQueueState> get stateStream => _stateController.stream;
Future<void> init() async {
_queue.addAll(await storage.loadQueue());
_dead.addAll(await storage.loadDead());
_sort();
_emit();
_networkSubscription = monitor.stream.listen((state) {
if (state.isOffline) {
_replayDebounceTimer?.cancel();
return;
}
if (state.isOnline) {
_replayDebounceTimer?.cancel();
_replayDebounceTimer = Timer(const Duration(seconds: 2), replay);
}
});
}
Future<void> enqueue(OfflineRequest request) async {
if (_isDuplicate(request)) return;
_queue.add(request);
_sort();
await storage.saveQueue(_queue);
_emit();
}
Future<void> replay() async {
if (!monitor.current.isOnline || _replaying || _queue.isEmpty) return;
_replaying = true;
_emit();
try {
while (_queue.isNotEmpty && monitor.current.isOnline) {
final request = _queue.first;
try {
await dio.request<dynamic>(
request.path,
data: request.body,
queryParameters: request.query,
options: Options(
method: request.method,
headers: request.headers,
extra: const {'replay': true, 'offline': false},
),
);
_queue.removeAt(0);
await storage.saveQueue(_queue);
_emit();
} catch (error, stackTrace) {
AppLogger.debug(
'Offline replay failed: ${request.method} ${request.path}',
error: error,
stackTrace: stackTrace,
);
final updated = request.copyWith(retryCount: request.retryCount + 1);
_queue.removeAt(0);
if (updated.isDead) {
_dead.add(updated);
await storage.saveDead(_dead);
} else {
_queue.add(updated);
_sort();
}
await storage.saveQueue(_queue);
_emit();
break;
}
}
} finally {
_replaying = false;
_emit();
}
}
bool _isDuplicate(OfflineRequest request) {
return _queue.any(
(item) =>
(request.idempotencyKey.isNotEmpty &&
item.idempotencyKey == request.idempotencyKey) ||
(item.method == request.method && item.path == request.path),
);
}
void _sort() {
_queue.sort((a, b) {
final priority = a.priority.index.compareTo(b.priority.index);
return priority != 0
? priority
: a.category.index.compareTo(b.category.index);
});
}
void _emit() {
_stateController.add(
OfflineQueueState(
pending: _queue.length,
dead: _dead.length,
replaying: _replaying,
),
);
}
void dispose() {
_replayDebounceTimer?.cancel();
_networkSubscription?.cancel();
_stateController.close();
}
}

View File

@@ -0,0 +1,13 @@
class OfflineQueueState {
const OfflineQueueState({
required this.pending,
required this.dead,
required this.replaying,
});
final int pending;
final int dead;
final bool replaying;
static const empty = OfflineQueueState(pending: 0, dead: 0, replaying: false);
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter_template/core/cache/app_storage.dart';
import 'package:flutter_template/core/cache/storage_keys.dart';
import 'package:flutter_template/core/network/offline_queue/offline_request.dart';
class OfflineQueueStorage {
Future<List<OfflineRequest>> loadQueue() async {
final list = AppStorage.getStringList(StorageKeys.offlineQueue);
return list.map(OfflineRequest.decode).toList();
}
Future<List<OfflineRequest>> loadDead() async {
final list = AppStorage.getStringList(StorageKeys.offlineDeadQueue);
return list.map(OfflineRequest.decode).toList();
}
Future<void> saveQueue(List<OfflineRequest> list) async {
await AppStorage.setStringList(
StorageKeys.offlineQueue,
list.map((item) => item.encode()).toList(),
);
}
Future<void> saveDead(List<OfflineRequest> list) async {
await AppStorage.setStringList(
StorageKeys.offlineDeadQueue,
list.map((item) => item.encode()).toList(),
);
}
}

View File

@@ -0,0 +1,89 @@
import 'dart:convert';
enum QueuePriority { high, normal, low }
enum QueueCategory { userAction, sync, analytics, log }
class OfflineRequest {
const OfflineRequest({
required this.id,
required this.idempotencyKey,
required this.method,
required this.path,
this.query,
this.body,
this.headers,
this.priority = QueuePriority.normal,
this.category = QueueCategory.sync,
this.retryCount = 0,
this.maxRetry = 3,
});
final String id;
final String idempotencyKey;
final String method;
final String path;
final Map<String, dynamic>? query;
final dynamic body;
final Map<String, dynamic>? headers;
final QueuePriority priority;
final QueueCategory category;
final int retryCount;
final int maxRetry;
bool get isDead => retryCount >= maxRetry;
OfflineRequest copyWith({int? retryCount}) {
return OfflineRequest(
id: id,
idempotencyKey: idempotencyKey,
method: method,
path: path,
query: query,
body: body,
headers: headers,
priority: priority,
category: category,
retryCount: retryCount ?? this.retryCount,
maxRetry: maxRetry,
);
}
factory OfflineRequest.fromJson(Map<String, dynamic> json) {
return OfflineRequest(
id: json['id'] as String,
idempotencyKey: json['idempotencyKey'] as String,
method: json['method'] as String,
path: json['path'] as String,
query: (json['query'] as Map?)?.cast<String, dynamic>(),
body: json['body'],
headers: (json['headers'] as Map?)?.cast<String, dynamic>(),
priority: QueuePriority.values[(json['priority'] as num).toInt()],
category: QueueCategory.values[(json['category'] as num).toInt()],
retryCount: (json['retryCount'] as num).toInt(),
maxRetry: (json['maxRetry'] as num).toInt(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'idempotencyKey': idempotencyKey,
'method': method,
'path': path,
'query': query,
'body': body,
'headers': headers,
'priority': priority.index,
'category': category.index,
'retryCount': retryCount,
'maxRetry': maxRetry,
};
}
String encode() => jsonEncode(toJson());
static OfflineRequest decode(String raw) {
return OfflineRequest.fromJson(jsonDecode(raw) as Map<String, dynamic>);
}
}

View File

@@ -0,0 +1,42 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/app/config/app_config.dart';
import 'package:flutter_template/core/network/api_client.dart';
import 'package:flutter_template/core/network/header_interceptor.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_interceptor.dart';
import 'package:flutter_template/core/network/providers/network_providers.dart';
import 'package:flutter_template/core/network/providers/offline_queue_providers.dart';
final dioProvider = Provider<Dio>((ref) {
final dio = Dio(
BaseOptions(
baseUrl: AppConfig.current.baseUrl,
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
responseType: ResponseType.json,
),
);
dio.interceptors.add(HeaderInterceptor());
final monitor = ref.watch(networkMonitorProvider);
final queueManager = ref.watch(offlineQueueManagerProvider);
dio.interceptors.add(
OfflineQueueInterceptor(
monitor: monitor,
manager: queueManager,
enabled: false,
),
);
if (AppConfig.current.enableNetworkLog) {
dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
}
return dio;
});
final apiClientProvider = Provider<ApiClient>((ref) {
return ApiClient(ref.watch(dioProvider));
});

View File

@@ -0,0 +1,14 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/core/network/network_monitor.dart';
import 'package:flutter_template/core/network/network_state.dart';
final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
final monitor = NetworkMonitor()..start();
ref.onDispose(monitor.dispose);
return monitor;
});
final networkStateProvider = StreamProvider<NetworkState>((ref) {
final monitor = ref.watch(networkMonitorProvider);
return monitor.stream;
});

View File

@@ -0,0 +1,29 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_manager.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_state.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_storage.dart';
import 'package:flutter_template/core/network/providers/network_providers.dart';
import 'package:dio/dio.dart';
final offlineQueueStorageProvider = Provider<OfflineQueueStorage>((ref) {
return OfflineQueueStorage();
});
final offlineQueueDioProvider = Provider<Dio>((ref) {
return Dio();
});
final offlineQueueManagerProvider = Provider<OfflineQueueManager>((ref) {
final manager = OfflineQueueManager(
dio: ref.watch(offlineQueueDioProvider),
monitor: ref.watch(networkMonitorProvider),
storage: ref.watch(offlineQueueStorageProvider),
);
manager.init();
ref.onDispose(manager.dispose);
return manager;
});
final offlineQueueStateProvider = StreamProvider<OfflineQueueState>((ref) {
return ref.watch(offlineQueueManagerProvider).stateStream;
});

View File

@@ -0,0 +1,33 @@
import 'package:permission_handler/permission_handler.dart';
class PermissionService {
PermissionService._();
static Future<bool> request(Permission permission) async {
final status = await permission.status;
if (status.isGranted || status.isLimited) return true;
final next = await permission.request();
return next.isGranted || next.isLimited;
}
static Future<Map<Permission, PermissionStatus>> requestAll(
Iterable<Permission> permissions,
) {
return permissions.toList().request();
}
static Future<bool> ensure(
Permission permission, {
bool openSettingsWhenPermanentlyDenied = true,
}) async {
final granted = await request(permission);
if (granted) return true;
final status = await permission.status;
if (openSettingsWhenPermanentlyDenied && status.isPermanentlyDenied) {
await openAppSettings();
}
return false;
}
}

View File

@@ -0,0 +1,34 @@
import 'package:intl/intl.dart';
class DateTimeFormatter {
DateTimeFormatter._();
static String format(
DateTime? value, {
String pattern = 'yyyy-MM-dd HH:mm',
String locale = 'zh_CN',
}) {
if (value == null) return '';
return DateFormat(pattern, locale).format(value);
}
static String dayOrDate(String? isoString, {DateTime? now}) {
if (isoString == null || isoString.trim().isEmpty) return '';
final parsed = DateTime.tryParse(isoString)?.toLocal();
if (parsed == null) return '';
final current = (now ?? DateTime.now()).toLocal();
final currentDate = DateTime(current.year, current.month, current.day);
final parsedDate = DateTime(parsed.year, parsed.month, parsed.day);
final diffDays = currentDate.difference(parsedDate).inDays;
final time = DateFormat('HH:mm').format(parsed);
return switch (diffDays) {
0 => '今天 $time',
1 => '昨天 $time',
2 => '前天 $time',
_ => DateFormat('yyyy-MM-dd').format(parsed),
};
}
}

View File

@@ -0,0 +1,54 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
class DeviceUtils {
DeviceUtils._();
static double screenWidth(BuildContext context) =>
MediaQuery.sizeOf(context).width;
static double screenHeight(BuildContext context) =>
MediaQuery.sizeOf(context).height;
static double topSafePadding(BuildContext context) =>
MediaQuery.paddingOf(context).top;
static double bottomSafePadding(BuildContext context) =>
MediaQuery.paddingOf(context).bottom;
static Future<bool> isPhysicalDevice() async {
final plugin = DeviceInfoPlugin();
if (Platform.isAndroid) {
return (await plugin.androidInfo).isPhysicalDevice;
}
if (Platform.isIOS) {
return (await plugin.iosInfo).isPhysicalDevice;
}
return true;
}
static Future<Map<String, String>> deviceInfo() async {
final plugin = DeviceInfoPlugin();
if (Platform.isAndroid) {
final info = await plugin.androidInfo;
return {
'platform': 'android',
'brand': info.brand,
'model': info.model,
'systemVersion': info.version.release,
};
}
if (Platform.isIOS) {
final info = await plugin.iosInfo;
return {
'platform': 'ios',
'brand': info.systemName,
'model': info.utsname.machine,
'systemVersion': info.systemVersion,
};
}
return {'platform': Platform.operatingSystem};
}
}

View File

@@ -0,0 +1,25 @@
class FormValidators {
FormValidators._();
static String? required(String? value, {String message = '请输入内容'}) {
if (value == null || value.trim().isEmpty) return message;
return null;
}
static String? email(String? value, {String message = '请输入有效邮箱'}) {
if (value == null || value.trim().isEmpty) return null;
final regex = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');
return regex.hasMatch(value.trim()) ? null : message;
}
static String? phoneCN(String? value, {String message = '请输入有效手机号'}) {
if (value == null || value.trim().isEmpty) return null;
final regex = RegExp(r'^1[3-9]\d{9}$');
return regex.hasMatch(value.trim()) ? null : message;
}
static String? minLength(String? value, int min, {String? message}) {
if (value == null || value.isEmpty) return null;
return value.length >= min ? null : message ?? '至少输入 $min 个字符';
}
}

View File

@@ -0,0 +1,166 @@
import 'dart:async';
enum RateLimitMode { debounce, throttle }
class ThrottleOptions {
const ThrottleOptions({this.leading = true, this.trailing = true});
final bool leading;
final bool trailing;
}
class DebounceThrottle<T> {
DebounceThrottle({
required this.duration,
required this.mode,
this.onCallback,
this.throttleOptions = const ThrottleOptions(),
});
final Duration duration;
final RateLimitMode mode;
final void Function(T value)? onCallback;
final ThrottleOptions throttleOptions;
Timer? _timer;
T? _lastValue;
DateTime? _lastExecuteTime;
bool _waitingTrailing = false;
void call(T value) {
_lastValue = value;
switch (mode) {
case RateLimitMode.debounce:
_debounce(value);
case RateLimitMode.throttle:
_throttle(value);
}
}
void _debounce(T value) {
_timer?.cancel();
_timer = Timer(duration, () {
onCallback?.call(value);
_lastExecuteTime = DateTime.now();
});
}
void _throttle(T value) {
final now = DateTime.now();
final last = _lastExecuteTime;
final inWindow = last != null && now.difference(last) < duration;
if (!inWindow && throttleOptions.leading) {
onCallback?.call(value);
_lastExecuteTime = now;
_waitingTrailing = false;
_timer?.cancel();
return;
}
if (!throttleOptions.trailing || _waitingTrailing) return;
_waitingTrailing = true;
final remaining = last == null ? duration : duration - now.difference(last);
_timer?.cancel();
_timer = Timer(remaining, () {
final value = _lastValue;
if (value != null) onCallback?.call(value);
_lastExecuteTime = DateTime.now();
_waitingTrailing = false;
});
}
void flush() {
_timer?.cancel();
final value = _lastValue;
if (value != null) {
onCallback?.call(value);
_lastExecuteTime = DateTime.now();
}
_lastValue = null;
_waitingTrailing = false;
}
void cancel() {
_timer?.cancel();
_lastValue = null;
_waitingTrailing = false;
}
void dispose() => cancel();
}
class RateLimitHub {
RateLimitHub({this.removeDebouncerOnDone = true});
final bool removeDebouncerOnDone;
final Map<Object, DebounceThrottle<dynamic>> _debouncers = {};
final Map<Object, DebounceThrottle<dynamic>> _throttlers = {};
void debounce<T>({
required Object key,
required T value,
Duration duration = const Duration(milliseconds: 300),
required void Function(T value) onCallback,
}) {
_debouncers[key]?.cancel();
late DebounceThrottle<T> limiter;
limiter = DebounceThrottle<T>(
duration: duration,
mode: RateLimitMode.debounce,
onCallback: (value) {
onCallback(value);
if (removeDebouncerOnDone) _debouncers.remove(key);
},
);
_debouncers[key] = limiter;
limiter(value);
}
void throttle<T>({
required Object key,
required T value,
Duration duration = const Duration(milliseconds: 300),
ThrottleOptions options = const ThrottleOptions(),
required void Function(T value) onCallback,
}) {
_throttlers.putIfAbsent(
key,
() =>
DebounceThrottle<T>(
duration: duration,
mode: RateLimitMode.throttle,
throttleOptions: options,
onCallback: onCallback,
)
as DebounceThrottle<dynamic>,
);
(_throttlers[key] as DebounceThrottle<T>)(value);
}
void cancel(Object key) {
_debouncers.remove(key)?.cancel();
_throttlers.remove(key)?.cancel();
}
void clear() {
for (final limiter in _debouncers.values) {
limiter.dispose();
}
for (final limiter in _throttlers.values) {
limiter.dispose();
}
_debouncers.clear();
_throttlers.clear();
}
}
class RateLimit {
RateLimit._();
static final instance = RateLimitHub();
}

View File

@@ -0,0 +1,19 @@
class UrlUtils {
UrlUtils._();
static String buildQueryString(Map<String, dynamic>? params) {
if (params == null || params.isEmpty) return '';
final queryParams = <String, String>{};
for (final entry in params.entries) {
final value = entry.value;
if (value == null) continue;
if (value is String || value is num || value is bool) {
queryParams[entry.key] = value.toString();
}
}
if (queryParams.isEmpty) return '';
return '?${Uri(queryParameters: queryParams).query}';
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
class DemoState {
const DemoState({this.count = 0, this.query = ''});
final int count;
final String query;
DemoState copyWith({int? count, String? query}) {
return DemoState(count: count ?? this.count, query: query ?? this.query);
}
}
class DemoController extends Notifier<DemoState> {
@override
DemoState build() => const DemoState();
void increment() {
state = state.copyWith(count: state.count + 1);
}
void updateQuery(String query) {
state = state.copyWith(query: query);
}
}
final demoControllerProvider = NotifierProvider<DemoController, DemoState>(
DemoController.new,
);

View File

@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/app/theme/app_theme.dart';
import 'package:flutter_template/features/demo/demo_controller.dart';
import 'package:flutter_template/shared/widgets/widgets.dart';
class DemoPage extends ConsumerWidget {
const DemoPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(demoControllerProvider);
final controller = ref.read(demoControllerProvider.notifier);
return Scaffold(
appBar: AppBar(title: const Text('Flutter Template')),
body: SafeAreaWrapper(
child: ListView(
padding: const EdgeInsets.all(AppSpacing.lg),
children: [
AppSearchBar(hint: '搜索模板组件', onChanged: controller.updateQuery),
const SizedBox(height: AppSpacing.lg),
AppCard(
child: Row(
children: [
const AppAvatar(initials: 'T', size: 48),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'通用 Flutter 快速开发模板',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
'已内置网络、缓存、路由、主题、权限、日志和常用 UI 组件。',
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
),
const SizedBox(height: AppSpacing.md),
Wrap(
spacing: 8,
runSpacing: 8,
children: const [
AppTag(label: 'Riverpod', tone: AppTagTone.info),
AppTag(label: 'Dio', tone: AppTagTone.success),
AppTag(label: '缓存', tone: AppTagTone.warning),
AppTag(label: '无业务代码'),
],
),
const SizedBox(height: AppSpacing.lg),
AppCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'状态管理示例',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: AppSpacing.sm),
Text('当前计数:${state.count}'),
if (state.query.isNotEmpty) ...[
const SizedBox(height: AppSpacing.sm),
Text('搜索关键字:${state.query}'),
],
const SizedBox(height: AppSpacing.md),
AppButton(
label: '增加计数',
icon: const Icon(Icons.add, size: 18),
onPressed: controller.increment,
),
],
),
),
const SizedBox(height: AppSpacing.lg),
AppStatusView(
status: AppViewStatus.empty,
empty: AppEmptyView(
title: '空状态组件',
message: '业务项目可替换图标、文案和操作按钮。',
action: AppButton(
label: '显示确认弹窗',
variant: AppButtonVariant.outline,
icon: const Icon(Icons.open_in_new, size: 18),
onPressed: () async {
final confirmed = await AppDialog.confirm(
context,
title: '模板弹窗',
message: '这是可复用的确认弹窗示例。',
);
if (confirmed == true) {
AppToast.show('已确认');
}
},
),
),
child: const SizedBox.shrink(),
),
],
),
),
);
}
}

3
lib/main.dart Normal file
View File

@@ -0,0 +1,3 @@
import 'package:flutter_template/app/bootstrap.dart';
Future<void> main() => AppBootstrapper.bootstrap();

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_template/shared/widgets/app_network_image.dart';
class AppAvatar extends StatelessWidget {
const AppAvatar({super.key, this.imageUrl, this.initials, this.size = 40});
final String? imageUrl;
final String? initials;
final double size;
@override
Widget build(BuildContext context) {
final radius = BorderRadius.circular(size / 2);
return ClipRRect(
borderRadius: radius,
child: SizedBox.square(
dimension: size,
child: imageUrl == null || imageUrl!.isEmpty
? ColoredBox(
color: Theme.of(context).colorScheme.primaryContainer,
child: Center(
child: Text(
(initials == null || initials!.isEmpty)
? 'A'
: initials!.characters.first.toUpperCase(),
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontWeight: FontWeight.w700,
),
),
),
)
: AppNetworkImage(url: imageUrl!, fit: BoxFit.cover),
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More