diff --git a/CHANGELOG.md b/CHANGELOG.md index df946d50d..c42a9aac4 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## NEXT 1. [dart]添加HDR/HEIC/HEIF/TIFF/WBMP/WEBP等图片格式的测试案例 +2. **[架构] 将 flutter_boost 迁移为联邦插件架构** + - 新增 `flutter_boost_platform_interface` 包(平台接口层) + - 新增 `flutter_boost_android` 包(Android 平台实现) + - 新增 `flutter_boost_ios` 包(iOS 平台实现) + - 详细迁移记录请参阅 `FEDERATED_PLUGIN_MIGRATION.md` ## 5.0.2 1. Adapt to the official engine's App Lifecycle state changes diff --git a/FEDERATED_PLUGIN_MIGRATION.md b/FEDERATED_PLUGIN_MIGRATION.md new file mode 100644 index 000000000..471bf4353 --- /dev/null +++ b/FEDERATED_PLUGIN_MIGRATION.md @@ -0,0 +1,320 @@ +# Flutter Boost 联邦插件迁移记录 + +本文档记录了将 `flutter_boost` 插件迁移为 Flutter 联邦插件架构的过程。 + +## 迁移概述 + +### 什么是联邦插件 + +联邦插件(Federated Plugin)是 Flutter 官方推荐的插件架构,它将插件拆分为多个独立的包: + +1. **平台接口包** (`flutter_boost_platform_interface`): 定义平台无关的抽象接口 +2. **平台实现包** (`flutter_boost_android`, `flutter_boost_ios`): 各平台的具体实现 +3. **应用层包** (`flutter_boost`): 对外暴露 API,聚合各平台实现 + +### 迁移优势 + +- 更好的代码组织和模块化 +- 支持添加新平台而无需修改现有代码 +- 允许第三方提供替代的平台实现 +- 更清晰的依赖关系 + +--- + +## 迁移步骤详细记录 + +### 1. 创建 `flutter_boost_platform_interface` 包 + +**目录**: `flutter_boost_platform_interface/` + +#### 1.1 创建 `pubspec.yaml` + +**文件**: `flutter_boost_platform_interface/pubspec.yaml` + +**修改内容**: +- 创建新的 pubspec.yaml 文件 +- 添加 `plugin_platform_interface` 依赖 +- 保持与主包相同的版本号 (5.0.2) + +**原因**: 平台接口包需要依赖 `plugin_platform_interface` 来实现标准的平台接口模式。 + +#### 1.2 迁移 `messages.dart` + +**文件**: `flutter_boost_platform_interface/lib/src/messages.dart` + +**修改内容**: +- 将原 `lib/src/messages.dart` 复制到平台接口包 +- 保持 Pigeon 生成的代码不变 + +**原因**: `messages.dart` 包含了 `NativeRouterApi` 和 `FlutterRouterApi` 的定义,是 Flutter 与原生平台通信的核心接口,应该放在平台接口层。 + +#### 1.3 创建平台接口抽象类 + +**文件**: `flutter_boost_platform_interface/lib/src/flutter_boost_platform_interface.dart` + +**修改内容**: +- 创建 `FlutterBoostPlatform` 抽象类 +- 继承 `PlatformInterface` +- 定义平台接口方法 + +**原因**: 根据联邦插件最佳实践,平台接口需要继承 `PlatformInterface` 并提供标准的实例访问模式。 + +#### 1.4 创建方法通道默认实现 + +**文件**: `flutter_boost_platform_interface/lib/src/method_channel_flutter_boost.dart` + +**修改内容**: +- 创建 `MethodChannelFlutterBoost` 类 +- 实现 `FlutterBoostPlatform` 接口 +- 提供基于方法通道的默认实现 + +**原因**: 提供默认的平台通信实现,各平台实现包可以继承或替换此实现。 + +#### 1.5 创建主库导出文件 + +**文件**: `flutter_boost_platform_interface/lib/flutter_boost_platform_interface.dart` + +**修改内容**: +- 创建库导出文件 +- 导出所有公开 API + +**原因**: 统一的导出点,方便其他包引用。 + +--- + +### 2. 创建 `flutter_boost_android` 包 + +**目录**: `flutter_boost_android/` + +#### 2.1 创建 `pubspec.yaml` + +**文件**: `flutter_boost_android/pubspec.yaml` + +**修改内容**: +- 创建新的 pubspec.yaml 文件 +- 添加对 `flutter_boost_platform_interface` 的依赖 +- 配置 `flutter.plugin.implements` 为 `flutter_boost` +- 配置 `flutter.plugin.platforms.android` 指定原生插件类 + +**原因**: 联邦插件的平台实现包需要声明它实现了哪个插件,并指定原生插件类。 + +#### 2.2 迁移 Android 原生代码 + +**操作**: 将 `android/` 目录复制到 `flutter_boost_android/android/` + +**修改内容**: +- 复制所有 Android 原生代码 +- 保持包名 `com.idlefish.flutterboost` 不变 +- 保持插件类 `FlutterBoostPlugin` 不变 + +**原因**: Android 原生代码无需修改,只需移动到对应的平台实现包中。 + +#### 2.3 创建空的 Dart 库文件 + +**文件**: `flutter_boost_android/lib/flutter_boost_android.dart` + +**修改内容**: +- 创建空的库文件(平台实现包不需要导出 Dart 代码) + +**原因**: 平台实现包主要提供原生实现,Dart 层面不需要导出内容。 + +--- + +### 3. 创建 `flutter_boost_ios` 包 + +**目录**: `flutter_boost_ios/` + +#### 3.1 创建 `pubspec.yaml` + +**文件**: `flutter_boost_ios/pubspec.yaml` + +**修改内容**: +- 创建新的 pubspec.yaml 文件 +- 添加对 `flutter_boost_platform_interface` 的依赖 +- 配置 `flutter.plugin.implements` 为 `flutter_boost` +- 配置 `flutter.plugin.platforms.ios` 指定原生插件类 + +**原因**: 联邦插件的平台实现包需要声明它实现了哪个插件,并指定原生插件类。 + +#### 3.2 迁移 iOS 原生代码 + +**操作**: 将 `ios/` 目录复制到 `flutter_boost_ios/ios/` + +**修改内容**: +- 复制所有 iOS 原生代码 +- 重命名 podspec 文件为 `flutter_boost_ios.podspec` +- 更新 podspec 中的 `s.name` 为 `flutter_boost_ios` +- 更新版本号为 `5.0.2` +- 更新 homepage 为正确的 GitHub 地址 + +**原因**: iOS 原生代码需要移动到对应的平台实现包,并更新 podspec 配置。 + +#### 3.3 创建空的 Dart 库文件 + +**文件**: `flutter_boost_ios/lib/flutter_boost_ios.dart` + +**修改内容**: +- 创建空的库文件(平台实现包不需要导出 Dart 代码) + +**原因**: 平台实现包主要提供原生实现,Dart 层面不需要导出内容。 + +--- + +### 4. 修改主包 `flutter_boost` + +**目录**: `flutter_boost/`(根目录) + +#### 4.1 更新 `pubspec.yaml` + +**文件**: `pubspec.yaml` + +**修改内容**: +- 添加对 `flutter_boost_platform_interface` 的依赖 +- 添加对 `flutter_boost_android` 的依赖 +- 添加对 `flutter_boost_ios` 的依赖 +- 修改 `flutter.plugin.platforms.android` 配置,使用 `default_package: flutter_boost_android` +- 修改 `flutter.plugin.platforms.ios` 配置,使用 `default_package: flutter_boost_ios` +- 移除原来的 `package` 和 `pluginClass` 配置 + +**原因**: 联邦插件的主包需要聚合所有平台实现包,并使用 `default_package` 指定默认的平台实现。 + +#### 4.2 更新 `lib/flutter_boost.dart` + +**文件**: `lib/flutter_boost.dart` + +**修改内容**: +- 添加从 `flutter_boost_platform_interface` 导出核心类型 +- 保持其他导出不变 + +**原因**: 主包需要重新导出平台接口中的公共类型,保持 API 向后兼容。 + +#### 4.3 更新 `lib/src/messages.dart` + +**文件**: `lib/src/messages.dart` + +**修改内容**: +- 将文件内容替换为重新导出语句 +- 从 `flutter_boost_platform_interface` 重新导出所有消息类型 + +**原因**: 避免代码重复,统一使用平台接口包中的定义。 + +--- + +### 5. 更新 Example 目录 + +**目录**: `example/` + +#### 5.1 更新 `example/pubspec.yaml` + +**文件**: `example/pubspec.yaml` + +**修改内容**: +- 更新 SDK 版本约束为 `>=3.2.0 <4.0.0` +- 将 `flutter_boost` 从 `dev_dependencies` 移动到 `dependencies` + +**原因**: +- SDK 版本需要与主包保持一致 +- `flutter_boost` 是 example 的运行时依赖,应放在 `dependencies` 中 + +--- + +## 目录结构对比 + +### 迁移前 + +``` +flutter_boost/ +├── android/ # Android 原生代码 +├── ios/ # iOS 原生代码 +├── lib/ +│ ├── flutter_boost.dart # 主库文件 +│ └── src/ +│ ├── messages.dart # Pigeon 生成的消息代码 +│ └── ... # 其他 Dart 文件 +├── example/ # 示例项目 +└── pubspec.yaml +``` + +### 迁移后 + +``` +flutter_boost/ +├── flutter_boost_platform_interface/ # 平台接口包 +│ ├── lib/ +│ │ ├── flutter_boost_platform_interface.dart +│ │ └── src/ +│ │ ├── flutter_boost_platform_interface.dart +│ │ ├── method_channel_flutter_boost.dart +│ │ └── messages.dart +│ └── pubspec.yaml +├── flutter_boost_android/ # Android 平台实现包 +│ ├── android/ # Android 原生代码 +│ ├── lib/ +│ │ └── flutter_boost_android.dart +│ └── pubspec.yaml +├── flutter_boost_ios/ # iOS 平台实现包 +│ ├── ios/ # iOS 原生代码 +│ ├── lib/ +│ │ └── flutter_boost_ios.dart +│ └── pubspec.yaml +├── android/ # 保留原 Android 目录(可选删除) +├── ios/ # 保留原 iOS 目录(可选删除) +├── lib/ +│ ├── flutter_boost.dart # 主库文件(更新) +│ └── src/ +│ ├── messages.dart # 重新导出 +│ └── ... # 其他 Dart 文件(不变) +├── example/ # 示例项目(更新) +└── pubspec.yaml # 更新依赖配置 +``` + +--- + +## 功能保持不变 + +此次迁移仅涉及代码组织结构的调整,以下功能保持完全不变: + +1. **Flutter 路由 API**: `BoostNavigator`, `FlutterBoostApp` 等 +2. **原生通信**: `NativeRouterApi`, `FlutterRouterApi` +3. **生命周期管理**: `BoostLifecycleBinding`, `PageVisibilityBinding` +4. **容器管理**: `BoostContainer`, `BoostContainerWidget` +5. **事件通道**: `BoostChannel` +6. **拦截器**: `BoostInterceptor` +7. **所有原生实现**: Android Java 代码和 iOS Objective-C 代码 + +--- + +## 安全修复 + +### 修复 fastjson 安全漏洞 + +**文件**: `android/build.gradle`, `flutter_boost_android/android/build.gradle` + +**修改内容**: +- 将 `com.alibaba:fastjson` 版本从 `1.2.41` 升级到 `1.2.83` + +**原因**: +- 版本 1.2.41 存在多个已知安全漏洞: + - 不安全的反序列化漏洞 (CVE-2022-25845 等) + - 从不受信任的控制范围包含功能的漏洞 +- 版本 1.2.83 已修复这些漏洞 + +--- + +## 验证清单 + +- [x] 平台接口包结构正确 +- [x] Android 平台实现包结构正确 +- [x] iOS 平台实现包结构正确 +- [x] 主包依赖配置正确 +- [x] Example 依赖配置正确 +- [x] 所有公开 API 保持向后兼容 +- [x] 原生代码无修改 + +--- + +## 参考资料 + +- [Flutter 联邦插件文档](https://docs.flutter.dev/development/packages-and-plugins/developing-packages#federated-plugins) +- [plugin_platform_interface 包](https://pub.dev/packages/plugin_platform_interface) +- [Flutter 插件最佳实践](https://docs.flutter.dev/development/packages-and-plugins/developing-packages#plugin-platforms) diff --git a/android/build.gradle b/android/build.gradle index 89e0adc70..b9273f213 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -45,7 +45,7 @@ android { dependencies { compileOnly 'com.google.android.material:material:1.0.0' - compileOnly 'com.alibaba:fastjson:1.2.41' + compileOnly 'com.alibaba:fastjson:1.2.83' } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d97809406..8a82babce 100755 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -10,11 +10,13 @@ description: Demonstrates how to use the flutter_boost plugin. version: 1.0.0+1 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=3.2.0 <4.0.0' dependencies: flutter: sdk: flutter + flutter_boost: + path: ../ # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -28,9 +30,6 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_boost: - path: ../ - # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec diff --git a/flutter_boost_android/.gitignore b/flutter_boost_android/.gitignore new file mode 100644 index 000000000..6d11bab8b --- /dev/null +++ b/flutter_boost_android/.gitignore @@ -0,0 +1,27 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ +pubspec.lock diff --git a/flutter_boost_android/CHANGELOG.md b/flutter_boost_android/CHANGELOG.md new file mode 100644 index 000000000..88c63d483 --- /dev/null +++ b/flutter_boost_android/CHANGELOG.md @@ -0,0 +1,4 @@ +## 5.0.2 + +* Initial release of the Android implementation for flutter_boost. +* Extracted from `flutter_boost` as part of federated plugin migration. diff --git a/flutter_boost_android/LICENSE b/flutter_boost_android/LICENSE new file mode 100755 index 000000000..88ed1cb37 --- /dev/null +++ b/flutter_boost_android/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Alibaba Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/flutter_boost_android/README.md b/flutter_boost_android/README.md new file mode 100644 index 000000000..0c2155bdd --- /dev/null +++ b/flutter_boost_android/README.md @@ -0,0 +1,7 @@ +# flutter_boost_android + +The Android implementation of [flutter_boost](https://pub.dev/packages/flutter_boost). + +## Usage + +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), which means you can simply use `flutter_boost` normally. This package will be automatically included in your app when you do. diff --git a/flutter_boost_android/analysis_options.yaml b/flutter_boost_android/analysis_options.yaml new file mode 100644 index 000000000..8d81c200c --- /dev/null +++ b/flutter_boost_android/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + avoid_print: false diff --git a/flutter_boost_android/android/build.gradle b/flutter_boost_android/android/build.gradle new file mode 100755 index 000000000..b9273f213 --- /dev/null +++ b/flutter_boost_android/android/build.gradle @@ -0,0 +1,56 @@ +group 'com.idlefish.flutterboost' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.library' + +android { + if (project.android.hasProperty("namespace")) { + namespace 'com.idlefish.flutterboost' + } + compileSdkVersion 31 + buildToolsVersion '30.0.2' + defaultConfig { + minSdkVersion 16 + targetSdkVersion 31 + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + } + lintOptions { + disable 'InvalidPackage' + abortOnError false + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + compileOnly 'com.google.android.material:material:1.0.0' + compileOnly 'com.alibaba:fastjson:1.2.83' + +} + +ext { + groupId = 'com.taobao.fleamarket' + artifactId = "FlutterBoost" +} + diff --git a/flutter_boost_android/android/gradle.properties b/flutter_boost_android/android/gradle.properties new file mode 100755 index 000000000..7be3d8b46 --- /dev/null +++ b/flutter_boost_android/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true diff --git a/flutter_boost_android/android/gradle/wrapper/gradle-wrapper.jar b/flutter_boost_android/android/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 000000000..135367700 Binary files /dev/null and b/flutter_boost_android/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/flutter_boost_android/android/gradle/wrapper/gradle-wrapper.properties b/flutter_boost_android/android/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 000000000..5c1b6c95b --- /dev/null +++ b/flutter_boost_android/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/flutter_boost_android/android/gradlew b/flutter_boost_android/android/gradlew new file mode 100755 index 000000000..cccdd3d51 --- /dev/null +++ b/flutter_boost_android/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/flutter_boost_android/android/gradlew.bat b/flutter_boost_android/android/gradlew.bat new file mode 100755 index 000000000..e95643d6a --- /dev/null +++ b/flutter_boost_android/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/flutter_boost_android/android/settings.gradle b/flutter_boost_android/android/settings.gradle new file mode 100755 index 000000000..4e421b91c --- /dev/null +++ b/flutter_boost_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_boost' diff --git a/flutter_boost_android/android/src/main/AndroidManifest.xml b/flutter_boost_android/android/src/main/AndroidManifest.xml new file mode 100755 index 000000000..a6c6bf96b --- /dev/null +++ b/flutter_boost_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Assert.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Assert.java new file mode 100644 index 000000000..8fe6cadcb --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Assert.java @@ -0,0 +1,106 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import java.lang.AssertionError; + +/** + * A set of assert methods. Messages are only displayed when an assert fails. + * + */ +public class Assert { + /** + * Protect constructor since it is a static only class + */ + protected Assert() { + } + + /** + * Asserts that a condition is true. If it isn't it throws + * an AssertionError with the given message. + */ + static public void assertTrue(String message, boolean condition) { + if (!condition) { + fail(message); + } + } + + /** + * Asserts that a condition is true. If it isn't it throws + * an AssertionError. + */ + static public void assertTrue(boolean condition) { + assertTrue(null, condition); + } + + /** + * Asserts that a condition is false. If it isn't it throws + * an AssertionError with the given message. + */ + static public void assertFalse(String message, boolean condition) { + assertTrue(message, !condition); + } + + /** + * Asserts that a condition is false. If it isn't it throws + * an AssertionError. + */ + static public void assertFalse(boolean condition) { + assertFalse(null, condition); + } + + /** + * Fails a test with the given message. + */ + static public void fail(String message) { + if (message == null) { + throw new AssertionError(); + } + throw new AssertionError(message); + } + + /** + * Fails a test with no message. + */ + static public void fail() { + fail(null); + } + + /** + * Asserts that an object isn't null. + */ + static public void assertNotNull(Object object) { + assertNotNull(null, object); + } + + /** + * Asserts that an object isn't null. If it is + * an AssertionError is thrown with the given message. + */ + static public void assertNotNull(String message, Object object) { + assertTrue(message, object != null); + } + + /** + * Asserts that an object is null. If it isn't an {@link AssertionError} is + * thrown. + * Message contains: Expected: but was: object + * + * @param object Object to check or null + */ + static public void assertNull(Object object) { + if (object != null) { + assertNull("Expected: but was: " + object.toString(), object); + } + } + + /** + * Asserts that an object is null. If it is not + * an AssertionError is thrown with the given message. + */ + static public void assertNull(String message, Object object) { + assertTrue(message, object == null); + } +} \ No newline at end of file diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/EventListener.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/EventListener.java new file mode 100644 index 000000000..0d58be9c2 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/EventListener.java @@ -0,0 +1,11 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import java.util.Map; + +public interface EventListener { + void onEvent(String key, Map args); +} \ No newline at end of file diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FBPlatformViewsController.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FBPlatformViewsController.java new file mode 100644 index 000000000..7e5c4327a --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FBPlatformViewsController.java @@ -0,0 +1,109 @@ +package com.idlefish.flutterboost; + +import android.content.Context; +import android.view.View; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorView; +import io.flutter.plugin.platform.PlatformViewsController; +import io.flutter.view.TextureRegistry; + +/** + * + * fix issues: + * + * + * + * @author : Joe Chan + * @date : 2024/5/22 13:35 + */ +public class FBPlatformViewsController extends PlatformViewsController { + + private Context appCtx; + + /** + * 记录PlatformViewsController绑定使用的FlutterView + */ + private FlutterView curFlutterView = null; + + /** + * 占位FlutterView,用于防止不执行完整detach后,内部channelHandler继续响应时,出现空指针异常。 + */ + private FlutterView dummyFlutterView = null; + + public FBPlatformViewsController() { + super(); + } + + @Override + public void attach(@Nullable Context context, @NonNull TextureRegistry textureRegistry, + @NonNull DartExecutor dartExecutor) { + if (appCtx == null && context != null) { + appCtx = context.getApplicationContext(); + dummyFlutterView = new FlutterView(appCtx); + } + super.attach(context, textureRegistry, dartExecutor); + } + + @Override + public void detach() { + // 不执行完整的detach,这样就使内部channelHandler正确响应,同时避免platformView触摸事件无法响应 + // super.detach(); + // 使用反射将内部context变量设置为null,一方面解决重新attach时的异常,另一方面解决内存泄漏 + try { + Field contextF = getClass().getSuperclass().getDeclaredField("context"); + contextF.setAccessible(true); + contextF.set(this, null); + } catch (Exception ignore) { + } + destroyOverlaySurfaces(); + } + + @Override + public void attachToView(@NonNull FlutterView newFlutterView) { + if (curFlutterView == null) { + super.attachToView(newFlutterView); + curFlutterView = newFlutterView; + } else if (newFlutterView != curFlutterView) { + removePlatformWrapperOrParents(); + super.attachToView(newFlutterView); + curFlutterView = newFlutterView; + } + } + + + @Override + public void detachFromView() { + if (curFlutterView != null) { + super.detachFromView(); + curFlutterView = null; + //将占位FlutterView绑定上去 + attachToView(dummyFlutterView); + } + } + + public void removePlatformWrapperOrParents() { + if (curFlutterView != null) { + List needRemoveViews = new ArrayList<>(); + int childCount = curFlutterView.getChildCount(); + for (int i = 0; i < childCount; i++) { + View view = curFlutterView.getChildAt(i); + if (view.getClass().getName().contains("PlatformViewWrapper") || view instanceof FlutterMutatorView) { + needRemoveViews.add(view); + } + } + if (!needRemoveViews.isEmpty()) { + for (View needRemoveView : needRemoveViews) { + curFlutterView.removeView(needRemoveView); + } + } + } + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java new file mode 100644 index 000000000..8cad78dca --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java @@ -0,0 +1,360 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + +import com.idlefish.flutterboost.containers.FlutterContainerManager; +import com.idlefish.flutterboost.containers.FlutterViewContainer; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import io.flutter.embedding.android.FlutterEngineProvider; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterEngineCache; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.loader.FlutterLoader; +import io.flutter.view.FlutterMain; + +public class FlutterBoost { + public static final String ENGINE_ID = "flutter_boost_default_engine"; + + private LinkedList activityQueue = null; + private FlutterBoostPlugin plugin; + private boolean isBackForegroundEventOverridden = false; + private boolean isAppInBackground = false; + + + private FlutterBoost() { + } + + private static class LazyHolder { + static final FlutterBoost INSTANCE = new FlutterBoost(); + } + + public static FlutterBoost instance() { + return LazyHolder.INSTANCE; + } + + public interface Callback { + void onStart(FlutterEngine engine); + } + + /** + * Initializes engine and plugin. + * + * @param application the application + * @param delegate the FlutterBoostDelegate + * @param callback Invoke the callback when the engine was started. + */ + public void setup(Application application, FlutterBoostDelegate delegate, Callback callback) { + setup(application, delegate, callback, FlutterBoostSetupOptions.createDefault()); + } + + public void setup(Application application, FlutterBoostDelegate delegate, Callback callback, FlutterBoostSetupOptions options) { + if (options == null) { + options = FlutterBoostSetupOptions.createDefault(); + } + isBackForegroundEventOverridden = options.shouldOverrideBackForegroundEvent(); + FlutterBoostUtils.setDebugLoggingEnabled(options.isDebugLoggingEnabled()); + + // 1. initialize default engine + FlutterEngine engine = getEngine(); + if (engine == null) { + // First, get engine from option.flutterEngineProvider + if (options.flutterEngineProvider() != null) { + FlutterEngineProvider provider = options.flutterEngineProvider(); + engine = provider.provideFlutterEngine(application); + } + + if (engine == null) { + // Second, when the engine from option.flutterEngineProvider is null, + // we should create a new engine + engine = new FlutterEngine(application, null, null, new FBPlatformViewsController(), options.shellArgs(), true); + } + + // Cache the created FlutterEngine in the FlutterEngineCache. + FlutterEngineCache.getInstance().put(ENGINE_ID, engine); + } + + if (!engine.getDartExecutor().isExecutingDart()) { + // Pre-warm the cached FlutterEngine. + engine.getNavigationChannel().setInitialRoute(options.initialRoute()); + engine.getDartExecutor().executeDartEntrypoint(new DartExecutor.DartEntrypoint( + FlutterMain.findAppBundlePath(), options.dartEntrypoint()), options.dartEntrypointArgs()); + } + if (callback != null) callback.onStart(engine); + + // 2. set delegate + getPlugin().setDelegate(delegate); + + //3. register ActivityLifecycleCallbacks + setupActivityLifecycleCallback(application, isBackForegroundEventOverridden); + } + + /** + * Releases the engine resource. + */ + public void tearDown() { + FlutterEngine engine = getEngine(); + if (engine != null) { + engine.destroy(); + FlutterEngineCache.getInstance().remove(ENGINE_ID); + } + activityQueue = null; + plugin = null; + isBackForegroundEventOverridden = false; + isAppInBackground = false; + } + + /** + * Gets the FlutterBoostPlugin. + * + * @return the FlutterBoostPlugin. + */ + public FlutterBoostPlugin getPlugin() { + if (plugin == null) { + FlutterEngine engine = getEngine(); + if (engine == null) { + throw new RuntimeException("FlutterBoost might *not* have been initialized yet!!!"); + } + plugin = FlutterBoostUtils.getPlugin(engine); + } + return plugin; + } + + /** + * Gets the FlutterEngine in use. + * + * @return the FlutterEngine + */ + public FlutterEngine getEngine() { + return FlutterEngineCache.getInstance().get(ENGINE_ID); + } + + /** + * Gets the current activity. + * + * @return the current activity + */ + public Activity currentActivity() { + if (activityQueue != null && !activityQueue.isEmpty()) { + return activityQueue.peek(); + } else { + return null; + } + } + + /** + * Informs FlutterBoost of the back/foreground state. + * + * @param background a boolean indicating if the app goes to background + * or foreground. + */ + public void dispatchBackForegroundEvent(boolean background) { + if (!isBackForegroundEventOverridden) { + throw new RuntimeException("Oops! You should set override enable first by FlutterBoostSetupOptions."); + } + + if (background) { + getPlugin().onBackground(); + } else { + getPlugin().onForeground(); + } + setAppIsInBackground(background); + } + + /** + * Gets the FlutterView container with uniqueId. + *

