Flutter Interact 除了带来各种新的开发工具之外,最大的亮点莫过于 1.12 稳定版本的发布。

不同于之前的版本,1.12.x 版本Flutter Framework 做了较多的不兼容性升级,例如在 Dart 层: ImageProviderload 增加了 DecoderCallback 参数、TextField’s minimum height 从 40 调整到了 48PageView 开始使用 SliverLayoutBuilder 而弃用 RenderSliverFillViewport 等相关的不兼容升级。

但是上述的问题都不致命,因为只需要调整相关的 Dart 代码便可以直接解决问题,而此次涉及最大的调整,应该是 Android 插件的改进 Android plugins APIs 的相关变化,该调整需要用户重新调整 Flutter 项目中 Android 模块和插件的代码进行适配。

一、Android Plugins

1、介绍

在 Flutter 1.12 开始 Flutter 团队调整了 Android 插件的实现代码,在 1.12 之后 Android 开始使用新的插件 API ,基于的旧的 PluginRegistry.Registrar 不会立即被弃用,但官方建议迁移到基于的新API FlutterPlugin ,另外新版本官方建议插件直接使用 Androidx 支持,官方提供的插件也已经全面升级到 Androidx

与旧的 API 相比,新 API 的优势在于:为插件所依赖的生命周期提供了一套更解耦的使用方法,例如以前 PluginRegistry.Registrar.activity() 在使用时,如果 Flutter 还没有添加到 Activity 上时可能返回 null ,同时插件不知道自己何时被引擎加载使用,而新的 API 上这些问题都得到了优化。

1、升级

在新 API 上 Android 插件需要使用 FlutterPluginMethodCallHandler 进行实现,同时还提供了 ActivityAware 用于 Activity 的生命周期管理和获取,提供 ServiceAware 用于 Service 的生命周期管理和获取,具体迁移步骤为:

1、更新主插件类(*Plugin.java)用于实现 FlutterPlugin, 也就是正常情况下 Android 插件需要继承 FlutterPluginMethodCallHandler 这两个接口,如果需要用到 Activity 有需要继承 ActivityAware 接口。

以前的 Flutter 插件都是直接继承 MethodCallHandler 然后提供 registerWith 静态方法;而升级后如下代码所示,这里还保留了 registerWith 静态方法,是因为还需要针对旧版本做兼容支持,同时新版 API 中 MethodCallHandler 将在 onAttachedToEngine 方法中被初始化和构建,在 onDetachedFromEngine 方法中释放;同时 Activity 相关的四个实现方法也提供了相应的操作逻辑。

  1. public class FlutterPluginTestNewPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
  2. private static MethodChannel channel;
  3. /// 保留旧版本的兼容
  4. public static void registerWith(Registrar registerWith) {
  5. Log.e("registerWith", "registerWith");
  6. channel = new MethodChannel(registerWith.messenger(), "flutter_plugin_test_new");
  7. channel.setMethodCallHandler(new FlutterPluginTestNewPlugin());
  8. }
  9. @Override
  10. public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
  11. if (call.method.equals("getPlatformVersion")) {
  12. Log.e("onMethodCall", call.method);
  13. result.success("Android " + android.os.Build.VERSION.RELEASE);
  14. Map<String, String> map = new HashMap<>();
  15. map.put("message", "message");
  16. channel.invokeMethod("onMessageTest", map);
  17. } else {
  18. result.notImplemented();
  19. }
  20. }
  21. //// FlutterPlugin 的两个 方法
  22. @Override
  23. public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
  24. Log.e("onAttachedToEngine", "onAttachedToEngine");
  25. channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "flutter_plugin_test_new");
  26. channel.setMethodCallHandler(new FlutterPluginTestNewPlugin());
  27. }
  28. @Override
  29. public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
  30. Log.e("onDetachedFromEngine", "onDetachedFromEngine");
  31. }
  32. ///activity 生命周期
  33. @Override
  34. public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) {
  35. Log.e("onAttachedToActivity", "onAttachedToActivity");
  36. }
  37. @Override
  38. public void onDetachedFromActivityForConfigChanges() {
  39. Log.e("onDetachedFromActivityForConfigChanges", "onDetachedFromActivityForConfigChanges");
  40. }
  41. @Override
  42. public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) {
  43. Log.e("onReattachedToActivityForConfigChanges", "onReattachedToActivityForConfigChanges");
  44. }
  45. @Override
  46. public void onDetachedFromActivity() {
  47. Log.e("onDetachedFromActivity", "onDetachedFromActivity");
  48. }
  49. }