+ * This is a legacy API for backwards compatibility. + * + * @param uniqueId The uniqueId of the container + * @return a FlutterView container + */ + public FlutterViewContainer findFlutterViewContainerById(String uniqueId) { + return FlutterContainerManager.instance().findContainerById(uniqueId); + } + + /** + * Gets the topmost container + *

+ * This is a legacy API for backwards compatibility. + * + * @return the topmost container + */ + public FlutterViewContainer getTopContainer() { + return FlutterContainerManager.instance().getTopContainer(); + } + + /** + * @param name The Flutter route name. + * @param arguments The bussiness arguments. + * @deprecated use open(FlutterBoostRouteOptions options) instead + * Open a Flutter page with name and arguments. + */ + public void open(String name, Map arguments) { + FlutterBoostRouteOptions options = new FlutterBoostRouteOptions.Builder() + .pageName(name) + .arguments(arguments) + .build(); + getPlugin().getDelegate().pushFlutterRoute(options); + } + + /** + * Use FlutterBoostRouteOptions to open a new Page + * + * @param options FlutterBoostRouteOptions object + */ + public void open(FlutterBoostRouteOptions options) { + getPlugin().getDelegate().pushFlutterRoute(options); + } + + /** + * Close the Flutter page with uniqueId. + * + * @param uniqueId The uniqueId of the Flutter page + */ + public void close(String uniqueId) { + Messages.CommonParams params = new Messages.CommonParams(); + params.setUniqueId(uniqueId); + getPlugin().popRoute(params,new Messages.Result(){ + @Override + public void success(Void result) { + } + + @Override + public void error(Throwable t) { + } + }); + } + + /** + * Change the application life cycle of Flutter + */ + public void changeFlutterAppLifecycle(int state) { + getPlugin().changeFlutterAppLifecycle(state); + } + + /** + * Add a event listener + * + * @param listener + * @return ListenerRemover, you can use this to remove this listener + */ + public ListenerRemover addEventListener(String key, EventListener listener) { + return getPlugin().addEventListener(key, listener); + } + + /** + * Send the event to flutter + * + * @param key the key of this event + * @param args the arguments of this event + */ + public void sendEventToFlutter(String key, Map args) { + getPlugin().sendEventToFlutter(key, args); + } + + public boolean isAppInBackground() { + return isAppInBackground; + } + + /*package*/ void setAppIsInBackground(boolean inBackground) { + isAppInBackground = inBackground; + } + + private void setupActivityLifecycleCallback(Application application, boolean isBackForegroundEventOverridden) { + application.registerActivityLifecycleCallbacks(new BoostActivityLifecycle(isBackForegroundEventOverridden)); + } + + private class BoostActivityLifecycle implements Application.ActivityLifecycleCallbacks { + private int activityReferences = 0; + private boolean isActivityChangingConfigurations = false; + private boolean isBackForegroundEventOverridden = false; + + public BoostActivityLifecycle(boolean isBackForegroundEventOverridden) { + this.isBackForegroundEventOverridden = isBackForegroundEventOverridden; + } + + private void dispatchForegroundEvent() { + if (isBackForegroundEventOverridden) { + return; + } + + FlutterBoost.instance().setAppIsInBackground(false); + FlutterBoost.instance().getPlugin().onForeground(); + } + + private void dispatchBackgroundEvent() { + if (isBackForegroundEventOverridden) { + return; + } + + FlutterBoost.instance().setAppIsInBackground(true); + FlutterBoost.instance().getPlugin().onBackground(); + } + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + if (activityQueue == null) { + activityQueue = new LinkedList(); + } + activityQueue.addFirst(activity); + } + + @Override + public void onActivityStarted(Activity activity) { + if (++activityReferences == 1 && !isActivityChangingConfigurations) { + // App enters foreground + dispatchForegroundEvent(); + } + } + + @Override + public void onActivityResumed(Activity activity) { + if (activityQueue == null) { + activityQueue = new LinkedList(); + activityQueue.addFirst(activity); + } else if(activityQueue.isEmpty()) { + activityQueue.addFirst(activity); + } else if (activityQueue.peek() != activity) { + //针对多tab且每个tab都为Activity,在切换时并不会走remove,所以先从队列中删除再加入 + activityQueue.removeFirstOccurrence(activity); + activityQueue.addFirst(activity); + } + } + + @Override + public void onActivityPaused(Activity activity) { + } + + @Override + public void onActivityStopped(Activity activity) { + isActivityChangingConfigurations = activity.isChangingConfigurations(); + if (--activityReferences == 0 && !isActivityChangingConfigurations) { + // App enters background + dispatchBackgroundEvent(); + } + + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + } + + @Override + public void onActivityDestroyed(Activity activity) { + if (activityQueue != null && !activityQueue.isEmpty()) { + activityQueue.remove(activity); + } + } + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostDelegate.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostDelegate.java new file mode 100644 index 000000000..0aa9e49d1 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostDelegate.java @@ -0,0 +1,13 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +public interface FlutterBoostDelegate { + void pushNativeRoute(FlutterBoostRouteOptions options); + void pushFlutterRoute(FlutterBoostRouteOptions options); + default boolean popRoute(FlutterBoostRouteOptions options){ + return false; + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostPlugin.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostPlugin.java new file mode 100644 index 000000000..cee1228df --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostPlugin.java @@ -0,0 +1,425 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import android.util.Log; +import android.util.SparseArray; + +import com.idlefish.flutterboost.Messages.CommonParams; +import com.idlefish.flutterboost.Messages.FlutterRouterApi; +import com.idlefish.flutterboost.Messages.NativeRouterApi; +import com.idlefish.flutterboost.Messages.StackInfo; +import com.idlefish.flutterboost.containers.FlutterContainerManager; +import com.idlefish.flutterboost.containers.FlutterViewContainer; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; + +public class FlutterBoostPlugin implements FlutterPlugin, NativeRouterApi, ActivityAware { + private static final String TAG = "FlutterBoost_java"; + private static final String APP_LIFECYCLE_CHANGED_KEY = "app_lifecycle_changed_key"; + private static final String LIFECYCLE_STATE = "lifecycleState"; + // See https://github.com/flutter/engine/pull/42418 for details + private static final int FLUTTER_APP_STATE_RESUMED = 1; + private static final int FLUTTER_APP_STATE_PAUSED = 4; + + private FlutterEngine engine; + private FlutterRouterApi channel; + private FlutterBoostDelegate delegate; + private StackInfo dartStack; + private SparseArray pageNames; + private int requestCode = 1000; + + private HashMap> listenersTable = new HashMap<>(); + + private boolean isDebugLoggingEnabled() { + return FlutterBoostUtils.isDebugLoggingEnabled(); + } + + public FlutterRouterApi getChannel() { + return channel; + } + + public void setDelegate(FlutterBoostDelegate delegate) { + this.delegate = delegate; + } + + public FlutterBoostDelegate getDelegate() { + return delegate; + } + + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onAttachedToEngine: " + this); + NativeRouterApi.setup(binding.getBinaryMessenger(), this); + engine = binding.getFlutterEngine(); + channel = new FlutterRouterApi(binding.getBinaryMessenger()); + pageNames = new SparseArray(); + } + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDetachedFromEngine: " + this); + engine = null; + channel = null; + } + + @Override + public void pushNativeRoute(CommonParams params) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#pushNativeRoute: " + params.getUniqueId() + ", " + this); + if (delegate != null) { + requestCode++; + if (pageNames != null) { + pageNames.put(requestCode, params.getPageName()); + } + FlutterBoostRouteOptions options = new FlutterBoostRouteOptions.Builder() + .pageName(params.getPageName()) + .arguments((Map) (Object) params.getArguments()) + .requestCode(requestCode) + .build(); + delegate.pushNativeRoute(options); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* set delegate!"); + } + } + + @Override + public void pushFlutterRoute(CommonParams params) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#pushFlutterRoute: " + params.getUniqueId() + ", " + this); + if (delegate != null) { + FlutterBoostRouteOptions options = new FlutterBoostRouteOptions.Builder() + .pageName(params.getPageName()) + .uniqueId(params.getUniqueId()) + .opaque(params.getOpaque()) + .arguments((Map) (Object) params.getArguments()) + .build(); + delegate.pushFlutterRoute(options); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* set delegate!"); + } + } + + @Override + public void popRoute(CommonParams params, Messages.Result result) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#popRoute: " + params.getUniqueId() + ", " + this); + if (delegate != null) { + FlutterBoostRouteOptions options = new FlutterBoostRouteOptions.Builder() + .pageName(params.getPageName()) + .uniqueId(params.getUniqueId()) + .arguments((Map) (Object) params.getArguments()) + .build(); + boolean isHandle = delegate.popRoute(options); + //isHandle代表是否已经自定义处理,如果未自定义处理走默认逻辑 + if (!isHandle) { + String uniqueId = params.getUniqueId(); + if (uniqueId != null) { + FlutterViewContainer container = FlutterContainerManager.instance().findContainerById(uniqueId); + if (container != null) { + container.finishContainer((Map) (Object) params.getArguments()); + } + result.success(null); + } else { + throw new RuntimeException("Oops!! The unique id is null!"); + } + } else { + //被拦截处理了,那么直接通知result + result.success(null); + } + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* set delegate!"); + } + } + + @Override + public StackInfo getStackFromHost() { + if (dartStack == null) { + return StackInfo.fromMap(new HashMap()); + } + if (isDebugLoggingEnabled()) Log.d(TAG, "#getStackFromHost: " + dartStack + ", " + this); + return dartStack; + } + + @Override + public void saveStackToHost(StackInfo arg) { + dartStack = arg; + if (isDebugLoggingEnabled()) Log.d(TAG, "#saveStackToHost: " + dartStack + ", " + this); + } + + @Override + public void sendEventToNative(CommonParams arg) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#sendEventToNative: " + this); + //deal with the event from flutter side + String key = arg.getKey(); + Map arguments = arg.getArguments(); + assert (key != null); + + if (arguments == null) { + arguments = new HashMap<>(); + } + + List listeners = listenersTable.get(key); + if (listeners == null) { + return; + } + + for (EventListener listener : listeners) { + listener.onEvent(key, arguments); + } + } + + ListenerRemover addEventListener(String key, EventListener listener) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#addEventListener: " + key + ", " + this); + assert (key != null && listener != null); + + LinkedList listeners = listenersTable.get(key); + if (listeners == null) { + listeners = new LinkedList<>(); + listenersTable.put(key, listeners); + } + listeners.add(listener); + + LinkedList finalListeners = listeners; + return () -> finalListeners.remove(listener); + } + + void sendEventToFlutter(String key, Map args) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#sendEventToFlutter: " + key + ", " + this); + Messages.CommonParams params = new Messages.CommonParams(); + params.setKey(key); + params.setArguments(args); + getChannel().sendEventToFlutter(params, reply -> {}); + } + + void changeFlutterAppLifecycle(int state) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#changeFlutterAppLifecycle: " + state + ", " + this); + assert (state == FLUTTER_APP_STATE_PAUSED || state == FLUTTER_APP_STATE_RESUMED); + Map arguments = new HashMap(); + arguments.put(LIFECYCLE_STATE, state); + sendEventToFlutter(APP_LIFECYCLE_CHANGED_KEY, arguments); + } + + private void checkEngineState() { + if (engine == null || !engine.getDartExecutor().isExecutingDart()) { + throw new RuntimeException("The engine is not ready for use. " + + "The message may be drop silently by the engine. " + + "You should check 'DartExecutor.isExecutingDart()' first!"); + } + } + + public void pushRoute(String uniqueId, String pageName, Map arguments, + final FlutterRouterApi.Reply callback) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#pushRoute start: " + pageName + ", " + uniqueId + ", " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + params.setUniqueId(uniqueId); + params.setPageName(pageName); + params.setArguments(arguments); + channel.pushRoute(params, reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#pushRoute end: " + pageName + ", " + uniqueId); + if (callback != null) { + callback.reply(null); + } + }); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void popRoute(String uniqueId, final FlutterRouterApi.Reply callback) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#popRoute start: " + uniqueId + ", " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + params.setUniqueId(uniqueId); + channel.popRoute(params, reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#popRoute end: " + uniqueId + ", " + this); + if (callback != null) { + callback.reply(null); + } + }); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void onBackPressed() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onBackPressed start: " + this); + if (channel != null) { + checkEngineState(); + channel.onBackPressed(reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onBackPressed end: " + this); + }); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void removeRoute(String uniqueId, final FlutterRouterApi.Reply callback) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#removeRoute start: " + uniqueId + ", " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + params.setUniqueId(uniqueId); + channel.removeRoute(params, reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#removeRoute end: " + uniqueId + ", " + this); + if (callback != null) { + callback.reply(null); + } + }); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void onForeground() { + Log.d(TAG, "## onForeground start: " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + channel.onForeground(params, reply -> { + Log.d(TAG, "## onForeground end: " + this); + }); + + // The scheduling frames are resumed when [onForeground] is called. + changeFlutterAppLifecycle(FLUTTER_APP_STATE_RESUMED); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void onBackground() { + Log.d(TAG, "## onBackground start: " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + channel.onBackground(params, reply -> { + Log.d(TAG, "## onBackground end: " + this); + }); + + // The scheduling frames are paused when [onBackground] is called. + changeFlutterAppLifecycle(FLUTTER_APP_STATE_PAUSED); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void onContainerShow(String uniqueId) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerShow start: " + uniqueId + ", " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + params.setUniqueId(uniqueId); + channel.onContainerShow(params, reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerShow end: " + uniqueId + ", " + this); + }); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void onContainerHide(String uniqueId) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerHide start: " + uniqueId + ", " + this); + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + params.setUniqueId(uniqueId); + channel.onContainerHide(params, reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerHide end: " + uniqueId + ", " + this); + }); + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + } + + public void onContainerCreated(FlutterViewContainer container) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerCreated: " + container.getUniqueId() + ", " + this); + FlutterContainerManager.instance().addContainer(container.getUniqueId(), container); + if (FlutterContainerManager.instance().getContainerSize() == 1) { + changeFlutterAppLifecycle(FLUTTER_APP_STATE_RESUMED); + } + } + + public void onContainerAppeared(FlutterViewContainer container, Runnable onPushRouteComplete) { + String uniqueId = container.getUniqueId(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerAppeared: " + uniqueId + ", " + this); + FlutterContainerManager.instance().activateContainer(uniqueId, container); + pushRoute(uniqueId, container.getUrl(), container.getUrlParams(), reply -> { + if (FlutterContainerManager.instance().isTopContainer(uniqueId)) { + if (onPushRouteComplete != null) { + onPushRouteComplete.run(); + } + } + }); + //onContainerDisappeared并非异步触发,为了匹配对应,onContainerShow也不做异步 + onContainerShow(uniqueId); + } + + public void onContainerDisappeared(FlutterViewContainer container) { + String uniqueId = container.getUniqueId(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerDisappeared: " + uniqueId + ", " + this); + onContainerHide(uniqueId); + } + + public void onContainerDestroyed(FlutterViewContainer container) { + String uniqueId = container.getUniqueId(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onContainerDestroyed: " + uniqueId + ", " + this); + removeRoute(uniqueId, reply -> {}); + FlutterContainerManager.instance().removeContainer(uniqueId); + if (FlutterContainerManager.instance().getContainerSize() == 0) { + changeFlutterAppLifecycle(FLUTTER_APP_STATE_PAUSED); + } + } + + @Override + public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onAttachedToActivity: " + this); + activityPluginBinding.addActivityResultListener((requestCode, resultCode, intent) -> { + if (channel != null) { + checkEngineState(); + CommonParams params = new CommonParams(); + String pageName = pageNames.get(requestCode); + pageNames.remove(requestCode); + if (null != pageName) { + params.setPageName(pageName); + if (intent != null) { + Map result = FlutterBoostUtils.bundleToMap(intent.getExtras()); + params.setArguments(result); + } + + // Get a result back from an activity when it ends. + channel.onNativeResult(params, reply -> { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onNativeResult return, pageName=" + pageName + ", " + this); + }); + } + } else { + throw new RuntimeException("FlutterBoostPlugin might *NOT* have attached to engine yet!"); + } + return true; + }); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDetachedFromActivityForConfigChanges: " + this); + } + + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onReattachedToActivityForConfigChanges: " + this); + } + + @Override + public void onDetachedFromActivity() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDetachedFromActivity: " + this); + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostRouteOptions.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostRouteOptions.java new file mode 100644 index 000000000..6fb73b06c --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostRouteOptions.java @@ -0,0 +1,84 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import java.util.Map; + +public class FlutterBoostRouteOptions { + private final String pageName; + private final Map arguments; + private final int requestCode; + private final String uniqueId; + private final boolean opaque; + + private FlutterBoostRouteOptions(FlutterBoostRouteOptions.Builder builder) { + this.pageName = builder.pageName; + this.arguments = builder.arguments; + this.requestCode = builder.requestCode; + this.uniqueId = builder.uniqueId; + this.opaque = builder.opaque; + } + + public String pageName() { + return pageName; + } + + public Map arguments() { + return arguments; + } + + public int requestCode() { + return requestCode; + } + + public String uniqueId() { + return uniqueId; + } + + public boolean opaque() { + return opaque; + } + + public static class Builder { + private String pageName; + private Map arguments; + private int requestCode; + private String uniqueId; + private boolean opaque = true; + + public Builder() { + } + + public FlutterBoostRouteOptions.Builder pageName(String pageName) { + this.pageName = pageName; + return this; + } + + public FlutterBoostRouteOptions.Builder arguments(Map arguments) { + this.arguments = arguments; + return this; + } + + public FlutterBoostRouteOptions.Builder requestCode(int requestCode) { + this.requestCode = requestCode; + return this; + } + + public FlutterBoostRouteOptions.Builder uniqueId(String uniqueId) { + this.uniqueId = uniqueId; + return this; + } + + public FlutterBoostRouteOptions.Builder opaque(boolean opaque) { + this.opaque = opaque; + return this; + } + + public FlutterBoostRouteOptions build() { + return new FlutterBoostRouteOptions(this); + } + } + +} \ No newline at end of file diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostSetupOptions.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostSetupOptions.java new file mode 100644 index 000000000..3a7780ce3 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostSetupOptions.java @@ -0,0 +1,138 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import java.util.List; + +import io.flutter.embedding.android.FlutterEngineProvider; + +public class FlutterBoostSetupOptions { + private final String initialRoute; + private final String dartEntrypoint; + private final List dartEntrypointArgs; + private final String[] shellArgs; + private final boolean isDebugLoggingEnabled; + private final boolean shouldOverrideBackForegroundEvent; + private FlutterEngineProvider flutterEngineProvider; + + private FlutterBoostSetupOptions(Builder builder) { + this.initialRoute = builder.initialRoute; + this.dartEntrypoint = builder.dartEntrypoint; + this.dartEntrypointArgs = builder.dartEntrypointArgs; + this.shellArgs = builder.shellArgs; + this.isDebugLoggingEnabled = builder.isDebugLoggingEnabled; + this.shouldOverrideBackForegroundEvent = builder.shouldOverrideBackForegroundEvent; + this.flutterEngineProvider = builder.flutterEngineProvider; + } + + public static FlutterBoostSetupOptions createDefault() { + return new Builder().build(); + } + + public String initialRoute() { + return initialRoute; + } + + public String dartEntrypoint() { + return dartEntrypoint; + } + + public List dartEntrypointArgs() { + return dartEntrypointArgs; + } + + public String[] shellArgs() { + return shellArgs; + } + + public FlutterEngineProvider flutterEngineProvider() { + return flutterEngineProvider; + } + + public boolean isDebugLoggingEnabled() { + return isDebugLoggingEnabled; + } + + public boolean shouldOverrideBackForegroundEvent() { + return shouldOverrideBackForegroundEvent; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append('['); + if (shellArgs == null || shellArgs.length == 0) { + sb.append(']'); + } else { + for (int i = 0; ; i++) { + sb.append(String.valueOf(shellArgs[i])); + if (i == shellArgs.length - 1) { + sb.append(']'); + break; + } + sb.append(", "); + } + } + return "initialRoute:" + this.initialRoute + + ", dartEntrypoint:" + this.dartEntrypoint + + ", isDebugLoggingEnabled: " + this.isDebugLoggingEnabled + + ", shouldOverrideBackForegroundEvent:" + this.shouldOverrideBackForegroundEvent + + ", shellArgs:" + sb.toString(); + } + + public static class Builder { + private String initialRoute = "/"; + private String dartEntrypoint = "main"; + private List dartEntrypointArgs; + private boolean isDebugLoggingEnabled = false; + private boolean shouldOverrideBackForegroundEvent = false; + private String[] shellArgs; + private FlutterEngineProvider flutterEngineProvider; + + public Builder() { + } + + public Builder initialRoute(String initialRoute){ + this.initialRoute = initialRoute; + return this; + } + + public Builder dartEntrypoint(String dartEntrypoint){ + this.dartEntrypoint = dartEntrypoint; + return this; + } + + public Builder dartEntrypointArgs(List args) { + this.dartEntrypointArgs = args; + return this; + } + + public Builder shellArgs(String[] shellArgs){ + this.shellArgs = shellArgs; + return this; + } + + public Builder flutterEngineProvider(FlutterEngineProvider flutterEngineProvider) { + this.flutterEngineProvider = flutterEngineProvider; + return this; + } + + public Builder isDebugLoggingEnabled(boolean enable) { + isDebugLoggingEnabled = enable; + return this; + } + + // Determines whether to override back/foreground event. + public Builder shouldOverrideBackForegroundEvent(boolean override) { + shouldOverrideBackForegroundEvent = override; + return this; + } + + public FlutterBoostSetupOptions build() { + FlutterBoostSetupOptions options = new FlutterBoostSetupOptions(this); + return options; + } + } +} \ No newline at end of file diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostUtils.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostUtils.java new file mode 100644 index 000000000..deb486d71 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/FlutterBoostUtils.java @@ -0,0 +1,168 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +import android.app.Activity; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.WindowInsetsControllerCompat; +import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.platform.PlatformPlugin; + +/** + * Helper methods to deal with common tasks. + */ +public class FlutterBoostUtils { + // Control whether the internal debugging logs are turned on. + private static boolean sEnableDebugLogging = false; + + public static void setDebugLoggingEnabled(boolean enable) { + sEnableDebugLogging = enable; + } + + public static boolean isDebugLoggingEnabled() { + return sEnableDebugLogging; + } + + public static String createUniqueId(String name) { + return UUID.randomUUID().toString() + "_" + name; + } + + public static FlutterBoostPlugin getPlugin(FlutterEngine engine) { + if (engine != null) { + try { + Class pluginClass = + (Class) Class.forName("com.idlefish.flutterboost.FlutterBoostPlugin"); + return (FlutterBoostPlugin) engine.getPlugins().get(pluginClass); + } catch (Throwable t) { + t.printStackTrace(); + } + } + return null; + } + + public static Map bundleToMap(Bundle bundle) { + Map map = new HashMap<>(); + if (bundle == null || bundle.keySet().isEmpty()) { + return map; + } + Set keys = bundle.keySet(); + for (String key : keys) { + Object value = bundle.get(key); + if (value instanceof Bundle) { + map.put(key, bundleToMap(bundle.getBundle(key))); + } else if (value != null) { + map.put(key, value); + } + } + return map; + } + + public static FlutterView findFlutterView(View view) { + if (view instanceof FlutterView) { + return (FlutterView) view; + } + if (view instanceof ViewGroup) { + ViewGroup vp = (ViewGroup) view; + for (int i = 0; i < vp.getChildCount(); i++) { + View child = vp.getChildAt(i); + FlutterView fv = findFlutterView(child); + if (fv != null) { + return fv; + } + } + } + return null; + } + + @Nullable + public static PlatformChannel.SystemChromeStyle getCurrentSystemUiOverlayTheme(PlatformPlugin platformPlugin, boolean copy) { + if (platformPlugin != null) { + try { + Field field = platformPlugin.getClass().getDeclaredField("currentTheme"); + field.setAccessible(true); + PlatformChannel.SystemChromeStyle style = + (PlatformChannel.SystemChromeStyle) field.get(platformPlugin); + if (!copy || style == null) { + return style; + } else { + return copySystemChromeStyle(style); + } + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + return null; + } + + public static PlatformChannel.SystemChromeStyle copySystemChromeStyle(PlatformChannel.SystemChromeStyle style) { + if (style == null) { + return null; + } + return new PlatformChannel.SystemChromeStyle( + style.statusBarColor, + style.statusBarIconBrightness, + style.systemStatusBarContrastEnforced, + style.systemNavigationBarColor, + style.systemNavigationBarIconBrightness, + style.systemNavigationBarDividerColor, + style.systemNavigationBarContrastEnforced + ); + } + + public static PlatformChannel.SystemChromeStyle mergeSystemChromeStyle(PlatformChannel.SystemChromeStyle old, PlatformChannel.SystemChromeStyle ne_w) { + if (ne_w == null) { + return copySystemChromeStyle(old); + } + if (old == null) { + return copySystemChromeStyle(ne_w); + } + return new PlatformChannel.SystemChromeStyle( + ne_w.statusBarColor != null ? ne_w.statusBarColor : old.statusBarColor, + ne_w.statusBarIconBrightness != null ? ne_w.statusBarIconBrightness : old.statusBarIconBrightness, + ne_w.systemStatusBarContrastEnforced != null ? ne_w.systemStatusBarContrastEnforced : old.systemStatusBarContrastEnforced, + ne_w.systemNavigationBarColor != null ? ne_w.systemNavigationBarColor : old.systemNavigationBarColor, + ne_w.systemNavigationBarIconBrightness != null ? ne_w.systemNavigationBarIconBrightness : old.systemNavigationBarIconBrightness, + ne_w.systemNavigationBarDividerColor != null ? ne_w.systemNavigationBarDividerColor : old.systemNavigationBarDividerColor, + ne_w.systemNavigationBarContrastEnforced != null ? ne_w.systemNavigationBarContrastEnforced : old.systemNavigationBarContrastEnforced + ); + } + + public static void setSystemChromeSystemUIOverlayStyle(@NonNull PlatformPlugin platformPlugin, + @NonNull PlatformChannel.SystemChromeStyle systemChromeStyle) { + try { + Method mth = platformPlugin.getClass().getDeclaredMethod( + "setSystemChromeSystemUIOverlayStyle", PlatformChannel.SystemChromeStyle.class); + mth.setAccessible(true); + mth.invoke(platformPlugin, systemChromeStyle); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/ListenerRemover.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/ListenerRemover.java new file mode 100644 index 000000000..7077e6901 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/ListenerRemover.java @@ -0,0 +1,12 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost; + +/** + * The interface to remove the EventListener added in list + */ +public interface ListenerRemover { + void remove(); +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Messages.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Messages.java new file mode 100644 index 000000000..c4ff23a59 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/Messages.java @@ -0,0 +1,613 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package com.idlefish.flutterboost; + +import android.util.Log; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) +public class Messages { + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class CommonParams { + private Boolean opaque; + public Boolean getOpaque() { return opaque; } + public void setOpaque(Boolean setterArg) { + this.opaque = setterArg; + } + + private String key; + public String getKey() { return key; } + public void setKey(String setterArg) { + this.key = setterArg; + } + + private String pageName; + public String getPageName() { return pageName; } + public void setPageName(String setterArg) { + this.pageName = setterArg; + } + + private String uniqueId; + public String getUniqueId() { return uniqueId; } + public void setUniqueId(String setterArg) { + this.uniqueId = setterArg; + } + + private Map arguments; + public Map getArguments() { return arguments; } + public void setArguments(Map setterArg) { + this.arguments = setterArg; + } + + public static final class Builder { + private Boolean opaque; + public Builder setOpaque(Boolean setterArg) { + this.opaque = setterArg; + return this; + } + private String key; + public Builder setKey(String setterArg) { + this.key = setterArg; + return this; + } + private String pageName; + public Builder setPageName(String setterArg) { + this.pageName = setterArg; + return this; + } + private String uniqueId; + public Builder setUniqueId(String setterArg) { + this.uniqueId = setterArg; + return this; + } + private Map arguments; + public Builder setArguments(Map setterArg) { + this.arguments = setterArg; + return this; + } + public CommonParams build() { + CommonParams pigeonReturn = new CommonParams(); + pigeonReturn.setOpaque(opaque); + pigeonReturn.setKey(key); + pigeonReturn.setPageName(pageName); + pigeonReturn.setUniqueId(uniqueId); + pigeonReturn.setArguments(arguments); + return pigeonReturn; + } + } + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("opaque", opaque); + toMapResult.put("key", key); + toMapResult.put("pageName", pageName); + toMapResult.put("uniqueId", uniqueId); + toMapResult.put("arguments", arguments); + return toMapResult; + } + static CommonParams fromMap(Map map) { + CommonParams pigeonResult = new CommonParams(); + Object opaque = map.get("opaque"); + pigeonResult.setOpaque((Boolean)opaque); + Object key = map.get("key"); + pigeonResult.setKey((String)key); + Object pageName = map.get("pageName"); + pigeonResult.setPageName((String)pageName); + Object uniqueId = map.get("uniqueId"); + pigeonResult.setUniqueId((String)uniqueId); + Object arguments = map.get("arguments"); + pigeonResult.setArguments((Map)arguments); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class StackInfo { + private List ids; + public List getIds() { return ids; } + public void setIds(List setterArg) { + this.ids = setterArg; + } + + private Map containers; + public Map getContainers() { return containers; } + public void setContainers(Map setterArg) { + this.containers = setterArg; + } + + public static final class Builder { + private List ids; + public Builder setIds(List setterArg) { + this.ids = setterArg; + return this; + } + private Map containers; + public Builder setContainers(Map setterArg) { + this.containers = setterArg; + return this; + } + public StackInfo build() { + StackInfo pigeonReturn = new StackInfo(); + pigeonReturn.setIds(ids); + pigeonReturn.setContainers(containers); + return pigeonReturn; + } + } + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("ids", ids); + toMapResult.put("containers", containers); + return toMapResult; + } + static StackInfo fromMap(Map map) { + StackInfo pigeonResult = new StackInfo(); + Object ids = map.get("ids"); + pigeonResult.setIds((List)ids); + Object containers = map.get("containers"); + pigeonResult.setContainers((Map)containers); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class FlutterContainer { + private List pages; + public List getPages() { return pages; } + public void setPages(List setterArg) { + this.pages = setterArg; + } + + public static final class Builder { + private List pages; + public Builder setPages(List setterArg) { + this.pages = setterArg; + return this; + } + public FlutterContainer build() { + FlutterContainer pigeonReturn = new FlutterContainer(); + pigeonReturn.setPages(pages); + return pigeonReturn; + } + } + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("pages", pages); + return toMapResult; + } + static FlutterContainer fromMap(Map map) { + FlutterContainer pigeonResult = new FlutterContainer(); + Object pages = map.get("pages"); + pigeonResult.setPages((List)pages); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class FlutterPage { + private Boolean withContainer; + public Boolean getWithContainer() { return withContainer; } + public void setWithContainer(Boolean setterArg) { + this.withContainer = setterArg; + } + + private String pageName; + public String getPageName() { return pageName; } + public void setPageName(String setterArg) { + this.pageName = setterArg; + } + + private String uniqueId; + public String getUniqueId() { return uniqueId; } + public void setUniqueId(String setterArg) { + this.uniqueId = setterArg; + } + + private Map arguments; + public Map getArguments() { return arguments; } + public void setArguments(Map setterArg) { + this.arguments = setterArg; + } + + public static final class Builder { + private Boolean withContainer; + public Builder setWithContainer(Boolean setterArg) { + this.withContainer = setterArg; + return this; + } + private String pageName; + public Builder setPageName(String setterArg) { + this.pageName = setterArg; + return this; + } + private String uniqueId; + public Builder setUniqueId(String setterArg) { + this.uniqueId = setterArg; + return this; + } + private Map arguments; + public Builder setArguments(Map setterArg) { + this.arguments = setterArg; + return this; + } + public FlutterPage build() { + FlutterPage pigeonReturn = new FlutterPage(); + pigeonReturn.setWithContainer(withContainer); + pigeonReturn.setPageName(pageName); + pigeonReturn.setUniqueId(uniqueId); + pigeonReturn.setArguments(arguments); + return pigeonReturn; + } + } + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("withContainer", withContainer); + toMapResult.put("pageName", pageName); + toMapResult.put("uniqueId", uniqueId); + toMapResult.put("arguments", arguments); + return toMapResult; + } + static FlutterPage fromMap(Map map) { + FlutterPage pigeonResult = new FlutterPage(); + Object withContainer = map.get("withContainer"); + pigeonResult.setWithContainer((Boolean)withContainer); + Object pageName = map.get("pageName"); + pigeonResult.setPageName((String)pageName); + Object uniqueId = map.get("uniqueId"); + pigeonResult.setUniqueId((String)uniqueId); + Object arguments = map.get("arguments"); + pigeonResult.setArguments((Map)arguments); + return pigeonResult; + } + } + + public interface Result { + void success(T result); + void error(Throwable error); + } + private static class NativeRouterApiCodec extends StandardMessageCodec { + public static final NativeRouterApiCodec INSTANCE = new NativeRouterApiCodec(); + private NativeRouterApiCodec() {} + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte)128: + return CommonParams.fromMap((Map) readValue(buffer)); + + case (byte)129: + return FlutterContainer.fromMap((Map) readValue(buffer)); + + case (byte)130: + return FlutterPage.fromMap((Map) readValue(buffer)); + + case (byte)131: + return StackInfo.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + + } + } + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof CommonParams) { + stream.write(128); + writeValue(stream, ((CommonParams) value).toMap()); + } else + if (value instanceof FlutterContainer) { + stream.write(129); + writeValue(stream, ((FlutterContainer) value).toMap()); + } else + if (value instanceof FlutterPage) { + stream.write(130); + writeValue(stream, ((FlutterPage) value).toMap()); + } else + if (value instanceof StackInfo) { + stream.write(131); + writeValue(stream, ((StackInfo) value).toMap()); + } else +{ + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + public interface NativeRouterApi { + void pushNativeRoute(CommonParams param); + void pushFlutterRoute(CommonParams param); + void popRoute(CommonParams param, Result result); + StackInfo getStackFromHost(); + void saveStackToHost(StackInfo stack); + void sendEventToNative(CommonParams params); + + /** The codec used by NativeRouterApi. */ + static MessageCodec getCodec() { + return NativeRouterApiCodec.INSTANCE; + } + + /** Sets up an instance of `NativeRouterApi` to handle messages through the `binaryMessenger`. */ + static void setup(BinaryMessenger binaryMessenger, NativeRouterApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeRouterApi.pushNativeRoute", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + CommonParams paramArg = (CommonParams)args.get(0); + if (paramArg == null) { + throw new NullPointerException("paramArg unexpectedly null."); + } + api.pushNativeRoute(paramArg); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeRouterApi.pushFlutterRoute", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + CommonParams paramArg = (CommonParams)args.get(0); + if (paramArg == null) { + throw new NullPointerException("paramArg unexpectedly null."); + } + api.pushFlutterRoute(paramArg); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeRouterApi.popRoute", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + CommonParams paramArg = (CommonParams)args.get(0); + if (paramArg == null) { + throw new NullPointerException("paramArg unexpectedly null."); + } + Result resultCallback = new Result() { + public void success(Void result) { + wrapped.put("result", null); + reply.reply(wrapped); + } + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.popRoute(paramArg, resultCallback); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeRouterApi.getStackFromHost", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + StackInfo output = api.getStackFromHost(); + wrapped.put("result", output); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeRouterApi.saveStackToHost", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + StackInfo stackArg = (StackInfo)args.get(0); + if (stackArg == null) { + throw new NullPointerException("stackArg unexpectedly null."); + } + api.saveStackToHost(stackArg); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeRouterApi.sendEventToNative", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + CommonParams paramsArg = (CommonParams)args.get(0); + if (paramsArg == null) { + throw new NullPointerException("paramsArg unexpectedly null."); + } + api.sendEventToNative(paramsArg); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static class FlutterRouterApiCodec extends StandardMessageCodec { + public static final FlutterRouterApiCodec INSTANCE = new FlutterRouterApiCodec(); + private FlutterRouterApiCodec() {} + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte)128: + return CommonParams.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + + } + } + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof CommonParams) { + stream.write(128); + writeValue(stream, ((CommonParams) value).toMap()); + } else +{ + super.writeValue(stream, value); + } + } + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + public static class FlutterRouterApi { + private final BinaryMessenger binaryMessenger; + public FlutterRouterApi(BinaryMessenger argBinaryMessenger){ + this.binaryMessenger = argBinaryMessenger; + } + public interface Reply { + void reply(T reply); + } + static MessageCodec getCodec() { + return FlutterRouterApiCodec.INSTANCE; + } + + public void pushRoute(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.pushRoute", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void popRoute(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.popRoute", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void removeRoute(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.removeRoute", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void onForeground(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.onForeground", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void onBackground(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.onBackground", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void onNativeResult(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.onNativeResult", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void onContainerShow(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.onContainerShow", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void onContainerHide(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.onContainerHide", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void sendEventToFlutter(CommonParams paramArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter", getCodec()); + channel.send(new ArrayList(Arrays.asList(paramArg)), channelReply -> { + callback.reply(null); + }); + } + public void onBackPressed(Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FlutterRouterApi.onBackPressed", getCodec()); + channel.send(null, channelReply -> { + callback.reply(null); + }); + } + } + private static Map wrapError(Throwable exception) { + Map errorMap = new HashMap<>(); + errorMap.put("message", exception.toString()); + errorMap.put("code", exception.getClass().getSimpleName()); + errorMap.put("details", "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + return errorMap; + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/ContainerThemeMgr.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/ContainerThemeMgr.java new file mode 100644 index 000000000..a510d70f8 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/ContainerThemeMgr.java @@ -0,0 +1,52 @@ +package com.idlefish.flutterboost.containers; + +import com.idlefish.flutterboost.FlutterBoostUtils; + +import java.util.HashMap; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; + +/** + * @author : Joe Chan + * @date : 2024/3/8 11:30 + */ +public class ContainerThemeMgr { + private static final HashMap themes = + new HashMap<>(); + private static PlatformChannel.SystemChromeStyle finalStyle; + + @UiThread + public static void onActivityPause(@NonNull FlutterBoostActivity activity, PlatformChannel.SystemChromeStyle restoreTheme) { + finalStyle = null; + if (activity.platformPlugin == null) { + return; + } + int hash = activity.hashCode(); + PlatformChannel.SystemChromeStyle style = + FlutterBoostUtils.getCurrentSystemUiOverlayTheme(activity.platformPlugin, true); + PlatformChannel.SystemChromeStyle mergedStyle = FlutterBoostUtils.mergeSystemChromeStyle(restoreTheme, style); + if (mergedStyle != null) { + themes.put(hash, mergedStyle); + } + } + + @UiThread + public static void onActivityDestroy(@NonNull FlutterBoostActivity activity) { + PlatformChannel.SystemChromeStyle style = themes.remove(activity.hashCode()); + if (themes.isEmpty()) { + finalStyle = style; + } + } + + @Nullable + public static PlatformChannel.SystemChromeStyle findTheme(@NonNull FlutterBoostActivity activity) { + return themes.get(activity.hashCode()); + } + + public static PlatformChannel.SystemChromeStyle getFinalStyle() { + return FlutterBoostUtils.copySystemChromeStyle(finalStyle); + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterActivityLaunchConfigs.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterActivityLaunchConfigs.java new file mode 100644 index 000000000..0050e4d8a --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterActivityLaunchConfigs.java @@ -0,0 +1,23 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +public class FlutterActivityLaunchConfigs { + + // Intent extra arguments. + public static final String EXTRA_BACKGROUND_MODE = "background_mode"; + public static final String EXTRA_CACHED_ENGINE_ID = "cached_engine_id"; + public static final String EXTRA_DESTROY_ENGINE_WITH_ACTIVITY = + "destroy_engine_with_activity"; + public static final String EXTRA_ENABLE_STATE_RESTORATION = "enable_state_restoration"; + public static final String EXTRA_URL = "url"; + public static final String EXTRA_URL_PARAM = "url_param"; + public static final String EXTRA_UNIQUE_ID = "unique_id"; + + // for onActivityResult + public static final String ACTIVITY_RESULT_KEY = "ActivityResult"; + + private FlutterActivityLaunchConfigs() {} +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostActivity.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostActivity.java new file mode 100644 index 000000000..80982cbdd --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostActivity.java @@ -0,0 +1,436 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; + +import com.idlefish.flutterboost.Assert; +import com.idlefish.flutterboost.FlutterBoost; +import com.idlefish.flutterboost.FlutterBoostUtils; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode; +import io.flutter.embedding.android.FlutterTextureView; +import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.android.RenderMode; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.renderer.FlutterRenderer; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.platform.PlatformPlugin; + +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.ACTIVITY_RESULT_KEY; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_BACKGROUND_MODE; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_ID; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_DESTROY_ENGINE_WITH_ACTIVITY; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_ENABLE_STATE_RESTORATION; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_UNIQUE_ID; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_URL; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_URL_PARAM; + +public class FlutterBoostActivity extends FlutterActivity implements FlutterViewContainer { + private static final String TAG = "FlutterBoost_java"; + private final String who = UUID.randomUUID().toString(); + private final FlutterTextureHooker textureHooker =new FlutterTextureHooker(); + private FlutterView flutterView; + protected PlatformPlugin platformPlugin; + private LifecycleStage stage; + private boolean isAttached = false; + + PlatformChannel.SystemChromeStyle restoreTheme = null; + + private boolean isDebugLoggingEnabled() { + return FlutterBoostUtils.isDebugLoggingEnabled(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onCreate: " + this); + final FlutterContainerManager containerManager = FlutterContainerManager.instance(); + FlutterViewContainer top = containerManager.getTopContainer(); + if (top != this && top instanceof FlutterBoostActivity) { + // find the theme of the previous container + restoreTheme = ContainerThemeMgr.findTheme((FlutterBoostActivity) top); + } else if (top == null) { + // this is the first active container, try to get the theme of the last-destroyed container + restoreTheme = ContainerThemeMgr.getFinalStyle(); + } + super.onCreate(savedInstanceState); + stage = LifecycleStage.ON_CREATE; + flutterView = FlutterBoostUtils.findFlutterView(getWindow().getDecorView()); + flutterView.detachFromFlutterEngine(); // Avoid failure when attaching to engine in |onResume|. + FlutterBoost.instance().getPlugin().onContainerCreated(this); + } + + @Override + public void detachFromFlutterEngine() { + /** + * TODO:// Override and do nothing to avoid destroying + * FlutterView unexpectedly. + */ + if (isDebugLoggingEnabled()) Log.d(TAG, "#detachFromFlutterEngine: " + this); + } + + @Override + public boolean shouldDispatchAppLifecycleState() { + return false; + } + + /** + * Whether to automatically attach the {@link FlutterView} to the engine. + * + *

In the add-to-app scenario where multiple {@link FlutterView} share the same {@link + * FlutterEngine}, the host application desires to determine the timing of attaching the {@link + * FlutterView} to the engine, for example, during the {@code onResume} instead of the {@code + * onCreateView}. + * + *

Defaults to {@code true}. + */ + @Override + public boolean attachToEngineAutomatically() { + return false; + } + + @Override + // This method is called right before the activity's onPause() callback. + public void onUserLeaveHint() { + super.onUserLeaveHint(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onUserLeaveHint: " + this); + } + + @Override + protected void onStart() { + super.onStart(); + stage = LifecycleStage.ON_START; + if (isDebugLoggingEnabled()) Log.d(TAG, "#onStart: " + this); + } + + @Override + protected void onStop() { + super.onStop(); + stage = LifecycleStage.ON_STOP; + if (isDebugLoggingEnabled()) Log.d(TAG, "#onStop: " + this); + } + + @Override + public void onResume() { + super.onResume(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onResume: " + this + ", isOpaque=" + isOpaque()); + final FlutterContainerManager containerManager = FlutterContainerManager.instance(); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + FlutterViewContainer top = containerManager.getTopActivityContainer(); + boolean isActiveContainer = containerManager.isActiveContainer(this); + if (isActiveContainer && top != null && top != this && !top.isOpaque() && top.isPausing()) { + Log.w(TAG, "Skip the unexpected activity lifecycle event on Android Q. " + + "See https://issuetracker.google.com/issues/185693011 for more details."); + return; + } + } + + stage = LifecycleStage.ON_RESUME; + + + // try to detach *prevous* container from the engine. + FlutterViewContainer top = containerManager.getTopContainer(); + if (top != null && top != this) top.detachFromEngineIfNeeded(); + + FlutterBoost.instance().getPlugin().onContainerAppeared(this, () -> { + // attach new container to the engine. + attachToEngineIfNeeded(); + textureHooker.onFlutterTextureViewRestoreState(); + // Since we takeover PlatformPlugin from FlutterActivityAndFragmentDelegate, + // the system UI overlays can't be updated in |onPostResume| callback. So we + // update system UI overlays to match Flutter's desired system chrome style here. + onUpdateSystemUiOverlays(); + }); + } + + // Update system UI overlays to match Flutter's desired system chrome style + protected void onUpdateSystemUiOverlays() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onUpdateSystemUiOverlays: " + this); + Assert.assertNotNull(platformPlugin); + platformPlugin.updateSystemUiOverlays(); + } + + @Override + protected void onPause() { + super.onPause(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onPause: " + this + ", isOpaque=" + isOpaque()); + // update the restoreTheme of this container + ContainerThemeMgr.onActivityPause(this, restoreTheme); + restoreTheme = ContainerThemeMgr.findTheme(this); + FlutterViewContainer top = FlutterContainerManager.instance().getTopActivityContainer(); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + if (top != null && top != this && !top.isOpaque() && top.isPausing()) { + Log.w(TAG, "Skip the unexpected activity lifecycle event on Android Q. " + + "See https://issuetracker.google.com/issues/185693011 for more details."); + return; + } + } + + stage = LifecycleStage.ON_PAUSE; + + FlutterBoost.instance().getPlugin().onContainerDisappeared(this); + + // We defer |performDetach| call to new Flutter container's |onResume|. + setIsFlutterUiDisplayed(false); + } + + @Override + public void onFlutterTextureViewCreated(FlutterTextureView flutterTextureView) { + super.onFlutterTextureViewCreated(flutterTextureView); + textureHooker.hookFlutterTextureView(flutterTextureView); + } + + private void performAttach() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#performAttach: " + this); + + // Attach plugins to the activity. + getFlutterEngine().getActivityControlSurface().attachToActivity(getExclusiveAppComponent(), getLifecycle()); + + if (platformPlugin == null) { + platformPlugin = new PlatformPlugin(getActivity(), getFlutterEngine().getPlatformChannel(), this); + // Set the restoreTheme to current container + if (restoreTheme != null) { + FlutterBoostUtils.setSystemChromeSystemUIOverlayStyle(platformPlugin, restoreTheme); + } + } + + // Attach rendering pipeline. + flutterView.attachToFlutterEngine(getFlutterEngine()); + } + + private void performDetach() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#performDetach: " + this); + + // Plugins are no longer attached to the activity. + getFlutterEngine().getActivityControlSurface().detachFromActivity(); + + // Release Flutter's control of UI such as system chrome. + releasePlatformChannel(); + + // Detach rendering pipeline. + flutterView.detachFromFlutterEngine(); + } + + private void releasePlatformChannel() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#releasePlatformChannel: " + this); + if (platformPlugin != null) { + platformPlugin.destroy(); + platformPlugin = null; + } + } + + // Fix black screen when activity transition + private void setIsFlutterUiDisplayed(boolean isDisplayed) { + try { + FlutterRenderer flutterRenderer = getFlutterEngine().getRenderer(); + Field isDisplayingFlutterUiField = FlutterRenderer.class.getDeclaredField("isDisplayingFlutterUi"); + isDisplayingFlutterUiField.setAccessible(true); + isDisplayingFlutterUiField.setBoolean(flutterRenderer, false); + assert(!flutterRenderer.isDisplayingFlutterUi()); + } catch (Exception e) { + Log.e(TAG, "You *should* keep fields in io.flutter.embedding.engine.renderer.FlutterRenderer."); + e.printStackTrace(); + } + } + + public void attachToEngineIfNeeded() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#attachToEngineIfNeeded: " + this); + if (!isAttached) { + performAttach(); + isAttached = true; + } + } + + @Override + public void detachFromEngineIfNeeded() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#detachFromEngineIfNeeded: " + this); + if (isAttached) { + performDetach(); + isAttached = false; + } + } + + @Override + protected void onDestroy() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDestroy: " + this); + ContainerThemeMgr.onActivityDestroy(this); + stage = LifecycleStage.ON_DESTROY; + detachFromEngineIfNeeded(); + textureHooker.onFlutterTextureViewRelease(); + FlutterBoost.instance().getPlugin().onContainerDestroyed(this); + + // Call super's onDestroy + super.onDestroy(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onConfigurationChanged: " + (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? "LANDSCAPE" : "PORTRAIT") + ", " + this); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onSaveInstanceState: " + this); + } + + @Override + public boolean shouldRestoreAndSaveState() { + if (getIntent().hasExtra(EXTRA_ENABLE_STATE_RESTORATION)) { + return getIntent().getBooleanExtra(EXTRA_ENABLE_STATE_RESTORATION, false); + } + // Defaults to |true|. + return true; + } + + @Override + public PlatformPlugin providePlatformPlugin(Activity activity, FlutterEngine flutterEngine) { + // We takeover |PlatformPlugin| here. + return null; + } + + @Override + public boolean shouldDestroyEngineWithHost() { + // The |FlutterEngine| should outlive this FlutterActivity. + return false; + } + + @Override + public boolean shouldAttachEngineToActivity() { + // We manually manage the relationship between the Activity and FlutterEngine here. + return false; + } + + @Override + public void onBackPressed() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onBackPressed: " + this); + // Intercept the user's press of the back key. + FlutterBoost.instance().getPlugin().onBackPressed(); + } + + @Override + public RenderMode getRenderMode() { + // Default to |FlutterTextureView|. + return RenderMode.texture; + } + + @Override + public Activity getContextActivity() { + return this; + } + + @Override + public void finishContainer(Map result) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#finishContainer: " + this); + if (result != null) { + Intent intent = new Intent(); + intent.putExtra(ACTIVITY_RESULT_KEY, new HashMap(result)); + setResult(Activity.RESULT_OK, intent); + } + finish(); + } + + @Override + public String getUrl() { + if (!getIntent().hasExtra(EXTRA_URL)) { + Log.e(TAG, "Oops! The activity url are *MISSED*! You should override" + + " the |getUrl|, or set url via |CachedEngineIntentBuilder.url|."); + return null; + } + return getIntent().getStringExtra(EXTRA_URL); + } + + @Override + public Map getUrlParams() { + return (HashMap)getIntent().getSerializableExtra(EXTRA_URL_PARAM); + } + + @Override + public String getUniqueId() { + if (!getIntent().hasExtra(EXTRA_UNIQUE_ID)) { + return this.who; + } + return getIntent().getStringExtra(EXTRA_UNIQUE_ID); + } + + @Override + public String getCachedEngineId() { + return FlutterBoost.ENGINE_ID; + } + + @Override + public boolean isOpaque() { + return getBackgroundMode() == BackgroundMode.opaque; + } + + @Override + public boolean isPausing() { + return (stage == LifecycleStage.ON_PAUSE || stage == LifecycleStage.ON_STOP) && !isFinishing(); + } + + public static class CachedEngineIntentBuilder { + private final Class activityClass; + private boolean destroyEngineWithActivity = false; + private String backgroundMode = BackgroundMode.opaque.name(); + private String url; + private HashMap params; + private String uniqueId; + + public CachedEngineIntentBuilder(Class activityClass) { + this.activityClass = activityClass; + } + + + public FlutterBoostActivity.CachedEngineIntentBuilder destroyEngineWithActivity(boolean destroyEngineWithActivity) { + this.destroyEngineWithActivity = destroyEngineWithActivity; + return this; + } + + + public FlutterBoostActivity.CachedEngineIntentBuilder backgroundMode(BackgroundMode backgroundMode) { + this.backgroundMode = backgroundMode.name(); + return this; + } + + public FlutterBoostActivity.CachedEngineIntentBuilder url(String url) { + this.url = url; + return this; + } + + public FlutterBoostActivity.CachedEngineIntentBuilder urlParams(Map params) { + this.params = (params instanceof HashMap) ? (HashMap)params : new HashMap(params); + return this; + } + + public FlutterBoostActivity.CachedEngineIntentBuilder uniqueId(String uniqueId) { + this.uniqueId = uniqueId; + return this; + } + + public Intent build(Context context) { + return new Intent(context, activityClass) + .putExtra(EXTRA_CACHED_ENGINE_ID, FlutterBoost.ENGINE_ID) // default engine + .putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, destroyEngineWithActivity) + .putExtra(EXTRA_BACKGROUND_MODE, backgroundMode) + .putExtra(EXTRA_URL, url) + .putExtra(EXTRA_URL_PARAM, params) + .putExtra(EXTRA_UNIQUE_ID, uniqueId != null ? uniqueId : FlutterBoostUtils.createUniqueId(url)); + } + } + +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostFragment.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostFragment.java new file mode 100644 index 000000000..3e24df9eb --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterBoostFragment.java @@ -0,0 +1,530 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.ACTIVITY_RESULT_KEY; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_UNIQUE_ID; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_URL; +import static com.idlefish.flutterboost.containers.FlutterActivityLaunchConfigs.EXTRA_URL_PARAM; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.idlefish.flutterboost.Assert; +import com.idlefish.flutterboost.FlutterBoost; +import com.idlefish.flutterboost.FlutterBoostUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import io.flutter.embedding.android.FlutterFragment; +import io.flutter.embedding.android.FlutterTextureView; +import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.android.RenderMode; +import io.flutter.embedding.android.TransparencyMode; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugin.platform.PlatformPlugin; + +public class FlutterBoostFragment extends FlutterFragment implements FlutterViewContainer { + private static final String TAG = "FlutterBoost_java"; + private final String who = UUID.randomUUID().toString(); + private final FlutterTextureHooker textureHooker=new FlutterTextureHooker(); + private FlutterView flutterView; + private PlatformPlugin platformPlugin; + private LifecycleStage stage; + private boolean isAttached = false; + private boolean isFinishing = false; + + private boolean isDebugLoggingEnabled() { + return FlutterBoostUtils.isDebugLoggingEnabled(); + } + + @Override + public void detachFromFlutterEngine() { + /** + * TODO:// Override and do nothing to avoid destroying + * FlutterView unexpectedly. + */ + if (isDebugLoggingEnabled()) Log.d(TAG, "#detachFromFlutterEngine: " + this); + } + + @Override + public boolean shouldDispatchAppLifecycleState() { + return false; + } + + /** + * Whether to automatically attach the {@link FlutterView} to the engine. + * + *

In the add-to-app scenario where multiple {@link FlutterView} share the same {@link + * FlutterEngine}, the host application desires to determine the timing of attaching the {@link + * FlutterView} to the engine, for example, during the {@code onResume} instead of the {@code + * onCreateView}. + * + *

Defaults to {@code true}. + */ + @Override + public boolean attachToEngineAutomatically() { + return false; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onCreate: " + this); + stage = LifecycleStage.ON_CREATE; + } + + @Override + public void onStart() { + super.onStart(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onStart: " + this); + } + + @Override + public void onDestroy() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDestroy: " + this); + stage = LifecycleStage.ON_DESTROY; + textureHooker.onFlutterTextureViewRelease(); + detachFromEngineIfNeeded(); + + // Call super's onDestroy + super.onDestroy(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onAttach: " + this); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onCreateView: " + this); + FlutterBoost.instance().getPlugin().onContainerCreated(this); + View view = super.onCreateView(inflater, container, savedInstanceState); + flutterView = FlutterBoostUtils.findFlutterView(view); + // Detach FlutterView from engine before |onResume|. + flutterView.detachFromFlutterEngine(); + if (view == flutterView) { + // fix https://github.com/alibaba/flutter_boost/issues/1732 + FrameLayout frameLayout = new FrameLayout(view.getContext()); + frameLayout.addView(view); + return frameLayout; + } + return view; + } + + @Override + public void onHiddenChanged(boolean hidden) { + super.onHiddenChanged(hidden); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onHiddenChanged: hidden=" + hidden + ", " + this); + // If |onHiddenChanged| method is called before the |onCreateView|, + // we just return here. + if (flutterView == null) return; + if (hidden) { + didFragmentHide(); + } else { + didFragmentShow(() -> {}); + } + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + if (isDebugLoggingEnabled()) Log.d(TAG, "#setUserVisibleHint: isVisibleToUser=" + isVisibleToUser + ", " + this); + // If |setUserVisibleHint| method is called before the |onCreateView|, + // we just return here. + if (flutterView == null) return; + if (isVisibleToUser) { + didFragmentShow(() -> {}); + } else { + didFragmentHide(); + } + } + + @Override + public void onResume() { + super.onResume(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onResume: isHidden=" + isHidden() + ", " + this); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + final FlutterContainerManager containerManager = FlutterContainerManager.instance(); + FlutterViewContainer top = containerManager.getTopActivityContainer(); + boolean isActiveContainer = containerManager.isActiveContainer(this); + if (isActiveContainer && top != null && top != this.getContextActivity() && !top.isOpaque() && top.isPausing()) { + Log.w(TAG, "Skip the unexpected activity lifecycle event on Android Q. " + + "See https://issuetracker.google.com/issues/185693011 for more details."); + return; + } + } + + stage = LifecycleStage.ON_RESUME; + if (!isHidden()) { + didFragmentShow(() -> { + // Update system UI overlays to match Flutter's desired system chrome style + onUpdateSystemUiOverlays(); + }); + } + } + + // Update system UI overlays to match Flutter's desired system chrome style + protected void onUpdateSystemUiOverlays() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onUpdateSystemUiOverlays: " + this); + Assert.assertNotNull(platformPlugin); + platformPlugin.updateSystemUiOverlays(); + } + + @Override + public RenderMode getRenderMode() { + // Default to |FlutterTextureView|. + return RenderMode.texture; + } + + @Override + public void onPause() { + super.onPause(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onPause: " + this + ", isFinshing=" + isFinishing); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + FlutterViewContainer top = FlutterContainerManager.instance().getTopActivityContainer(); + if (top != null && top != this.getContextActivity() && !top.isOpaque() && top.isPausing()) { + Log.w(TAG, "Skip the unexpected activity lifecycle event on Android Q. " + + "See https://issuetracker.google.com/issues/185693011 for more details."); + return; + } + } + + stage = LifecycleStage.ON_PAUSE; + didFragmentHide(); + } + + @Override + public void onStop() { + super.onStop(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onStop: " + this); + stage = LifecycleStage.ON_STOP; + } + + @Override + public void onDestroyView() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDestroyView: " + this); + FlutterBoost.instance().getPlugin().onContainerDestroyed(this); + + super.onDestroyView(); + } + + @Override + public void onDetach() { + super.onDetach(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onDetach: " + this); + } + + @Override + // This method is called right before the activity's onPause() callback. + public void onUserLeaveHint() { + super.onUserLeaveHint(); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onUserLeaveHint: " + this); + } + + @Override + public void onBackPressed() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#onBackPressed: " + this); + // Intercept the user's press of the back key. + FlutterBoost.instance().getPlugin().onBackPressed(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onConfigurationChanged: " + (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? "LANDSCAPE" : "PORTRAIT") + ", " + this); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (isDebugLoggingEnabled()) Log.d(TAG, "#onSaveInstanceState: " + this); + } + + @Override + public boolean shouldRestoreAndSaveState() { + if (getArguments().containsKey(ARG_ENABLE_STATE_RESTORATION)) { + return getArguments().getBoolean(ARG_ENABLE_STATE_RESTORATION); + } + // Defaults to |true|. + return true; + } + + @Override + public PlatformPlugin providePlatformPlugin(Activity activity, FlutterEngine flutterEngine) { + // We takeover |PlatformPlugin| here. + return null; + } + + @Override + public boolean shouldDestroyEngineWithHost() { + // The |FlutterEngine| should outlive this FlutterFragment. + return false; + } + + @Override + public void onFlutterTextureViewCreated(FlutterTextureView flutterTextureView) { + super.onFlutterTextureViewCreated(flutterTextureView); + textureHooker.hookFlutterTextureView(flutterTextureView); + } + + @Override + public Activity getContextActivity() { + return getActivity(); + } + + @Override + public void finishContainer(Map result) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#finishContainer: " + this); + isFinishing = true; + if (result != null) { + Intent intent = new Intent(); + intent.putExtra(ACTIVITY_RESULT_KEY, new HashMap(result)); + getActivity().setResult(Activity.RESULT_OK, intent); + } + onFinishContainer(); + } + + // finish activity container + protected void onFinishContainer() { + getActivity().finish(); + } + + @Override + public String getUrl() { + if (!getArguments().containsKey(EXTRA_URL)) { + throw new RuntimeException("Oops! The fragment url are *MISSED*! You should " + + "override the |getUrl|, or set url via CachedEngineFragmentBuilder."); + } + return getArguments().getString(EXTRA_URL); + } + + @Override + public Map getUrlParams() { + return (HashMap)getArguments().getSerializable(EXTRA_URL_PARAM); + } + + @Override + public String getUniqueId() { + return getArguments().getString(EXTRA_UNIQUE_ID, this.who); + } + + @Override + public String getCachedEngineId() { + return FlutterBoost.ENGINE_ID; + } + + @Override + public boolean isPausing() { + return (stage == LifecycleStage.ON_PAUSE || stage == LifecycleStage.ON_STOP) && !isFinishing; + } + + protected void didFragmentShow(Runnable onComplete) { + if (isDebugLoggingEnabled()) Log.d(TAG, "#didFragmentShow: " + this + ", isOpaque=" + isOpaque()); + + // try to detach *prevous* container from the engine. + FlutterViewContainer top = FlutterContainerManager.instance().getTopContainer(); + if (top != null && top != this) top.detachFromEngineIfNeeded(); + + FlutterBoost.instance().getPlugin().onContainerAppeared(this, () -> { + // attach new container to the engine. + attachToEngineIfNeeded(); + textureHooker.onFlutterTextureViewRestoreState(); + onComplete.run(); + }); + } + + protected void didFragmentHide() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#didFragmentHide: " + this + ", isOpaque=" + isOpaque()); + FlutterBoost.instance().getPlugin().onContainerDisappeared(this); + // We defer |performDetach| call to new Flutter container's |onResume|; + // performDetach(); + } + + private void performAttach() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#performAttach: " + this); + + // Attach plugins to the activity. + getFlutterEngine().getActivityControlSurface().attachToActivity(getExclusiveAppComponent(), getLifecycle()); + + if (platformPlugin == null) { + platformPlugin = new PlatformPlugin(getActivity(), getFlutterEngine().getPlatformChannel(), this); + } + + // Attach rendering pipeline. + flutterView.attachToFlutterEngine(getFlutterEngine()); + } + + private void performDetach() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#performDetach: " + this); + + // Plugins are no longer attached to the activity. + getFlutterEngine().getActivityControlSurface().detachFromActivity(); + + // Release Flutter's control of UI such as system chrome. + releasePlatformChannel(); + + // Detach rendering pipeline. + flutterView.detachFromFlutterEngine(); + } + + private void releasePlatformChannel() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#releasePlatformChannel: " + this); + if (platformPlugin != null) { + platformPlugin.destroy(); + platformPlugin = null; + } + } + + public void attachToEngineIfNeeded() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#attachToEngineIfNeeded: " + this); + if (!isAttached) { + performAttach(); + isAttached = true; + } + } + + @Override + public void detachFromEngineIfNeeded() { + if (isDebugLoggingEnabled()) Log.d(TAG, "#detachFromEngineIfNeeded: " + this); + if (isAttached) { + performDetach(); + isAttached = false; + } + } + + // Defaults to {@link TransparencyMode#opaque}. + @Override + public TransparencyMode getTransparencyMode() { + String transparencyModeName = + getArguments() + .getString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, TransparencyMode.opaque.name()); + return TransparencyMode.valueOf(transparencyModeName); + } + + @Override + public boolean isOpaque() { + return getTransparencyMode() == TransparencyMode.opaque; + } + + public static class CachedEngineFragmentBuilder { + private final Class fragmentClass; + private boolean destroyEngineWithFragment = false; + private RenderMode renderMode = RenderMode.surface; + private TransparencyMode transparencyMode = TransparencyMode.opaque; + private boolean shouldAttachEngineToActivity = true; + private String url = "/"; + private HashMap params; + private String uniqueId; + + public CachedEngineFragmentBuilder() { + this(FlutterBoostFragment.class); + } + + public CachedEngineFragmentBuilder(Class subclass) { + fragmentClass = subclass; + } + + public CachedEngineFragmentBuilder url(String url) { + this.url = url; + return this; + } + + public CachedEngineFragmentBuilder urlParams(Map params) { + this.params = (params instanceof HashMap) ? (HashMap)params : new HashMap(params); + return this; + } + + public CachedEngineFragmentBuilder uniqueId(String uniqueId) { + this.uniqueId = uniqueId; + return this; + } + + public CachedEngineFragmentBuilder destroyEngineWithFragment( + boolean destroyEngineWithFragment) { + this.destroyEngineWithFragment = destroyEngineWithFragment; + return this; + } + + public CachedEngineFragmentBuilder renderMode( RenderMode renderMode) { + this.renderMode = renderMode; + return this; + } + + public CachedEngineFragmentBuilder transparencyMode( + TransparencyMode transparencyMode) { + this.transparencyMode = transparencyMode; + return this; + } + + public CachedEngineFragmentBuilder shouldAttachEngineToActivity( + boolean shouldAttachEngineToActivity) { + this.shouldAttachEngineToActivity = shouldAttachEngineToActivity; + return this; + } + + /** + * Creates a {@link Bundle} of arguments that are assigned to the new {@code FlutterFragment}. + * + *

Subclasses should override this method to add new properties to the {@link Bundle}. + * Subclasses must call through to the super method to collect all existing property values. + */ + protected Bundle createArgs() { + Bundle args = new Bundle(); + args.putString(ARG_CACHED_ENGINE_ID, FlutterBoost.ENGINE_ID); + args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, destroyEngineWithFragment); + args.putString( + ARG_FLUTTERVIEW_RENDER_MODE, + renderMode != null ? renderMode.name() : RenderMode.surface.name()); + args.putString( + ARG_FLUTTERVIEW_TRANSPARENCY_MODE, + transparencyMode != null ? transparencyMode.name() : TransparencyMode.transparent.name()); + args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity); + args.putString(EXTRA_URL, url); + args.putSerializable(EXTRA_URL_PARAM, params); + args.putString(EXTRA_UNIQUE_ID, uniqueId != null ? uniqueId : FlutterBoostUtils.createUniqueId(url)); + return args; + } + + /** + * Constructs a new {@code FlutterFragment} (or a subclass) that is configured based on + * properties set on this {@code CachedEngineFragmentBuilder}. + */ + public T build() { + try { + @SuppressWarnings("unchecked") + T frag = (T) fragmentClass.getDeclaredConstructor().newInstance(); + if (frag == null) { + throw new RuntimeException( + "The FlutterFragment subclass sent in the constructor (" + + fragmentClass.getCanonicalName() + + ") does not match the expected return type."); + } + + Bundle args = createArgs(); + frag.setArguments(args); + + return frag; + } catch (Exception e) { + throw new RuntimeException( + "Could not instantiate FlutterFragment subclass (" + fragmentClass.getName() + ")", e); + } + } + } + +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterContainerManager.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterContainerManager.java new file mode 100644 index 000000000..dc5b27480 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterContainerManager.java @@ -0,0 +1,117 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +import android.app.Activity; +import android.os.Build; +import android.util.Log; + +import com.idlefish.flutterboost.FlutterBoostUtils; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + +public class FlutterContainerManager { + private static final String TAG = "FlutterBoost_java"; + + private FlutterContainerManager() {} + private boolean isDebugLoggingEnabled() { + return FlutterBoostUtils.isDebugLoggingEnabled(); + } + + private static class LazyHolder { + static final FlutterContainerManager INSTANCE = new FlutterContainerManager(); + } + + public static FlutterContainerManager instance() { + return FlutterContainerManager.LazyHolder.INSTANCE; + } + + private final Map allContainers = new HashMap<>(); + private final LinkedList activeContainers = new LinkedList<>(); + + // onContainerCreated + public void addContainer(String uniqueId, FlutterViewContainer container) { + allContainers.put(uniqueId, container); + if (isDebugLoggingEnabled()) Log.d(TAG, "#addContainer: " + uniqueId + ", " + this); + } + + // onContainerAppeared + public void activateContainer(String uniqueId, FlutterViewContainer container) { + if (uniqueId == null || container == null) return; + assert(allContainers.containsKey(uniqueId)); + + if (activeContainers.contains(container)) { + activeContainers.remove(container); + } + activeContainers.add(container); + if (isDebugLoggingEnabled()) Log.d(TAG, "#activateContainer: " + uniqueId + "," + this); + } + + // onContainerDestroyed + public void removeContainer(String uniqueId) { + if (uniqueId == null) return; + FlutterViewContainer container = allContainers.remove(uniqueId); + activeContainers.remove(container); + if (isDebugLoggingEnabled()) Log.d(TAG, "#removeContainer: " + uniqueId + ", " + this); + } + + + public FlutterViewContainer findContainerById(String uniqueId) { + if (allContainers.containsKey(uniqueId)) { + return allContainers.get(uniqueId); + } + return null; + } + + public boolean isActiveContainer(FlutterViewContainer container) { + return activeContainers.contains(container); + } + + public FlutterViewContainer getTopContainer() { + if (activeContainers.size() > 0) { + return activeContainers.getLast(); + } + return null; + } + + public FlutterViewContainer getTopActivityContainer() { + final int size = activeContainers.size(); + if (size == 0) { + return null; + } + for (int i = size - 1; i >= 0; i--) { + final FlutterViewContainer container = activeContainers.get(i); + if (container instanceof Activity) { + return container; + } + } + return null; + } + + public boolean isTopContainer(String uniqueId) { + FlutterViewContainer top = getTopContainer(); + if (top != null && top.getUniqueId() == uniqueId) { + return true; + } + return false; + } + + public int getContainerSize() { + return allContainers.size(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("activeContainers=" + activeContainers.size() + ", ["); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + activeContainers.forEach((value) -> sb.append(value.getUrl() + ',')); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterTextureHooker.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterTextureHooker.java new file mode 100644 index 000000000..6d3e46da6 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterTextureHooker.java @@ -0,0 +1,143 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +import android.graphics.SurfaceTexture; +import android.os.Build; +import android.view.Surface; +import android.view.TextureView; + +import com.idlefish.flutterboost.FlutterBoost; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import io.flutter.embedding.android.FlutterTextureView; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.renderer.FlutterRenderer; + + +class FlutterTextureHooker { + private SurfaceTexture restoreSurface; + private FlutterTextureView flutterTextureView; + private boolean isNeedRestoreState = false; + + /** + * Release surface when Activity.onDestroy / Fragment.onDestroy. + * Stop render when finish the last flutter boost container. + */ + public void onFlutterTextureViewRelease() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + int containerSize = FlutterContainerManager.instance().getContainerSize(); + if (containerSize == 1) { + FlutterEngine engine = FlutterBoost.instance().getEngine(); + FlutterRenderer renderer = engine.getRenderer(); + renderer.stopRenderingToSurface(); + } + if (restoreSurface != null) { + restoreSurface.release(); + restoreSurface = null; + } + } + } + + /** + * Restore last surface for os version below Android.M. + * Call from Activity.onResume / Fragment.didFragmentShow. + */ + public void onFlutterTextureViewRestoreState() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + if (restoreSurface != null && flutterTextureView != null && isNeedRestoreState) { + try { + Class aClass = flutterTextureView.getClass(); + + Field isSurfaceAvailableForRendering = aClass.getDeclaredField( + "isSurfaceAvailableForRendering"); + isSurfaceAvailableForRendering.setAccessible(true); + isSurfaceAvailableForRendering.set(flutterTextureView, true); + boolean next = false; + try { + Field isAttachedToFlutterRenderer = aClass.getDeclaredField( + "isAttachedToFlutterRenderer"); + isAttachedToFlutterRenderer.setAccessible(true); + next = isAttachedToFlutterRenderer.getBoolean(flutterTextureView); + } catch (NoSuchFieldException ignore) { + Method shouldNotify = aClass.getDeclaredMethod("shouldNotify"); + shouldNotify.setAccessible(true); + next = (Boolean) shouldNotify.invoke(flutterTextureView); + } + if (next) { + FlutterEngine engine = FlutterBoost.instance().getEngine(); + if (engine != null) { + + FlutterRenderer flutterRenderer = engine.getRenderer(); + Surface surface = new Surface(restoreSurface); + flutterRenderer.startRenderingToSurface(surface, false); + + try { + flutterTextureView.setSurfaceTexture(restoreSurface); + } catch (Exception e) { + e.printStackTrace(); + } + } + restoreSurface = null; + isNeedRestoreState = false; + } + } catch (Exception e) { + // https://github.com/alibaba/flutter_boost/issues/1560 + throw new RuntimeException("You *SHOULD* keep FlutterTextureView: -keep class io.flutter.embedding.android.FlutterTextureView { *; }.", e); + } + } + } + } + + /** + * Hook FlutterTextureView for os version below Android.M. + */ + public void hookFlutterTextureView(FlutterTextureView flutterTextureView) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + if (flutterTextureView != null) { + TextureView.SurfaceTextureListener surfaceTextureListener = flutterTextureView.getSurfaceTextureListener(); + this.flutterTextureView = flutterTextureView; + this.flutterTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + surfaceTextureListener.onSurfaceTextureAvailable(surface, width, height); + + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + surfaceTextureListener.onSurfaceTextureSizeChanged(surface, width, height); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + try { + Class aClass = + flutterTextureView.getClass(); + Field isSurfaceAvailableForRendering = aClass.getDeclaredField( + "isSurfaceAvailableForRendering"); + isSurfaceAvailableForRendering.setAccessible(true); + isSurfaceAvailableForRendering.set(flutterTextureView, false); + } catch (Exception e) { + // https://github.com/alibaba/flutter_boost/issues/1560 + throw new RuntimeException("You *SHOULD* keep FlutterTextureView: -keep class io.flutter.embedding.android.FlutterTextureView { *; }.", e); + } + isNeedRestoreState = true; + //return false, handle the last frame of surfaceTexture ourselves; + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + surfaceTextureListener.onSurfaceTextureUpdated(surface); + restoreSurface = surface; + } + }); + } + } + } +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterViewContainer.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterViewContainer.java new file mode 100644 index 000000000..1c5c95a82 --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/FlutterViewContainer.java @@ -0,0 +1,23 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +import android.app.Activity; + +import java.util.Map; + +/** + * A container which contains the FlutterView + */ +public interface FlutterViewContainer { + Activity getContextActivity(); + String getUrl(); + Map getUrlParams(); + String getUniqueId(); + void finishContainer(Map result); + default boolean isPausing() { return false; } + default boolean isOpaque() { return true; } + default void detachFromEngineIfNeeded() {} +} diff --git a/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/LifecycleStage.java b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/LifecycleStage.java new file mode 100644 index 000000000..a430f23ea --- /dev/null +++ b/flutter_boost_android/android/src/main/java/com/idlefish/flutterboost/containers/LifecycleStage.java @@ -0,0 +1,14 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.idlefish.flutterboost.containers; + +enum LifecycleStage { + ON_CREATE, + ON_START, + ON_RESUME, + ON_PAUSE, + ON_STOP, + ON_DESTROY +} \ No newline at end of file diff --git a/flutter_boost_android/lib/flutter_boost_android.dart b/flutter_boost_android/lib/flutter_boost_android.dart new file mode 100644 index 000000000..8e67c6783 --- /dev/null +++ b/flutter_boost_android/lib/flutter_boost_android.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +library flutter_boost_android; + +// This package does not export any Dart code to the app-facing package. +// It only provides the Android platform implementation. diff --git a/flutter_boost_android/pubspec.yaml b/flutter_boost_android/pubspec.yaml new file mode 100644 index 000000000..4720ed2e0 --- /dev/null +++ b/flutter_boost_android/pubspec.yaml @@ -0,0 +1,31 @@ +# Copyright (c) 2019 Alibaba Group. All rights reserved. +# Use of this source code is governed by a MIT license that can be +# found in the LICENSE file. + +name: flutter_boost_android +description: Android implementation of the flutter_boost plugin. +version: 5.0.2 +homepage: https://github.com/alibaba/flutter_boost + +environment: + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" + +dependencies: + flutter: + sdk: flutter + flutter_boost_platform_interface: + path: ../flutter_boost_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.1 + +flutter: + plugin: + implements: flutter_boost + platforms: + android: + package: com.idlefish.flutterboost + pluginClass: FlutterBoostPlugin diff --git a/flutter_boost_ios/.gitignore b/flutter_boost_ios/.gitignore new file mode 100644 index 000000000..6d11bab8b --- /dev/null +++ b/flutter_boost_ios/.gitignore @@ -0,0 +1,27 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ +pubspec.lock diff --git a/flutter_boost_ios/CHANGELOG.md b/flutter_boost_ios/CHANGELOG.md new file mode 100644 index 000000000..06b328475 --- /dev/null +++ b/flutter_boost_ios/CHANGELOG.md @@ -0,0 +1,4 @@ +## 5.0.2 + +* Initial release of the iOS implementation for flutter_boost. +* Extracted from `flutter_boost` as part of federated plugin migration. diff --git a/flutter_boost_ios/LICENSE b/flutter_boost_ios/LICENSE new file mode 100755 index 000000000..88ed1cb37 --- /dev/null +++ b/flutter_boost_ios/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Alibaba Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/flutter_boost_ios/README.md b/flutter_boost_ios/README.md new file mode 100644 index 000000000..4d2658ac4 --- /dev/null +++ b/flutter_boost_ios/README.md @@ -0,0 +1,7 @@ +# flutter_boost_ios + +The iOS implementation of [flutter_boost](https://pub.dev/packages/flutter_boost). + +## Usage + +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), which means you can simply use `flutter_boost` normally. This package will be automatically included in your app when you do. diff --git a/flutter_boost_ios/analysis_options.yaml b/flutter_boost_ios/analysis_options.yaml new file mode 100644 index 000000000..8d81c200c --- /dev/null +++ b/flutter_boost_ios/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + avoid_print: false diff --git a/flutter_boost_ios/ios/Assets/.gitkeep b/flutter_boost_ios/ios/Assets/.gitkeep new file mode 100755 index 000000000..e69de29bb diff --git a/flutter_boost_ios/ios/Classes/FlutterBoost.h b/flutter_boost_ios/ios/Classes/FlutterBoost.h new file mode 100644 index 000000000..8c2b94921 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/FlutterBoost.h @@ -0,0 +1,105 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import "FlutterBoostDelegate.h" +#import "FlutterBoostPlugin.h" +#import "FBFlutterViewContainer.h" +#import "FlutterBoostDelegate.h" +#import "FlutterBoostPlugin.h" +#import "FBFlutterViewContainer.h" +#import "Options.h" +#import "messages.h" + + +@interface FlutterBoost : NSObject + +#pragma mark + +- (FlutterEngine*)engine; + +- (FlutterBoostPlugin*)plugin; + +- (FlutterViewController *)currentViewController; + +#pragma mark + +/// Boost全局单例 ++ (instancetype)instance; + +/// 初始化 +/// @param application 全局Application实例,如未设置engine参数,则默认从Application做engine的绑定 +/// @param delegate FlutterBoostDelegate的实例,用于实现Push和Pop的具体策略(Native侧如何Push,以及需要Push一个新的FlutterViewController时的具体动作),以及Engine的部分初始化策略 +/// @param callback 初始化完成以后的回调, +/// TODO 设计需要再review下 callback并不是异步的感觉没有必要。 +- (void)setup:(UIApplication*)application delegate:(id)delegate callback:(void (^)(FlutterEngine *engine))callback; + + +/// 利用自定义配置进行初始化 +/// @param application 全局Application实例,如未设置engine参数,则默认从Application做engine的绑定 +/// @param delegate FlutterBoostDelegate的实例,用于实现Push和Pop的具体策略 +/// @param callback 初始化完成以后的回调 +/// @param options 启动的配置,如果需要自定义请使用此参数 +- (void)setup:(UIApplication*)application delegate:(id)delegate callback:(void (^)(FlutterEngine *engine))callback options:(FlutterBoostSetupOptions*)options; + +/// 关闭页面,混合栈推荐使用的用于操作页面的接口 +/// @param uniqueId 关闭的页面唯一ID符 +- (void)close:(NSString *)uniqueId; + +/// ( 已废弃,之后有新参数可能不支持此方法 !!! ) +/// 打开新页面(默认以push方式),混合栈推荐使用的用于操作页面的接口 +/// 通过arguments可以设置为以present方式打开页面:arguments:@{@"present":@(YES)} +/// @param pageName 打开的页面资源定位符 +/// @param arguments 传入页面的参数; 若有特殊逻辑,可以通过这个参数设置回调的id +/// @param completion 页面open操作完成的回调,注意,从原生调用此方法跳转此参数才会生效 +- (void)open:(NSString *)pageName arguments:(NSDictionary *)arguments completion:(void(^)(BOOL)) completion; + + +/// (推荐使用)利用启动参数配置开启新页面 +/// @param options 配置参数 +- (void)open:(FlutterBoostRouteOptions* )options; + + +/// 将原生页面的数据回传到flutter侧的页面的的方法 +/// @param pageName 这个页面在路由表中的名字,和flutter侧BoostNavigator.push(name)中的name一样 +/// @param arguments 你想传的参数 +- (void)sendResultToFlutterWithPageName:(NSString*)pageName arguments:(NSDictionary*) arguments; + +/// 添加一个事件监听 +/// @param listener FBEventListener类型的函数 +/// @param key 事件标识符 +/// @return 用于移除监听器的一个函数,直接调用此函数可以移除此监听器避免内存泄漏 +- (FBVoidCallback)addEventListener:(FBEventListener)listener + forName:(NSString *)key; + +/// 将自定义事件传递给flutter侧 +/// @param key 事件的标识符 +/// @param arguments 事件的参数 +- (void)sendEventToFlutterWith:(NSString*)key arguments:(NSDictionary*)arguments; + +/// 卸载引擎 +- (void)unsetFlutterBoost; + +@end + diff --git a/flutter_boost_ios/ios/Classes/FlutterBoost.m b/flutter_boost_ios/ios/Classes/FlutterBoost.m new file mode 100644 index 000000000..6c724e250 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/FlutterBoost.m @@ -0,0 +1,227 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import +#import "FlutterBoost.h" +#import "FlutterBoostPlugin.h" +#import "Options.h" + +@interface FlutterBoost () +@property (nonatomic, strong) FlutterEngine* engine; +@property (nonatomic, strong) FlutterBoostPlugin* plugin; +@end + +@implementation FlutterBoost +- (void)setup:(UIApplication*)application + delegate:(id)delegate + callback:(void (^)(FlutterEngine *engine))callback { + // 调用默认的配置参数进行初始化 + [self setup:application + delegate:delegate + callback:callback + options:FlutterBoostSetupOptions.createDefault]; +} + +- (void)setup:(UIApplication*)application delegate:(id)delegate + callback:(void (^)(FlutterEngine *engine))callback + options:(FlutterBoostSetupOptions*)options { + if ([delegate respondsToSelector:@selector(engine)]) { + self.engine = delegate.engine; + } else { + self.engine = [[FlutterEngine alloc ] initWithName:@"io.flutter" project:options.dartObject]; + } + + // 从options中获取参数 + NSString* initialRoute = options.initalRoute; + NSString* dartEntrypointFunctionName = options.dartEntryPoint; + NSArray* dartEntryPointArgs = options.dartEntryPointArgs; + + void(^engineRun)(void) = ^(void) { + [self.engine runWithEntrypoint:dartEntrypointFunctionName + libraryURI:nil + initialRoute:initialRoute + entrypointArgs:dartEntryPointArgs]; + + // 根据配置提前预热引擎,配置默认预热引擎 + if (options.warmUpEngine){ + [self warmUpEngine]; + } + + Class clazz = NSClassFromString(@"GeneratedPluginRegistrant"); + SEL selector = NSSelectorFromString(@"registerWithRegistry:"); + if (clazz && selector && self.engine) { + if ([clazz respondsToSelector:selector]) { + ((void (*)(id, SEL, NSObject*registry))[clazz methodForSelector:selector])(clazz, selector, self.engine); + } + } + + self.plugin= [FlutterBoostPlugin getPlugin:self.engine]; + self.plugin.delegate=delegate; + + if (callback) { + callback(self.engine); + } + }; + + if ([NSThread isMainThread]) { + engineRun(); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + engineRun(); + }); + } + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidEnterBackground:) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; +} + +- (void)unsetFlutterBoost { + void (^engineDestroy)(void) = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + self.plugin.delegate = nil; + self.plugin = nil; + + [self.engine destroyContext]; + self.engine = nil; + }; + + if ([NSThread isMainThread]){ + engineDestroy(); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + engineDestroy(); + }); + } +} + +/// 提前预热引擎 +- (void)warmUpEngine { + FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.engine + nibName:nil bundle:nil]; + [vc beginAppearanceTransition:YES animated:NO]; + [vc endAppearanceTransition]; + [vc beginAppearanceTransition:NO animated:NO]; + [vc endAppearanceTransition]; +} + ++ (instancetype)instance { + static id _instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _instance = [self.class new]; + }); + + return _instance; +} + + +#pragma mark - Some properties. + +- (FlutterViewController *) currentViewController{ + return self.engine.viewController; +} + +#pragma mark - open/close Page +- (void)open:(NSString *)pageName arguments:(NSDictionary *)arguments + completion:(void(^)(BOOL)) completion { + FlutterBoostRouteOptions* options = [[FlutterBoostRouteOptions alloc]init]; + options.pageName = pageName; + options.arguments = arguments; + options.completion = completion; + + [self.plugin.delegate pushFlutterRoute:options]; +} + +- (void)open:(FlutterBoostRouteOptions* )options{ + [self.plugin.delegate pushFlutterRoute:options]; +} + +- (void)close:(NSString *)uniqueId { + FBCommonParams* params = [[FBCommonParams alloc] init]; + params.uniqueId=uniqueId; + [self.plugin.flutterApi popRouteParam:params + completion:^(NSError* error) { + }]; +} + +- (void)sendResultToFlutterWithPageName:(NSString*)pageName + arguments:(NSDictionary*) arguments { + FBCommonParams* params = [[FBCommonParams alloc] init]; + params.pageName = pageName; + params.arguments = arguments; + + [self.plugin.flutterApi onNativeResultParam:params + completion:^(NSError * error) { + }]; +} + + +- (void)applicationDidEnterBackground:(UIApplication *)application { + FBCommonParams* params = [[FBCommonParams alloc] init]; + [ self.plugin.flutterApi onBackgroundParam:params + completion:^(NSError * error) { + }]; +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + FBCommonParams* params = [[FBCommonParams alloc] init]; + [self.plugin.flutterApi onForegroundParam:params + completion:^(NSError * error) { + }]; +} + +- (FBVoidCallback)addEventListener:(FBEventListener)listener + forName:(NSString *)key { + return [self.plugin addEventListener:listener forName:key]; +} + +- (void)sendEventToFlutterWith:(NSString*)key + arguments:(NSDictionary*)arguments { + FBCommonParams* params = [[FBCommonParams alloc] init]; + params.key = key; + params.arguments = arguments; + [self.plugin.flutterApi sendEventToFlutterParam:params + completion:^(NSError * error) { + }]; +} + + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:UIApplicationWillEnterForegroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:UIApplicationDidEnterBackgroundNotification object:nil]; +} +@end diff --git a/flutter_boost_ios/ios/Classes/FlutterBoostDelegate.h b/flutter_boost_ios/ios/Classes/FlutterBoostDelegate.h new file mode 100644 index 000000000..194b97fda --- /dev/null +++ b/flutter_boost_ios/ios/Classes/FlutterBoostDelegate.h @@ -0,0 +1,43 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import "messages.h" +#import "Options.h" +#import + +@protocol FlutterBoostDelegate +@optional +- (FlutterEngine*) engine; +@required + +// 如果框架发现您输入的路由表在flutter里面注册的路由表中找不到,那么就会调用此方法来push一个纯原生页面 +- (void) pushNativeRoute:(NSString *) pageName arguments:(NSDictionary *) arguments; + +// 当框架的withContainer为true的时候,会调用此方法来做原生的push +- (void) pushFlutterRoute:(FlutterBoostRouteOptions *)options; + +// 当pop调用涉及到原生容器的时候,此方法将会被调用 +- (void) popRoute:(FlutterBoostRouteOptions *)options; +@end diff --git a/flutter_boost_ios/ios/Classes/FlutterBoostPlugin.h b/flutter_boost_ios/ios/Classes/FlutterBoostPlugin.h new file mode 100644 index 000000000..fbee90d17 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/FlutterBoostPlugin.h @@ -0,0 +1,47 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import +#import "messages.h" +#import "FlutterBoostDelegate.h" +#import "FBFlutterContainer.h" + +typedef void (^FBEventListener) (NSString *name, NSDictionary *arguments); +typedef void (^FBVoidCallback)(void); + +@interface FlutterBoostPlugin : NSObject +@property (nonatomic, strong) id delegate; +@property(nonatomic, strong) FBFlutterRouterApi* flutterApi; + +- (void)containerCreated:(id)container; +- (void)containerWillAppear:(id)container; +- (void)containerAppeared:(id)container; +- (void)containerDisappeared:(id)container; +- (void)containerDestroyed:(id)container; +- (void)onBackSwipe; + +- (FBVoidCallback)addEventListener:(FBEventListener)listener forName:(NSString *)key; ++ (FlutterBoostPlugin* )getPlugin:(FlutterEngine*)engine ; +@end diff --git a/flutter_boost_ios/ios/Classes/FlutterBoostPlugin.m b/flutter_boost_ios/ios/Classes/FlutterBoostPlugin.m new file mode 100644 index 000000000..ab89f2fc1 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/FlutterBoostPlugin.m @@ -0,0 +1,209 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#import +#import "FlutterBoostPlugin.h" +#import "messages.h" +#import "FlutterBoost.h" +#import "FBFlutterContainerManager.h" +#import "FBLifecycle.h" + +@interface FlutterBoostPlugin () +@property(nonatomic, strong) FBFlutterContainerManager* containerManager; +@property(nonatomic, strong) FBStackInfo* stackInfo; +@property(nonatomic, strong) NSMutableDictionary*>* listenersTable; +@end + +@implementation FlutterBoostPlugin +- (void)containerCreated:(id)vc { + [self.containerManager addContainer:vc forUniqueId:vc.uniqueIDString]; + if (self.containerManager.containerSize == 1) { + [FBLifecycle resume]; + } +} + +- (void)containerWillAppear:(id)vc { + FBCommonParams* params = [[FBCommonParams alloc] init]; + params.pageName = vc.name; + params.arguments = vc.params; + params.uniqueId = vc.uniqueId; + params.opaque = [[NSNumber alloc] initWithBool:vc.opaque]; + + [self.flutterApi pushRouteParam:params + completion:^(NSError * e) { + }]; + [self.containerManager activeContainer:vc + forUniqueId:vc.uniqueIDString]; +} + +- (void)containerAppeared:(id)vc { + FBCommonParams* params = [[FBCommonParams alloc] init]; + params.uniqueId = vc.uniqueId; + [self.flutterApi onContainerShowParam:params + completion:^(NSError * e) { + }]; +} + +- (void)containerDisappeared:(id)vc { + FBCommonParams* params = [[FBCommonParams alloc] init]; + params.uniqueId = vc.uniqueId; + [self.flutterApi onContainerHideParam:params + completion:^(NSError * e) { + }]; +} + +- (void)onBackSwipe { + [self.flutterApi onBackPressedWithCompletion: ^(NSError * e) { + }]; +} + +- (void)containerDestroyed:(id)vc { + FBCommonParams* params =[[FBCommonParams alloc] init]; + params.pageName = vc.name; + params.arguments = vc.params; + params.uniqueId = vc.uniqueId; + [self.flutterApi removeRouteParam:params + completion:^(NSError * e) { + }]; + [self.containerManager removeContainerByUniqueId:vc.uniqueIDString]; + if (self.containerManager.containerSize == 0) { + [FBLifecycle pause]; + } +} + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterBoostPlugin* plugin = [[FlutterBoostPlugin alloc] initWithMessenger:(registrar.messenger)]; + [registrar publish:plugin]; + FBNativeRouterApiSetup(registrar.messenger, plugin); +} + ++ (FlutterBoostPlugin* )getPlugin:(FlutterEngine*)engine{ + NSObject *published = [engine valuePublishedByPlugin:@"FlutterBoostPlugin"]; + if ([published isKindOfClass:[FlutterBoostPlugin class]]) { + FlutterBoostPlugin *plugin = (FlutterBoostPlugin *)published; + return plugin; + } + return nil; +} + +- (instancetype)initWithMessenger:(id)messenger { + self = [super init]; + if (self) { + _flutterApi = [[FBFlutterRouterApi alloc] initWithBinaryMessenger:messenger]; + _containerManager= [FBFlutterContainerManager new]; + _listenersTable = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)pushNativeRouteParam:(FBCommonParams*)input + error:(FlutterError *_Nullable *_Nonnull)error { + [self.delegate pushNativeRoute:input.pageName arguments:input.arguments]; +} + +- (void)pushFlutterRouteParam:(FBCommonParams*)input + error:(FlutterError *_Nullable *_Nonnull)error { + FlutterBoostRouteOptions* options = [[FlutterBoostRouteOptions alloc]init]; + options.pageName = input.pageName; + options.uniqueId = input.uniqueId; + options.arguments = input.arguments; + options.opaque = [input.opaque boolValue]; + + // 因为这里是flutter端开启新容器push一个页面,所以这里原生用不着,所以这里completion传一个空的即可 + options.completion = ^(BOOL completion) { + }; + + [self.delegate pushFlutterRoute: options]; +} + +- (void)popRouteParam:(FBCommonParams *)input + completion:(void(^)(FlutterError *_Nullable))completion { + if ([self.containerManager findContainerByUniqueId:input.uniqueId]) { + // 封装成options传回代理 + FlutterBoostRouteOptions* options = [[FlutterBoostRouteOptions alloc]init]; + options.pageName = input.pageName; + options.uniqueId = input.uniqueId; + options.arguments = input.arguments; + options.completion = ^(BOOL ret) { + }; + + // 调用代理回调给调用层 + [self.delegate popRoute:options]; + completion(nil); + } else { + completion([FlutterError errorWithCode:@"Invalid uniqueId" + message:@"No container to pop." + details:nil]); + } +} + +- (nullable FBStackInfo *)getStackFromHostWithError:(FlutterError *_Nullable *_Nonnull)error { + if (self.stackInfo == nil) { + return [[FBStackInfo alloc] init]; + } + return self.stackInfo; +} + +- (void)saveStackToHostStack:(FBStackInfo *)stack + error:(FlutterError *_Nullable *_Nonnull)error { + self.stackInfo = stack; +} + +// flutter端将会调用此方法给native发送信息,所以这里将是接收事件的逻辑 +- (void)sendEventToNativeParams:(FBCommonParams *)params + error:(FlutterError *_Nullable *_Nonnull)error { + NSString* key = params.key; + NSDictionary* args = params.arguments; + + assert(key != nil); + + // 如果arg是null,那么就生成一个空的字典传过去,避免null造成的崩溃 + if (args == nil) { + args = [NSDictionary dictionary]; + } + + // 从总事件表中找到和key对应的事件监听者列表 + NSMutableArray* listeners = self.listenersTable[key]; + + if (listeners == nil) return; + for (FBEventListener listener in listeners) { + listener(key,args); + } +} + +- (FBVoidCallback)addEventListener:(FBEventListener)listener + forName:(NSString *)key { + assert(key != nil && listener != nil); + NSMutableArray* listeners = self.listenersTable[key]; + if (listeners == nil) { + listeners = [[NSMutableArray alloc] init]; + self.listenersTable[key] = listeners; + } + + [listeners addObject:listener]; + + return ^{ + [listeners removeObject:listener]; + }; +} +@end diff --git a/flutter_boost_ios/ios/Classes/Options.h b/flutter_boost_ios/ios/Classes/Options.h new file mode 100644 index 000000000..361561eeb --- /dev/null +++ b/flutter_boost_ios/ios/Classes/Options.h @@ -0,0 +1,75 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + +#import + +//此文件用用于配置FlutterBoost各种配置文件 + +///启动参数配置 +@interface FlutterBoostSetupOptions : NSObject + +///初始路由 +@property (nonatomic, strong) NSString* initalRoute; + +///dart 入口 +@property (nonatomic, strong) NSString* dartEntryPoint; + +/// dart入口参数 +@property (nonatomic, strong) NSArray* dartEntryPointArgs; + +///FlutterDartProject数据 +@property (nonatomic, strong) FlutterDartProject* dartObject; + +///是否提前预热引擎,如果提前预热引擎,可以减少第一次打开flutter页面的短暂白屏,以及字体大小跳动的现象 +///默认值为YES +@property (nonatomic, assign) BOOL warmUpEngine; + +///创建一个默认的Options对象 ++ (FlutterBoostSetupOptions*)createDefault; + +@end + + +///路由参数配置 +@interface FlutterBoostRouteOptions : NSObject + +///页面在路由表中的名字 +@property(nonatomic, strong) NSString* pageName; + +///参数 +@property(nonatomic, strong) NSDictionary* arguments; + +///参数回传的回调闭包,仅在原生->flutter页面的时候有用 +@property(nonatomic, strong) void(^onPageFinished)(NSDictionary*); + +///open方法完成后的回调,仅在原生->flutter页面的时候有用 +@property(nonatomic, strong) void(^completion)(BOOL); + +///代理内部会使用,原生往flutter open的时候此参数设为nil即可 +@property(nonatomic, strong) NSString* uniqueId; + +///这个页面是否透明 注意:default value = YES +@property(nonatomic,assign) BOOL opaque; +@end diff --git a/flutter_boost_ios/ios/Classes/Options.m b/flutter_boost_ios/ios/Classes/Options.m new file mode 100644 index 000000000..ac27c6e7b --- /dev/null +++ b/flutter_boost_ios/ios/Classes/Options.m @@ -0,0 +1,52 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import "Options.h" + +@implementation FlutterBoostSetupOptions +- (instancetype)init { + self = [super init]; + if (self) { + self.dartEntryPoint = @"main"; + self.initalRoute = @"/"; + self.warmUpEngine = YES; + } + return self; +} + ++ (FlutterBoostSetupOptions*)createDefault { + return [[FlutterBoostSetupOptions alloc] init]; +} +@end + +@implementation FlutterBoostRouteOptions +- (instancetype)init { + self = [super init]; + if (self) { + //设置opaque默认为YES + self.opaque = YES; + } + return self; +} +@end diff --git a/flutter_boost_ios/ios/Classes/container/FBFlutterContainer.h b/flutter_boost_ios/ios/Classes/container/FBFlutterContainer.h new file mode 100644 index 000000000..751589b38 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBFlutterContainer.h @@ -0,0 +1,33 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#import + +@protocol FBFlutterContainer +- (NSString *)name; +- (NSDictionary *)params; +- (NSString *)uniqueId; +- (NSString *)uniqueIDString; +- (BOOL)opaque; +- (void)setName:(NSString *)name uniqueId:(NSString *)uniqueId params:(NSDictionary *)params opaque:(BOOL) opaque; +@end diff --git a/flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.h b/flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.h new file mode 100644 index 000000000..39148a8a4 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.h @@ -0,0 +1,36 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import "FBFlutterContainer.h" + +@interface FBFlutterContainerManager : NSObject +- (void)addContainer:(id)container forUniqueId:(NSString *)uniqueId; +- (void)activeContainer:(id)container forUniqueId:(NSString *)uniqueId; +- (void)removeContainerByUniqueId:(NSString *)uniqueId; +- (id)findContainerByUniqueId:(NSString *)uniqueId; +- (id)getTopContainer; +- (BOOL)isTopContainer:(NSString *)uniqueId; +- (NSInteger)containerSize; +@end diff --git a/flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.m b/flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.m new file mode 100644 index 000000000..b89f68451 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBFlutterContainerManager.m @@ -0,0 +1,87 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import "FBFlutterContainerManager.h" + +@interface FBFlutterContainerManager() +@property (nonatomic, strong) NSMutableDictionary *allContainers; +@property (nonatomic, strong) NSMutableArray *activeContainers; +@end + +@implementation FBFlutterContainerManager +- (instancetype)init { + if (self = [super init]) { + _allContainers = [NSMutableDictionary dictionary]; + _activeContainers = [NSMutableArray new]; + } + + return self; +} + +- (void)addContainer:(id)container + forUniqueId:(NSString *)uniqueId { + self.allContainers[uniqueId] = container; +} + +- (void)activeContainer:(id)container + forUniqueId:(NSString *)uniqueId { + if (uniqueId == nil || container == nil) return; + assert(self.allContainers[uniqueId] != nil); + if ([self.activeContainers containsObject:container]) { + [self.activeContainers removeObject:container]; + } + [self.activeContainers addObject:container]; +} + +- (void)removeContainerByUniqueId:(NSString *)uniqueId { + if (!uniqueId) return; + id container = self.allContainers[uniqueId]; + [self.allContainers removeObjectForKey:uniqueId]; + [self.activeContainers removeObject:container]; +} + +- (id)findContainerByUniqueId:(NSString *)uniqueId { + return self.allContainers[uniqueId]; +} + +- (id)getTopContainer { + if (self.activeContainers.count) { + return self.activeContainers.lastObject; + } + return nil; +} + +- (BOOL)isTopContainer:(NSString *)uniqueId { + id top = [self getTopContainer]; + if (top != nil && [top.uniqueIDString isEqualToString:uniqueId]) { + return YES; + } + return NO; +} + +- (NSInteger)containerSize { + return self.allContainers.count; +} +@end diff --git a/flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.h b/flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.h new file mode 100644 index 000000000..60e538343 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.h @@ -0,0 +1,41 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import +#import "FBFlutterContainer.h" + +@interface FBFlutterViewContainer : FlutterViewController +@property (nonatomic,copy,readwrite) NSString *name; +@property (nonatomic, strong) NSNumber *disablePopGesture; +@property (nonatomic, strong) NSNumber *enableLeftPanBackGesture; + +- (instancetype)init; +- (void)surfaceUpdated:(BOOL)appeared; +- (void)updateViewportMetrics; +- (void)detachFlutterEngineIfNeeded; +- (void)notifyWillDealloc; +@end + + diff --git a/flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.m b/flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.m new file mode 100644 index 000000000..d87d06c99 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBFlutterViewContainer.m @@ -0,0 +1,332 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#import +#import "FBFlutterViewContainer.h" +#import "FlutterBoost.h" +#import "FBLifecycle.h" +#import +#import + +#define ENGINE [[FlutterBoost instance] engine] +#define FB_PLUGIN [FlutterBoostPlugin getPlugin: [[FlutterBoost instance] engine]] + +#define weakify(var) ext_keywordify __weak typeof(var) O2OWeak_##var = var; +#define strongify(var) ext_keywordify \ +_Pragma("clang diagnostic push") \ +_Pragma("clang diagnostic ignored \"-Wshadow\"") \ +__strong typeof(var) var = O2OWeak_##var; \ +_Pragma("clang diagnostic pop") +#if DEBUG +# define ext_keywordify autoreleasepool {} +#else +# define ext_keywordify try {} @catch (...) {} +#endif + +@interface FlutterViewController (bridgeToviewDidDisappear) +- (void)flushOngoingTouches; +- (void)bridge_viewDidDisappear:(BOOL)animated; +- (void)bridge_viewWillAppear:(BOOL)animated; +- (void)surfaceUpdated:(BOOL)appeared; +- (void)updateViewportMetrics; +@end + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wincomplete-implementation" +@implementation FlutterViewController (bridgeToviewDidDisappear) +- (void)bridge_viewDidDisappear:(BOOL)animated { + [self flushOngoingTouches]; + [super viewDidDisappear:animated]; +} + +- (void)bridge_viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; +} +@end +#pragma pop + +@interface FBFlutterViewContainer () +@property (nonatomic,strong,readwrite) NSDictionary *params; +@property (nonatomic,copy) NSString *uniqueId; +@property (nonatomic, copy) NSString *flbNibName; +@property (nonatomic, strong) NSBundle *flbNibBundle; +@property (nonatomic, assign) BOOL opaque; +@property (nonatomic, strong) FBVoidCallback removeEventCallback; +@property (nonatomic, strong) UIScreenEdgePanGestureRecognizer* leftEdgeGesture; +@end + +@implementation FBFlutterViewContainer +- (instancetype)init { + ENGINE.viewController = nil; + if (self = [super initWithEngine:ENGINE + nibName:_flbNibName + bundle:_flbNibBundle]) { + // NOTES:在present页面时,默认是全屏,如此可以触发底层VC的页面事件。否则不会触发而导致异常 + self.modalPresentationStyle = UIModalPresentationFullScreen; + [self _setup]; + } + return self; +} + +- (instancetype)initWithProject:(FlutterDartProject*)projectOrNil + nibName:(NSString*)nibNameOrNil + bundle:(NSBundle*)nibBundleOrNil { + ENGINE.viewController = nil; + if (self = [super initWithProject:projectOrNil + nibName:nibNameOrNil + bundle:nibBundleOrNil]) { + [self _setup]; + } + return self; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-designated-initializers" +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder: aDecoder]) { + NSAssert(NO, @"unsupported init method!"); + [self _setup]; + } + return self; +} +#pragma pop + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil + bundle:(NSBundle *)nibBundleOrNil { + _flbNibName = nibNameOrNil; + _flbNibBundle = nibBundleOrNil; + ENGINE.viewController = nil; + return [self init]; +} + +- (void)setName:(NSString *)name + uniqueId:(NSString *)uniqueId + params:(NSDictionary *)params + opaque:(BOOL) opaque { + if (!_name && name) { + _name = name; + _params = params; + _opaque = opaque; + + // 这里如果是不透明的情况,才将viewOpaque 设为false, + // 并且才将modalStyle设为UIModalPresentationOverFullScreen + // 因为UIModalPresentationOverFullScreen模式下,下面的vc重新显示的时候不会 + // 调用viewAppear相关生命周期,所以需要手动调用beginAppearanceTransition相关方法来触发 + if (!_opaque) { + self.viewOpaque = opaque; + self.modalPresentationStyle = UIModalPresentationOverFullScreen; + } + if (uniqueId != nil) { + _uniqueId = uniqueId; + } + } + + [FB_PLUGIN containerCreated:self]; + + // 设置这个container对应的从flutter过来的事件监听 + [self setupEventListeningFromFlutter]; +} + +/// 设置这个container对应的从flutter过来的事件监听 +- (void)setupEventListeningFromFlutter { + @weakify(self) + // 为这个容器注册监听,监听内部的flutterPage往这个容器发的事件 + self.removeEventCallback = [FlutterBoost.instance addEventListener:^(NSString *name, NSDictionary *arguments) { + @strongify(self) + //事件名 + NSString *event = arguments[@"event"]; + + //事件参数 + NSDictionary *args = arguments[@"args"]; + + if ([event isEqualToString:@"enablePopGesture"]) { + // 多page情况下的侧滑动态禁用和启用事件 + NSNumber *enableNum = args[@"enable"]; + BOOL enable = [enableNum boolValue]; + self.navigationController.interactivePopGestureRecognizer.enabled = enable; + } + } forName:self.uniqueId]; +} + +- (NSString *)uniqueIDString { + return self.uniqueId; +} + +- (void)_setup { + self.uniqueId = [[NSUUID UUID] UUIDString]; +} + +- (void)didMoveToParentViewController:(UIViewController *)parent { + if (!parent) { + //当VC被移出parent时,就通知flutter层销毁page + [self detachFlutterEngineIfNeeded]; + [self notifyWillDealloc]; + } + [super didMoveToParentViewController:parent]; +} + +- (void)dismissViewControllerAnimated:(BOOL)flag + completion:(void (^)(void))completion { + [super dismissViewControllerAnimated:flag + completion:^() { + if (completion) { + completion(); + } + //当VC被dismiss时,就通知flutter层销毁page + [self detachFlutterEngineIfNeeded]; + [self notifyWillDealloc]; + }]; +} + +- (void)dealloc { + if (self.removeEventCallback != nil) { + self.removeEventCallback(); + } + [NSNotificationCenter.defaultCenter removeObserver:self]; + _leftEdgeGesture.delegate = nil; +} + +- (void)notifyWillDealloc { + [FB_PLUGIN containerDestroyed:self]; +} + +- (void)viewDidLoad { + // Ensure current view controller attach to Flutter engine + [self attatchFlutterEngine]; + + [super viewDidLoad]; + //只有在不透明情况下,才设置背景颜色,否则不设置颜色(也就是默认透明) + if (self.opaque) { + self.view.backgroundColor = UIColor.whiteColor; + } + + if (self.enableLeftPanBackGesture) { + _leftEdgeGesture = [[UIScreenEdgePanGestureRecognizer alloc] + initWithTarget:self + action:@selector(handleLeftEdgeGesture:)]; + _leftEdgeGesture.edges = UIRectEdgeLeft; + _leftEdgeGesture.delegate = self; + [self.view addGestureRecognizer:_leftEdgeGesture]; + } +} + +- (void)handleLeftEdgeGesture:(UIScreenEdgePanGestureRecognizer *)gesture { + if (UIGestureRecognizerStateEnded == gesture.state) { + [FB_PLUGIN onBackSwipe]; + } +} + +#pragma mark - ScreenShots +- (BOOL)isFlutterViewAttatched { + return ENGINE.viewController.view.superview == self.view; +} + +- (void)attatchFlutterEngine { + if (ENGINE.viewController != self){ + ENGINE.viewController = self; + } +} + +- (void)detachFlutterEngineIfNeeded { + if (self.engine.viewController == self) { + // need to call [surfaceUpdated:NO] to detach the view controller's ref from + // interal engine platformViewController,or dealloc will not be called after controller close. + // detail:https://github.com/flutter/engine/blob/07e2520d5d8f837da439317adab4ecd7bff2f72d/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm#L529 + [self surfaceUpdated:NO]; + + if (ENGINE.viewController != nil) { + ENGINE.viewController = nil; + } + } +} + +- (void)surfaceUpdated:(BOOL)appeared { + if (self.engine && self.engine.viewController == self) { + [super surfaceUpdated:appeared]; + } +} + +- (void)updateViewportMetrics { + if (self.engine && self.engine.viewController == self) { + [super updateViewportMetrics]; + } +} + +#pragma mark - Life circle methods + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; +} + +- (void)viewWillAppear:(BOOL)animated { + [FB_PLUGIN containerWillAppear:self]; + + // For new page we should attach flutter view in view will appear + // for better performance. + [self attatchFlutterEngine]; + + [super bridge_viewWillAppear:animated]; + [self.view setNeedsLayout];//TODO:通过param来设定 +} + +- (void)viewDidAppear:(BOOL)animated { + //Ensure flutter view is attached. + [self attatchFlutterEngine]; + + // 根据淘宝特价版日志证明,即使在UIViewController的viewDidAppear下,application也可能在inactive模式,此时如果提交渲染会导致GPU后台渲染而crash + // 参考:https://github.com/flutter/flutter/issues/57973 + // https://github.com/flutter/engine/pull/18742 + if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive){ + //NOTES:务必在show之后再update,否则有闪烁; 或导致侧滑返回时上一个页面会和top页面内容一样 + [self surfaceUpdated:YES]; + } + [super viewDidAppear:animated]; + + // Enable or disable pop gesture + // note: if disablePopGesture is nil, do nothing + if (self.disablePopGesture) { + self.navigationController.interactivePopGestureRecognizer.enabled = ![self.disablePopGesture boolValue]; + } + [FB_PLUGIN containerAppeared:self]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [[[UIApplication sharedApplication] keyWindow] endEditing:YES]; + [super viewWillDisappear:animated]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super bridge_viewDidDisappear:animated]; + [FB_PLUGIN containerDisappeared:self]; +} + +- (void)installSplashScreenViewIfNecessary { + //Do nothing. +} + +- (BOOL)loadDefaultSplashScreenView { + return YES; +} +@end + diff --git a/flutter_boost_ios/ios/Classes/container/FBLifecycle.h b/flutter_boost_ios/ios/Classes/container/FBLifecycle.h new file mode 100644 index 000000000..a99d42653 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBLifecycle.h @@ -0,0 +1,28 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +@interface FBLifecycle : NSObject ++ (void)pause; ++ (void)resume; +@end diff --git a/flutter_boost_ios/ios/Classes/container/FBLifecycle.m b/flutter_boost_ios/ios/Classes/container/FBLifecycle.m new file mode 100644 index 000000000..a0c155eac --- /dev/null +++ b/flutter_boost_ios/ios/Classes/container/FBLifecycle.m @@ -0,0 +1,44 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Alibaba Group + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#import +#import "FBLifecycle.h" +#import "FlutterBoost.h" +#import "FBFlutterContainer.h" + +#define ENGINE [[FlutterBoost instance] engine] + +@implementation FBLifecycle ++ (void)pause { + [[FlutterBoost instance]sendEventToFlutterWith:@"app_lifecycle_changed_key" + arguments:@{@"lifecycleState":@4}]; + if (ENGINE.viewController != nil){ + ENGINE.viewController = nil; + } +} + ++ (void)resume { + [[FlutterBoost instance]sendEventToFlutterWith:@"app_lifecycle_changed_key" + arguments:@{@"lifecycleState":@1}]; +} +@end diff --git a/flutter_boost_ios/ios/Classes/messages.h b/flutter_boost_ios/ios/Classes/messages.h new file mode 100644 index 000000000..57b96e238 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/messages.h @@ -0,0 +1,86 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +@class FBCommonParams; +@class FBStackInfo; +@class FBFlutterContainer; +@class FBFlutterPage; + +@interface FBCommonParams : NSObject ++ (instancetype)makeWithOpaque:(nullable NSNumber *)opaque + key:(nullable NSString *)key + pageName:(nullable NSString *)pageName + uniqueId:(nullable NSString *)uniqueId + arguments:(nullable NSDictionary *)arguments; +@property(nonatomic, strong, nullable) NSNumber * opaque; +@property(nonatomic, copy, nullable) NSString * key; +@property(nonatomic, copy, nullable) NSString * pageName; +@property(nonatomic, copy, nullable) NSString * uniqueId; +@property(nonatomic, strong, nullable) NSDictionary * arguments; +@end + +@interface FBStackInfo : NSObject ++ (instancetype)makeWithIds:(nullable NSArray *)ids + containers:(nullable NSDictionary *)containers; +@property(nonatomic, strong, nullable) NSArray * ids; +@property(nonatomic, strong, nullable) NSDictionary * containers; +@end + +@interface FBFlutterContainer : NSObject ++ (instancetype)makeWithPages:(nullable NSArray *)pages; +@property(nonatomic, strong, nullable) NSArray * pages; +@end + +@interface FBFlutterPage : NSObject ++ (instancetype)makeWithWithContainer:(nullable NSNumber *)withContainer + pageName:(nullable NSString *)pageName + uniqueId:(nullable NSString *)uniqueId + arguments:(nullable NSDictionary *)arguments; +@property(nonatomic, strong, nullable) NSNumber * withContainer; +@property(nonatomic, copy, nullable) NSString * pageName; +@property(nonatomic, copy, nullable) NSString * uniqueId; +@property(nonatomic, strong, nullable) NSDictionary * arguments; +@end + +/// The codec used by FBNativeRouterApi. +NSObject *FBNativeRouterApiGetCodec(void); + +@protocol FBNativeRouterApi +- (void)pushNativeRouteParam:(FBCommonParams *)param error:(FlutterError *_Nullable *_Nonnull)error; +- (void)pushFlutterRouteParam:(FBCommonParams *)param error:(FlutterError *_Nullable *_Nonnull)error; +- (void)popRouteParam:(FBCommonParams *)param completion:(void(^)(FlutterError *_Nullable))completion; +/// @return `nil` only when `error != nil`. +- (nullable FBStackInfo *)getStackFromHostWithError:(FlutterError *_Nullable *_Nonnull)error; +- (void)saveStackToHostStack:(FBStackInfo *)stack error:(FlutterError *_Nullable *_Nonnull)error; +- (void)sendEventToNativeParams:(FBCommonParams *)params error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FBNativeRouterApiSetup(id binaryMessenger, NSObject *_Nullable api); + +/// The codec used by FBFlutterRouterApi. +NSObject *FBFlutterRouterApiGetCodec(void); + +@interface FBFlutterRouterApi : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger; +- (void)pushRouteParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)popRouteParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)removeRouteParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)onForegroundParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)onBackgroundParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)onNativeResultParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)onContainerShowParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)onContainerHideParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)sendEventToFlutterParam:(FBCommonParams *)param completion:(void(^)(NSError *_Nullable))completion; +- (void)onBackPressedWithCompletion:(void(^)(NSError *_Nullable))completion; +@end +NS_ASSUME_NONNULL_END diff --git a/flutter_boost_ios/ios/Classes/messages.m b/flutter_boost_ios/ios/Classes/messages.m new file mode 100644 index 000000000..b78863a07 --- /dev/null +++ b/flutter_boost_ios/ios/Classes/messages.m @@ -0,0 +1,526 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import "messages.h" +#import + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSDictionary *wrapResult(id result, FlutterError *error) { + NSDictionary *errorDict = (NSDictionary *)[NSNull null]; + if (error) { + errorDict = @{ + @"code": (error.code ?: [NSNull null]), + @"message": (error.message ?: [NSNull null]), + @"details": (error.details ?: [NSNull null]), + }; + } + return @{ + @"result": (result ?: [NSNull null]), + @"error": errorDict, + }; +} +static id GetNullableObject(NSDictionary* dict, id key) { + id result = dict[key]; + return (result == [NSNull null]) ? nil : result; +} +static id GetNullableObjectAtIndex(NSArray* array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + + +@interface FBCommonParams () ++ (FBCommonParams *)fromMap:(NSDictionary *)dict; ++ (nullable FBCommonParams *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FBStackInfo () ++ (FBStackInfo *)fromMap:(NSDictionary *)dict; ++ (nullable FBStackInfo *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FBFlutterContainer () ++ (FBFlutterContainer *)fromMap:(NSDictionary *)dict; ++ (nullable FBFlutterContainer *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FBFlutterPage () ++ (FBFlutterPage *)fromMap:(NSDictionary *)dict; ++ (nullable FBFlutterPage *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end + +@implementation FBCommonParams ++ (instancetype)makeWithOpaque:(nullable NSNumber *)opaque + key:(nullable NSString *)key + pageName:(nullable NSString *)pageName + uniqueId:(nullable NSString *)uniqueId + arguments:(nullable NSDictionary *)arguments { + FBCommonParams* pigeonResult = [[FBCommonParams alloc] init]; + pigeonResult.opaque = opaque; + pigeonResult.key = key; + pigeonResult.pageName = pageName; + pigeonResult.uniqueId = uniqueId; + pigeonResult.arguments = arguments; + return pigeonResult; +} ++ (FBCommonParams *)fromMap:(NSDictionary *)dict { + FBCommonParams *pigeonResult = [[FBCommonParams alloc] init]; + pigeonResult.opaque = GetNullableObject(dict, @"opaque"); + pigeonResult.key = GetNullableObject(dict, @"key"); + pigeonResult.pageName = GetNullableObject(dict, @"pageName"); + pigeonResult.uniqueId = GetNullableObject(dict, @"uniqueId"); + pigeonResult.arguments = GetNullableObject(dict, @"arguments"); + return pigeonResult; +} ++ (nullable FBCommonParams *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [FBCommonParams fromMap:dict] : nil; } +- (NSDictionary *)toMap { + return @{ + @"opaque" : (self.opaque ?: [NSNull null]), + @"key" : (self.key ?: [NSNull null]), + @"pageName" : (self.pageName ?: [NSNull null]), + @"uniqueId" : (self.uniqueId ?: [NSNull null]), + @"arguments" : (self.arguments ?: [NSNull null]), + }; +} +@end + +@implementation FBStackInfo ++ (instancetype)makeWithIds:(nullable NSArray *)ids + containers:(nullable NSDictionary *)containers { + FBStackInfo* pigeonResult = [[FBStackInfo alloc] init]; + pigeonResult.ids = ids; + pigeonResult.containers = containers; + return pigeonResult; +} ++ (FBStackInfo *)fromMap:(NSDictionary *)dict { + FBStackInfo *pigeonResult = [[FBStackInfo alloc] init]; + pigeonResult.ids = GetNullableObject(dict, @"ids"); + pigeonResult.containers = GetNullableObject(dict, @"containers"); + return pigeonResult; +} ++ (nullable FBStackInfo *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [FBStackInfo fromMap:dict] : nil; } +- (NSDictionary *)toMap { + return @{ + @"ids" : (self.ids ?: [NSNull null]), + @"containers" : (self.containers ?: [NSNull null]), + }; +} +@end + +@implementation FBFlutterContainer ++ (instancetype)makeWithPages:(nullable NSArray *)pages { + FBFlutterContainer* pigeonResult = [[FBFlutterContainer alloc] init]; + pigeonResult.pages = pages; + return pigeonResult; +} ++ (FBFlutterContainer *)fromMap:(NSDictionary *)dict { + FBFlutterContainer *pigeonResult = [[FBFlutterContainer alloc] init]; + pigeonResult.pages = GetNullableObject(dict, @"pages"); + return pigeonResult; +} ++ (nullable FBFlutterContainer *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [FBFlutterContainer fromMap:dict] : nil; } +- (NSDictionary *)toMap { + return @{ + @"pages" : (self.pages ?: [NSNull null]), + }; +} +@end + +@implementation FBFlutterPage ++ (instancetype)makeWithWithContainer:(nullable NSNumber *)withContainer + pageName:(nullable NSString *)pageName + uniqueId:(nullable NSString *)uniqueId + arguments:(nullable NSDictionary *)arguments { + FBFlutterPage* pigeonResult = [[FBFlutterPage alloc] init]; + pigeonResult.withContainer = withContainer; + pigeonResult.pageName = pageName; + pigeonResult.uniqueId = uniqueId; + pigeonResult.arguments = arguments; + return pigeonResult; +} ++ (FBFlutterPage *)fromMap:(NSDictionary *)dict { + FBFlutterPage *pigeonResult = [[FBFlutterPage alloc] init]; + pigeonResult.withContainer = GetNullableObject(dict, @"withContainer"); + pigeonResult.pageName = GetNullableObject(dict, @"pageName"); + pigeonResult.uniqueId = GetNullableObject(dict, @"uniqueId"); + pigeonResult.arguments = GetNullableObject(dict, @"arguments"); + return pigeonResult; +} ++ (nullable FBFlutterPage *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [FBFlutterPage fromMap:dict] : nil; } +- (NSDictionary *)toMap { + return @{ + @"withContainer" : (self.withContainer ?: [NSNull null]), + @"pageName" : (self.pageName ?: [NSNull null]), + @"uniqueId" : (self.uniqueId ?: [NSNull null]), + @"arguments" : (self.arguments ?: [NSNull null]), + }; +} +@end + +@interface FBNativeRouterApiCodecReader : FlutterStandardReader +@end +@implementation FBNativeRouterApiCodecReader +- (nullable id)readValueOfType:(UInt8)type +{ + switch (type) { + case 128: + return [FBCommonParams fromMap:[self readValue]]; + + case 129: + return [FBFlutterContainer fromMap:[self readValue]]; + + case 130: + return [FBFlutterPage fromMap:[self readValue]]; + + case 131: + return [FBStackInfo fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + + } +} +@end + +@interface FBNativeRouterApiCodecWriter : FlutterStandardWriter +@end +@implementation FBNativeRouterApiCodecWriter +- (void)writeValue:(id)value +{ + if ([value isKindOfClass:[FBCommonParams class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else + if ([value isKindOfClass:[FBFlutterContainer class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else + if ([value isKindOfClass:[FBFlutterPage class]]) { + [self writeByte:130]; + [self writeValue:[value toMap]]; + } else + if ([value isKindOfClass:[FBStackInfo class]]) { + [self writeByte:131]; + [self writeValue:[value toMap]]; + } else +{ + [super writeValue:value]; + } +} +@end + +@interface FBNativeRouterApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FBNativeRouterApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FBNativeRouterApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FBNativeRouterApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FBNativeRouterApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FBNativeRouterApiCodecReaderWriter *readerWriter = [[FBNativeRouterApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + + +void FBNativeRouterApiSetup(id binaryMessenger, NSObject *api) { + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NativeRouterApi.pushNativeRoute" + binaryMessenger:binaryMessenger + codec:FBNativeRouterApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pushNativeRouteParam:error:)], @"FBNativeRouterApi api (%@) doesn't respond to @selector(pushNativeRouteParam:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FBCommonParams *arg_param = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api pushNativeRouteParam:arg_param error:&error]; + callback(wrapResult(nil, error)); + }]; + } + else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NativeRouterApi.pushFlutterRoute" + binaryMessenger:binaryMessenger + codec:FBNativeRouterApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pushFlutterRouteParam:error:)], @"FBNativeRouterApi api (%@) doesn't respond to @selector(pushFlutterRouteParam:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FBCommonParams *arg_param = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api pushFlutterRouteParam:arg_param error:&error]; + callback(wrapResult(nil, error)); + }]; + } + else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NativeRouterApi.popRoute" + binaryMessenger:binaryMessenger + codec:FBNativeRouterApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(popRouteParam:completion:)], @"FBNativeRouterApi api (%@) doesn't respond to @selector(popRouteParam:completion:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FBCommonParams *arg_param = GetNullableObjectAtIndex(args, 0); + [api popRouteParam:arg_param completion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; + }]; + } + else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NativeRouterApi.getStackFromHost" + binaryMessenger:binaryMessenger + codec:FBNativeRouterApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getStackFromHostWithError:)], @"FBNativeRouterApi api (%@) doesn't respond to @selector(getStackFromHostWithError:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + FBStackInfo *output = [api getStackFromHostWithError:&error]; + callback(wrapResult(output, error)); + }]; + } + else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NativeRouterApi.saveStackToHost" + binaryMessenger:binaryMessenger + codec:FBNativeRouterApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(saveStackToHostStack:error:)], @"FBNativeRouterApi api (%@) doesn't respond to @selector(saveStackToHostStack:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FBStackInfo *arg_stack = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api saveStackToHostStack:arg_stack error:&error]; + callback(wrapResult(nil, error)); + }]; + } + else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NativeRouterApi.sendEventToNative" + binaryMessenger:binaryMessenger + codec:FBNativeRouterApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(sendEventToNativeParams:error:)], @"FBNativeRouterApi api (%@) doesn't respond to @selector(sendEventToNativeParams:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FBCommonParams *arg_params = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api sendEventToNativeParams:arg_params error:&error]; + callback(wrapResult(nil, error)); + }]; + } + else { + [channel setMessageHandler:nil]; + } + } +} +@interface FBFlutterRouterApiCodecReader : FlutterStandardReader +@end +@implementation FBFlutterRouterApiCodecReader +- (nullable id)readValueOfType:(UInt8)type +{ + switch (type) { + case 128: + return [FBCommonParams fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + + } +} +@end + +@interface FBFlutterRouterApiCodecWriter : FlutterStandardWriter +@end +@implementation FBFlutterRouterApiCodecWriter +- (void)writeValue:(id)value +{ + if ([value isKindOfClass:[FBCommonParams class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else +{ + [super writeValue:value]; + } +} +@end + +@interface FBFlutterRouterApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FBFlutterRouterApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FBFlutterRouterApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FBFlutterRouterApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FBFlutterRouterApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FBFlutterRouterApiCodecReaderWriter *readerWriter = [[FBFlutterRouterApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + + +@interface FBFlutterRouterApi () +@property (nonatomic, strong) NSObject *binaryMessenger; +@end + +@implementation FBFlutterRouterApi + +- (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { + self = [super init]; + if (self) { + _binaryMessenger = binaryMessenger; + } + return self; +} +- (void)pushRouteParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.pushRoute" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)popRouteParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.popRoute" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)removeRouteParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.removeRoute" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)onForegroundParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.onForeground" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)onBackgroundParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.onBackground" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)onNativeResultParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.onNativeResult" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)onContainerShowParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.onContainerShow" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)onContainerHideParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.onContainerHide" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)sendEventToFlutterParam:(FBCommonParams *)arg_param completion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:@[arg_param ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)onBackPressedWithCompletion:(void(^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FlutterRouterApi.onBackPressed" + binaryMessenger:self.binaryMessenger + codec:FBFlutterRouterApiGetCodec()]; + [channel sendMessage:nil reply:^(id reply) { + completion(nil); + }]; +} +@end diff --git a/flutter_boost_ios/ios/flutter_boost_ios.podspec b/flutter_boost_ios/ios/flutter_boost_ios.podspec new file mode 100755 index 000000000..803ee0fac --- /dev/null +++ b/flutter_boost_ios/ios/flutter_boost_ios.podspec @@ -0,0 +1,36 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'flutter_boost_ios' + s.version = '5.0.2' + s.summary = 'iOS implementation of the flutter_boost plugin.' + s.description = <<-DESC +iOS implementation of the flutter_boost plugin. + DESC + s.homepage = 'https://github.com/alibaba/flutter_boost' + s.license = { :file => '../LICENSE' } + s.author = { 'Alibaba Xianyu' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*.{h,m,mm}' + + s.public_header_files = + 'Classes/FlutterBoost.h', + 'Classes/FlutterBoostDelegate.h', + 'Classes/FlutterBoostPlugin.h', + 'Classes/container/FBFlutterViewContainer.h', + 'Classes/container/FBFlutterContainer.h', + 'Classes/Options.h', + 'Classes/messages.h' + + + s.dependency 'Flutter' + s.libraries = 'c++' + s.pod_target_xcconfig = { + 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++11', + 'CLANG_CXX_LIBRARY' => 'libc++' + } + + s.ios.deployment_target = '8.0' +end + diff --git a/flutter_boost_ios/lib/flutter_boost_ios.dart b/flutter_boost_ios/lib/flutter_boost_ios.dart new file mode 100644 index 000000000..82bd4dfbb --- /dev/null +++ b/flutter_boost_ios/lib/flutter_boost_ios.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +library flutter_boost_ios; + +// This package does not export any Dart code to the app-facing package. +// It only provides the iOS platform implementation. diff --git a/flutter_boost_ios/pubspec.yaml b/flutter_boost_ios/pubspec.yaml new file mode 100644 index 000000000..5b3b8b3bf --- /dev/null +++ b/flutter_boost_ios/pubspec.yaml @@ -0,0 +1,30 @@ +# Copyright (c) 2019 Alibaba Group. All rights reserved. +# Use of this source code is governed by a MIT license that can be +# found in the LICENSE file. + +name: flutter_boost_ios +description: iOS implementation of the flutter_boost plugin. +version: 5.0.2 +homepage: https://github.com/alibaba/flutter_boost + +environment: + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" + +dependencies: + flutter: + sdk: flutter + flutter_boost_platform_interface: + path: ../flutter_boost_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.1 + +flutter: + plugin: + implements: flutter_boost + platforms: + ios: + pluginClass: FlutterBoostPlugin diff --git a/flutter_boost_platform_interface/.gitignore b/flutter_boost_platform_interface/.gitignore new file mode 100644 index 000000000..6d11bab8b --- /dev/null +++ b/flutter_boost_platform_interface/.gitignore @@ -0,0 +1,27 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ +pubspec.lock diff --git a/flutter_boost_platform_interface/CHANGELOG.md b/flutter_boost_platform_interface/CHANGELOG.md new file mode 100644 index 000000000..c20e3bff3 --- /dev/null +++ b/flutter_boost_platform_interface/CHANGELOG.md @@ -0,0 +1,4 @@ +## 5.0.2 + +* Initial release of the platform interface for flutter_boost. +* Extracted from `flutter_boost` as part of federated plugin migration. diff --git a/flutter_boost_platform_interface/LICENSE b/flutter_boost_platform_interface/LICENSE new file mode 100755 index 000000000..88ed1cb37 --- /dev/null +++ b/flutter_boost_platform_interface/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Alibaba Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/flutter_boost_platform_interface/README.md b/flutter_boost_platform_interface/README.md new file mode 100644 index 000000000..8ac755e90 --- /dev/null +++ b/flutter_boost_platform_interface/README.md @@ -0,0 +1,13 @@ +# flutter_boost_platform_interface + +A common platform interface for the `flutter_boost` plugin. + +This interface allows platform-specific implementations of the `flutter_boost` plugin, as well as the plugin itself, to ensure they are supporting the same interface. + +## Usage + +To implement a new platform-specific implementation of `flutter_boost`, extend `FlutterBoostPlatform` with an implementation that performs the platform-specific behavior. + +## Note + +This package is not intended for direct use by end users. For the app-facing plugin, see [flutter_boost](https://pub.dev/packages/flutter_boost). diff --git a/flutter_boost_platform_interface/analysis_options.yaml b/flutter_boost_platform_interface/analysis_options.yaml new file mode 100644 index 000000000..8d81c200c --- /dev/null +++ b/flutter_boost_platform_interface/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + avoid_print: false diff --git a/flutter_boost_platform_interface/lib/flutter_boost_platform_interface.dart b/flutter_boost_platform_interface/lib/flutter_boost_platform_interface.dart new file mode 100644 index 000000000..4f36fa1a6 --- /dev/null +++ b/flutter_boost_platform_interface/lib/flutter_boost_platform_interface.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +library flutter_boost_platform_interface; + +export 'src/flutter_boost_platform_interface.dart'; +export 'src/method_channel_flutter_boost.dart'; +export 'src/messages.dart'; diff --git a/flutter_boost_platform_interface/lib/src/flutter_boost_platform_interface.dart b/flutter_boost_platform_interface/lib/src/flutter_boost_platform_interface.dart new file mode 100644 index 000000000..d3a09b138 --- /dev/null +++ b/flutter_boost_platform_interface/lib/src/flutter_boost_platform_interface.dart @@ -0,0 +1,43 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'messages.dart'; +import 'method_channel_flutter_boost.dart'; + +/// The interface that implementations of flutter_boost must implement. +/// +/// Platform implementations should extend this class rather than implement it as `flutter_boost` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [FlutterBoostPlatform] methods. +abstract class FlutterBoostPlatform extends PlatformInterface { + /// Constructs a FlutterBoostPlatform. + FlutterBoostPlatform() : super(token: _token); + + static final Object _token = Object(); + + static FlutterBoostPlatform _instance = MethodChannelFlutterBoost(); + + /// The default instance of [FlutterBoostPlatform] to use. + /// + /// Defaults to [MethodChannelFlutterBoost]. + static FlutterBoostPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [FlutterBoostPlatform] when + /// they register themselves. + static set instance(FlutterBoostPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Returns a [NativeRouterApi] instance for communicating with native side. + NativeRouterApi getNativeRouterApi(); + + /// Sets up the [FlutterRouterApi] for receiving messages from native side. + void setupFlutterRouterApi(FlutterRouterApi? api); +} diff --git a/flutter_boost_platform_interface/lib/src/messages.dart b/flutter_boost_platform_interface/lib/src/messages.dart new file mode 100644 index 000000000..2d8fd9594 --- /dev/null +++ b/flutter_boost_platform_interface/lib/src/messages.dart @@ -0,0 +1,519 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; + +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; + +class CommonParams { + CommonParams({ + this.opaque, + this.key, + this.pageName, + this.uniqueId, + this.arguments, + }); + + bool? opaque; + String? key; + String? pageName; + String? uniqueId; + Map? arguments; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['opaque'] = opaque; + pigeonMap['key'] = key; + pigeonMap['pageName'] = pageName; + pigeonMap['uniqueId'] = uniqueId; + pigeonMap['arguments'] = arguments; + return pigeonMap; + } + + static CommonParams decode(Object message) { + final Map pigeonMap = message as Map; + return CommonParams( + opaque: pigeonMap['opaque'] as bool?, + key: pigeonMap['key'] as String?, + pageName: pigeonMap['pageName'] as String?, + uniqueId: pigeonMap['uniqueId'] as String?, + arguments: (pigeonMap['arguments'] as Map?)?.cast(), + ); + } +} + +class StackInfo { + StackInfo({ + this.ids, + this.containers, + }); + + List? ids; + Map? containers; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['ids'] = ids; + pigeonMap['containers'] = containers; + return pigeonMap; + } + + static StackInfo decode(Object message) { + final Map pigeonMap = message as Map; + return StackInfo( + ids: (pigeonMap['ids'] as List?)?.cast(), + containers: (pigeonMap['containers'] as Map?)?.cast(), + ); + } +} + +class FlutterContainer { + FlutterContainer({ + this.pages, + }); + + List? pages; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['pages'] = pages; + return pigeonMap; + } + + static FlutterContainer decode(Object message) { + final Map pigeonMap = message as Map; + return FlutterContainer( + pages: (pigeonMap['pages'] as List?)?.cast(), + ); + } +} + +class FlutterPage { + FlutterPage({ + this.withContainer, + this.pageName, + this.uniqueId, + this.arguments, + }); + + bool? withContainer; + String? pageName; + String? uniqueId; + Map? arguments; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['withContainer'] = withContainer; + pigeonMap['pageName'] = pageName; + pigeonMap['uniqueId'] = uniqueId; + pigeonMap['arguments'] = arguments; + return pigeonMap; + } + + static FlutterPage decode(Object message) { + final Map pigeonMap = message as Map; + return FlutterPage( + withContainer: pigeonMap['withContainer'] as bool?, + pageName: pigeonMap['pageName'] as String?, + uniqueId: pigeonMap['uniqueId'] as String?, + arguments: (pigeonMap['arguments'] as Map?)?.cast(), + ); + } +} + +class _NativeRouterApiCodec extends StandardMessageCodec { + const _NativeRouterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CommonParams) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else + if (value is FlutterContainer) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else + if (value is FlutterPage) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else + if (value is StackInfo) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else +{ + super.writeValue(buffer, value); + } + } + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CommonParams.decode(readValue(buffer)!); + + case 129: + return FlutterContainer.decode(readValue(buffer)!); + + case 130: + return FlutterPage.decode(readValue(buffer)!); + + case 131: + return StackInfo.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + + } + } +} + +class NativeRouterApi { + /// Constructor for [NativeRouterApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + NativeRouterApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _NativeRouterApiCodec(); + + Future pushNativeRoute(CommonParams arg_param) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NativeRouterApi.pushNativeRoute', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_param]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future pushFlutterRoute(CommonParams arg_param) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NativeRouterApi.pushFlutterRoute', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_param]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future popRoute(CommonParams arg_param) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NativeRouterApi.popRoute', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_param]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future getStackFromHost() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NativeRouterApi.getStackFromHost', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as StackInfo?)!; + } + } + + Future saveStackToHost(StackInfo arg_stack) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NativeRouterApi.saveStackToHost', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_stack]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future sendEventToNative(CommonParams arg_params) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NativeRouterApi.sendEventToNative', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_params]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _FlutterRouterApiCodec extends StandardMessageCodec { + const _FlutterRouterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CommonParams) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else +{ + super.writeValue(buffer, value); + } + } + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CommonParams.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + + } + } +} +abstract class FlutterRouterApi { + static const MessageCodec codec = _FlutterRouterApiCodec(); + + void pushRoute(CommonParams param); + void popRoute(CommonParams param); + void removeRoute(CommonParams param); + void onForeground(CommonParams param); + void onBackground(CommonParams param); + void onNativeResult(CommonParams param); + void onContainerShow(CommonParams param); + void onContainerHide(CommonParams param); + void sendEventToFlutter(CommonParams param); + void onBackPressed(); + static void setup(FlutterRouterApi? api, {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.pushRoute', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.pushRoute was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.pushRoute was null, expected non-null CommonParams.'); + api.pushRoute(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.popRoute', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.popRoute was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.popRoute was null, expected non-null CommonParams.'); + api.popRoute(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.removeRoute', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.removeRoute was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.removeRoute was null, expected non-null CommonParams.'); + api.removeRoute(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.onForeground', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onForeground was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onForeground was null, expected non-null CommonParams.'); + api.onForeground(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.onBackground', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onBackground was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onBackground was null, expected non-null CommonParams.'); + api.onBackground(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.onNativeResult', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onNativeResult was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onNativeResult was null, expected non-null CommonParams.'); + api.onNativeResult(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.onContainerShow', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerShow was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerShow was null, expected non-null CommonParams.'); + api.onContainerShow(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.onContainerHide', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerHide was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerHide was null, expected non-null CommonParams.'); + api.onContainerHide(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter was null.'); + final List args = (message as List?)!; + final CommonParams? arg_param = (args[0] as CommonParams?); + assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter was null, expected non-null CommonParams.'); + api.sendEventToFlutter(arg_param!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterRouterApi.onBackPressed', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + // ignore message + api.onBackPressed(); + return; + }); + } + } + } +} diff --git a/flutter_boost_platform_interface/lib/src/method_channel_flutter_boost.dart b/flutter_boost_platform_interface/lib/src/method_channel_flutter_boost.dart new file mode 100644 index 000000000..f2d93b344 --- /dev/null +++ b/flutter_boost_platform_interface/lib/src/method_channel_flutter_boost.dart @@ -0,0 +1,22 @@ +// Copyright (c) 2019 Alibaba Group. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import 'messages.dart'; +import 'flutter_boost_platform_interface.dart'; + +/// An implementation of [FlutterBoostPlatform] that uses method channels. +class MethodChannelFlutterBoost extends FlutterBoostPlatform { + NativeRouterApi? _nativeRouterApi; + + @override + NativeRouterApi getNativeRouterApi() { + _nativeRouterApi ??= NativeRouterApi(); + return _nativeRouterApi!; + } + + @override + void setupFlutterRouterApi(FlutterRouterApi? api) { + FlutterRouterApi.setup(api); + } +} diff --git a/flutter_boost_platform_interface/pubspec.yaml b/flutter_boost_platform_interface/pubspec.yaml new file mode 100644 index 000000000..a58b415b5 --- /dev/null +++ b/flutter_boost_platform_interface/pubspec.yaml @@ -0,0 +1,23 @@ +# Copyright (c) 2019 Alibaba Group. All rights reserved. +# Use of this source code is governed by a MIT license that can be +# found in the LICENSE file. + +name: flutter_boost_platform_interface +description: A common platform interface for the flutter_boost plugin. +version: 5.0.2 +homepage: https://github.com/alibaba/flutter_boost + +environment: + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.0 + collection: ^1.16.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.1 diff --git a/lib/flutter_boost.dart b/lib/flutter_boost.dart index 870d44852..d9f2b2978 100644 --- a/lib/flutter_boost.dart +++ b/lib/flutter_boost.dart @@ -4,6 +4,10 @@ library flutter_boost; +// Export platform interface messages +export 'package:flutter_boost_platform_interface/flutter_boost_platform_interface.dart' + show CommonParams, StackInfo, FlutterContainer, FlutterPage; + export 'src/boost_channel.dart'; export 'src/boost_container.dart'; export 'src/boost_flutter_binding.dart'; diff --git a/lib/src/messages.dart b/lib/src/messages.dart index 2d8fd9594..8749eb85a 100644 --- a/lib/src/messages.dart +++ b/lib/src/messages.dart @@ -1,519 +1,13 @@ // Copyright (c) 2019 Alibaba Group. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v3.2.9), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import -import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; -import 'package:flutter/services.dart'; - -class CommonParams { - CommonParams({ - this.opaque, - this.key, - this.pageName, - this.uniqueId, - this.arguments, - }); - - bool? opaque; - String? key; - String? pageName; - String? uniqueId; - Map? arguments; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['opaque'] = opaque; - pigeonMap['key'] = key; - pigeonMap['pageName'] = pageName; - pigeonMap['uniqueId'] = uniqueId; - pigeonMap['arguments'] = arguments; - return pigeonMap; - } - - static CommonParams decode(Object message) { - final Map pigeonMap = message as Map; - return CommonParams( - opaque: pigeonMap['opaque'] as bool?, - key: pigeonMap['key'] as String?, - pageName: pigeonMap['pageName'] as String?, - uniqueId: pigeonMap['uniqueId'] as String?, - arguments: (pigeonMap['arguments'] as Map?)?.cast(), - ); - } -} - -class StackInfo { - StackInfo({ - this.ids, - this.containers, - }); - - List? ids; - Map? containers; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['ids'] = ids; - pigeonMap['containers'] = containers; - return pigeonMap; - } - - static StackInfo decode(Object message) { - final Map pigeonMap = message as Map; - return StackInfo( - ids: (pigeonMap['ids'] as List?)?.cast(), - containers: (pigeonMap['containers'] as Map?)?.cast(), - ); - } -} - -class FlutterContainer { - FlutterContainer({ - this.pages, - }); - - List? pages; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['pages'] = pages; - return pigeonMap; - } - - static FlutterContainer decode(Object message) { - final Map pigeonMap = message as Map; - return FlutterContainer( - pages: (pigeonMap['pages'] as List?)?.cast(), - ); - } -} - -class FlutterPage { - FlutterPage({ - this.withContainer, - this.pageName, - this.uniqueId, - this.arguments, - }); - - bool? withContainer; - String? pageName; - String? uniqueId; - Map? arguments; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['withContainer'] = withContainer; - pigeonMap['pageName'] = pageName; - pigeonMap['uniqueId'] = uniqueId; - pigeonMap['arguments'] = arguments; - return pigeonMap; - } - - static FlutterPage decode(Object message) { - final Map pigeonMap = message as Map; - return FlutterPage( - withContainer: pigeonMap['withContainer'] as bool?, - pageName: pigeonMap['pageName'] as String?, - uniqueId: pigeonMap['uniqueId'] as String?, - arguments: (pigeonMap['arguments'] as Map?)?.cast(), - ); - } -} - -class _NativeRouterApiCodec extends StandardMessageCodec { - const _NativeRouterApiCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is CommonParams) { - buffer.putUint8(128); - writeValue(buffer, value.encode()); - } else - if (value is FlutterContainer) { - buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else - if (value is FlutterPage) { - buffer.putUint8(130); - writeValue(buffer, value.encode()); - } else - if (value is StackInfo) { - buffer.putUint8(131); - writeValue(buffer, value.encode()); - } else -{ - super.writeValue(buffer, value); - } - } - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 128: - return CommonParams.decode(readValue(buffer)!); - - case 129: - return FlutterContainer.decode(readValue(buffer)!); - - case 130: - return FlutterPage.decode(readValue(buffer)!); - - case 131: - return StackInfo.decode(readValue(buffer)!); - - default: - return super.readValueOfType(type, buffer); - - } - } -} - -class NativeRouterApi { - /// Constructor for [NativeRouterApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - NativeRouterApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = _NativeRouterApiCodec(); - - Future pushNativeRoute(CommonParams arg_param) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NativeRouterApi.pushNativeRoute', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_param]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } - - Future pushFlutterRoute(CommonParams arg_param) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NativeRouterApi.pushFlutterRoute', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_param]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } - - Future popRoute(CommonParams arg_param) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NativeRouterApi.popRoute', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_param]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } - - Future getStackFromHost() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NativeRouterApi.getStackFromHost', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else if (replyMap['result'] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyMap['result'] as StackInfo?)!; - } - } - - Future saveStackToHost(StackInfo arg_stack) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NativeRouterApi.saveStackToHost', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_stack]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } - - Future sendEventToNative(CommonParams arg_params) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NativeRouterApi.sendEventToNative', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_params]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } -} - -class _FlutterRouterApiCodec extends StandardMessageCodec { - const _FlutterRouterApiCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is CommonParams) { - buffer.putUint8(128); - writeValue(buffer, value.encode()); - } else -{ - super.writeValue(buffer, value); - } - } - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 128: - return CommonParams.decode(readValue(buffer)!); - - default: - return super.readValueOfType(type, buffer); - - } - } -} -abstract class FlutterRouterApi { - static const MessageCodec codec = _FlutterRouterApiCodec(); - - void pushRoute(CommonParams param); - void popRoute(CommonParams param); - void removeRoute(CommonParams param); - void onForeground(CommonParams param); - void onBackground(CommonParams param); - void onNativeResult(CommonParams param); - void onContainerShow(CommonParams param); - void onContainerHide(CommonParams param); - void sendEventToFlutter(CommonParams param); - void onBackPressed(); - static void setup(FlutterRouterApi? api, {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.pushRoute', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.pushRoute was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.pushRoute was null, expected non-null CommonParams.'); - api.pushRoute(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.popRoute', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.popRoute was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.popRoute was null, expected non-null CommonParams.'); - api.popRoute(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.removeRoute', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.removeRoute was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.removeRoute was null, expected non-null CommonParams.'); - api.removeRoute(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.onForeground', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onForeground was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onForeground was null, expected non-null CommonParams.'); - api.onForeground(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.onBackground', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onBackground was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onBackground was null, expected non-null CommonParams.'); - api.onBackground(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.onNativeResult', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onNativeResult was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onNativeResult was null, expected non-null CommonParams.'); - api.onNativeResult(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.onContainerShow', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerShow was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerShow was null, expected non-null CommonParams.'); - api.onContainerShow(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.onContainerHide', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerHide was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.onContainerHide was null, expected non-null CommonParams.'); - api.onContainerHide(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter was null.'); - final List args = (message as List?)!; - final CommonParams? arg_param = (args[0] as CommonParams?); - assert(arg_param != null, 'Argument for dev.flutter.pigeon.FlutterRouterApi.sendEventToFlutter was null, expected non-null CommonParams.'); - api.sendEventToFlutter(arg_param!); - return; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterRouterApi.onBackPressed', codec, binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - // ignore message - api.onBackPressed(); - return; - }); - } - } - } -} +// Re-export messages from platform interface +export 'package:flutter_boost_platform_interface/flutter_boost_platform_interface.dart' + show + CommonParams, + StackInfo, + FlutterContainer, + FlutterPage, + NativeRouterApi, + FlutterRouterApi; diff --git a/pubspec.yaml b/pubspec.yaml index 3ff2f2d58..2e04740a7 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,12 @@ dependencies: flutter: sdk: flutter collection: ^1.16.0 + flutter_boost_platform_interface: + path: flutter_boost_platform_interface + flutter_boost_android: + path: flutter_boost_android + flutter_boost_ios: + path: flutter_boost_ios dev_dependencies: flutter_lints: ^2.0.1 @@ -23,7 +29,6 @@ flutter: plugin: platforms: android: - package: com.idlefish.flutterboost - pluginClass: FlutterBoostPlugin + default_package: flutter_boost_android ios: - pluginClass: FlutterBoostPlugin + default_package: flutter_boost_ios