简单来说就是需要多继承 FlutterPlugin 接口,然后在 onAttachedToEngine 方法中构建 MethodCallHandler 并且 setMethodCallHandler ,之后同步在保留的 registerWith 方法中实现 onAttachedToEngine 中类似的初始化。

运行后的插件在正常情况下调用的输入如下所示:

  1. 2019-12-19 18:01:31.481 24809-24809/? E/onAttachedToEngine: onAttachedToEngine
  2. 2019-12-19 18:01:31.481 24809-24809/? E/onAttachedToActivity: onAttachedToActivity
  3. 2019-12-19 18:01:31.830 24809-24809/? E/onMethodCall: getPlatformVersion
  4. 2019-12-19 18:05:48.051 24809-24809/com.shuyu.flutter_plugin_test_new_example E/onDetachedFromActivity: onDetachedFromActivity
  5. 2019-12-19 18:05:48.052 24809-24809/com.shuyu.flutter_plugin_test_new_example E/onDetachedFromEngine: onDetachedFromEngine

另外,如果你插件是想要更好兼容模式对于旧版 Flutter Plugin 运行,registerWith 静态方法其实需要调整为如下代码所示:

  1. public static void registerWith(Registrar registrar) {
  2. channel = new MethodChannel(registrar.messenger(), "flutter_plugin_test_new");
  3. channel.startListening(registrar.messenger());
  4. }

当然,如果是 Kotlin 插件,可能会是如下图所示类似的更改。

Flutter 升级 1.12 适配教程 - 图1

2、如果条件允许可以修改主项目的 MainActivity 对象,将继承的 FlutterActivity 从 io.flutter.app.FlutterActivity 替换为 io.flutter.embedding.android.FlutterActivity,之后 插件就可以自动注册; 如果条件不允许不继承 FlutterActivity 的需要自己手动调用 GeneratedPluginRegistrant.registerWith 方法 ,当然到此处可能会提示 registerWith 方法调用不正确,不要急忽略它往下走。

  1. /// 这个方法如果在下面的 3 中 AndroidManifest.xml 不打开 flutterEmbedding v2 的配置,就需要手动调用
  2. @Override
  3. public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
  4. GeneratedPluginRegistrant.registerWith(flutterEngine);
  5. }

如果按照 3 中一样打开了 v2 ,那么生成的 GeneratedPluginRegistrant 就是使用 FlutterEngine ,不配置 v2 使用的就是 PluginRegistry 。

3、之后还需要调整 AndroidManifest.xml 文件,如下图所示,需要将原本的 io.flutter.app.android.SplashScreenUntilFirstFrame 这个 meta-data 移除,然后增加为 io.flutter.embedding.android.SplashScreenDrawableio.flutter.embedding.android.NormalTheme 这两个 meta-data ,主要是用于应用打开时的占位图样式和进入应用后的主题样式。

Flutter 升级 1.12 适配教程 - 图2

这里还要注意,如上图所示需要在 application 节点内配置 flutterEmbedding 才能生效新的插件加载逻辑。

  1. <meta-data
  2. android:name="flutterEmbedding"
  3. android:value="2" />

4、之后就可以执行 flutter packages get 去生成了新的 GeneratedPluginRegistrant 文件,如下代码所示,新的 FlutterPlugin 将被 flutterEngine.getPlugins().add 直接加载,而旧的插件实现方法会通过 ShimPluginRegistry 被兼容加载到 v2 的实现当中。

  1. @Keep
  2. public final class GeneratedPluginRegistrant {
  3. public static void registerWith(@NonNull FlutterEngine flutterEngine) {
  4. ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine);
  5. flutterEngine.getPlugins().add(new io.flutter.plugins.androidintent.AndroidIntentPlugin());
  6. flutterEngine.getPlugins().add(new io.flutter.plugins.connectivity.ConnectivityPlugin());
  7. flutterEngine.getPlugins().add(new io.flutter.plugins.deviceinfo.DeviceInfoPlugin());
  8. io.github.ponnamkarthik.toast.fluttertoast.FluttertoastPlugin.registerWith(shimPluginRegistry.registrarFor("io.github.ponnamkarthik.toast.fluttertoast.FluttertoastPlugin"));
  9. flutterEngine.getPlugins().add(new io.flutter.plugins.packageinfo.PackageInfoPlugin());
  10. flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
  11. com.baseflow.permissionhandler.PermissionHandlerPlugin.registerWith(shimPluginRegistry.registrarFor("com.baseflow.permissionhandler.PermissionHandlerPlugin"));
  12. flutterEngine.getPlugins().add(new io.flutter.plugins.share.SharePlugin());
  13. flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
  14. com.tekartik.sqflite.SqflitePlugin.registerWith(shimPluginRegistry.registrarFor("com.tekartik.sqflite.SqflitePlugin"));
  15. flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
  16. flutterEngine.getPlugins().add(new io.flutter.plugins.webviewflutter.WebViewFlutterPlugin());
  17. }
  18. }

5、最后是可选升级,在 android/gradle/wrapper 下的 gradle-wrapper.properties 文件,可以将 distributionUrl 修改为 gradle-5.6.2-all.zip 的版本,同时需要将 android/ 目录下的 build.gradle 文件的 gradle 也修改为 com.android.tools.build:gradle:3.5.0 ; 另外 kotlin 插件版本也可以升级到 ext.kotlin_version = '1.3.50'

二、其他升级

1、如果之前的项目还没有启用 Androidx ,那么可以在 android/ 目录下的 gradle.properties 添加如下代码打开 Androidx

  1. android.enableR8=true
  2. android.useAndroidX=true
  3. android.enableJetifier=true

2、需要在忽略文件增加 .flutter-plugins-dependencies

3、更新之后如果对 iOS 包变大有疑问,可以查阅 #47101 ,这里已经很好的描述了这段因果关系;另外如果发现 iOS13 真机无法输入 log 的问题,可以查看 #41133

Flutter 升级 1.12 适配教程 - 图3

4、如下图所示,1.12.x 的升级中 iOS 的 Podfile 文件也进行了调整,如果还使用旧文件可能会到相应的警告,相关配置也在下方贴出。

Flutter 升级 1.12 适配教程 - 图4

  1. # Uncomment this line to define a global platform for your project
  2. # platform :ios, '9.0'
  3. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
  4. ENV['COCOAPODS_DISABLE_STATS'] = 'true'
  5. project 'Runner', {
  6. 'Debug' => :debug,
  7. 'Profile' => :release,
  8. 'Release' => :release,
  9. }
  10. def parse_KV_file(file, separator='=')
  11. file_abs_path = File.expand_path(file)
  12. if !File.exists? file_abs_path
  13. return [];
  14. end
  15. generated_key_values = {}
  16. skip_line_start_symbols = ["#", "/"]
  17. File.foreach(file_abs_path) do |line|
  18. next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
  19. plugin = line.split(pattern=separator)
  20. if plugin.length == 2
  21. podname = plugin[0].strip()
  22. path = plugin[1].strip()
  23. podpath = File.expand_path("#{path}", file_abs_path)
  24. generated_key_values[podname] = podpath
  25. else
  26. puts "Invalid plugin specification: #{line}"
  27. end
  28. end
  29. generated_key_values
  30. end
  31. target 'Runner' do
  32. use_frameworks!
  33. use_modular_headers!
  34. # Flutter Pod
  35. copied_flutter_dir = File.join(__dir__, 'Flutter')
  36. copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
  37. copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
  38. unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
  39. # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
  40. # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
  41. # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
  42. generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
  43. unless File.exist?(generated_xcode_build_settings_path)
  44. raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
  45. end
  46. generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
  47. cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
  48. unless File.exist?(copied_framework_path)
  49. FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
  50. end
  51. unless File.exist?(copied_podspec_path)
  52. FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
  53. end
  54. end
  55. # Keep pod path relative so it can be checked into Podfile.lock.
  56. pod 'Flutter', :path => 'Flutter'
  57. # Plugin Pods
  58. # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
  59. # referring to absolute paths on developers' machines.
  60. system('rm -rf .symlinks')
  61. system('mkdir -p .symlinks/plugins')
  62. plugin_pods = parse_KV_file('../.flutter-plugins')
  63. plugin_pods.each do |name, path|
  64. symlink = File.join('.symlinks', 'plugins', name)
  65. File.symlink(path, symlink)
  66. pod name, :path => File.join(symlink, 'ios')
  67. end
  68. end
  69. # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system.
  70. install! 'cocoapods', :disable_input_output_paths => true
  71. post_install do |installer|
  72. installer.pods_project.targets.each do |target|
  73. target.build_configurations.each do |config|
  74. config.build_settings['ENABLE_BITCODE'] = 'NO'
  75. end
  76. end
  77. end

好了,暂时就到这了。

Flutter 文章汇总地址:

Flutter 完整实战实战系列文章专栏

Flutter 番外的世界系列文章专栏

资源推荐

Flutter 升级 1.12 适配教程 - 图5