Flutter 升級 1.12 適配教程

戀貓de小郭發表於2019-12-25

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 相關的四個實現方法也提供了相應的操作邏輯。

public class FlutterPluginTestNewPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
  private static MethodChannel channel;

   /// 保留舊版本的相容
  public static void registerWith(Registrar registerWith) {
    Log.e("registerWith", "registerWith");
    channel = new MethodChannel(registerWith.messenger(), "flutter_plugin_test_new");
    channel.setMethodCallHandler(new FlutterPluginTestNewPlugin());
  }

  @Override
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    if (call.method.equals("getPlatformVersion")) {
      Log.e("onMethodCall", call.method);
      result.success("Android " + android.os.Build.VERSION.RELEASE);
      Map<String, String> map = new HashMap<>();
      map.put("message", "message");
      channel.invokeMethod("onMessageTest", map);
    } else {
      result.notImplemented();
    }
  }

//// FlutterPlugin 的兩個 方法
  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    Log.e("onAttachedToEngine", "onAttachedToEngine");
    channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "flutter_plugin_test_new");
    channel.setMethodCallHandler(new FlutterPluginTestNewPlugin());
  }

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    Log.e("onDetachedFromEngine", "onDetachedFromEngine");
  }


  ///activity 生命週期
  @Override
  public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) {
    Log.e("onAttachedToActivity", "onAttachedToActivity");

  }

  @Override
  public void onDetachedFromActivityForConfigChanges() {
    Log.e("onDetachedFromActivityForConfigChanges", "onDetachedFromActivityForConfigChanges");

  }

  @Override
  public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) {
    Log.e("onReattachedToActivityForConfigChanges", "onReattachedToActivityForConfigChanges");
  }

  @Override
  public void onDetachedFromActivity() {
    Log.e("onDetachedFromActivity", "onDetachedFromActivity");
  }
}
複製程式碼

簡單來說就是需要多繼承 FlutterPlugin 介面,然後在 onAttachedToEngine 方法中構建 MethodCallHandler 並且 setMethodCallHandler ,之後同步在保留的 registerWith 方法中實現 onAttachedToEngine 中類似的初始化。

執行後的外掛在正常情況下呼叫的輸入如下所示:

2019-12-19 18:01:31.481 24809-24809/? E/onAttachedToEngine: onAttachedToEngine
2019-12-19 18:01:31.481 24809-24809/? E/onAttachedToActivity: onAttachedToActivity
2019-12-19 18:01:31.830 24809-24809/? E/onMethodCall: getPlatformVersion
2019-12-19 18:05:48.051 24809-24809/com.shuyu.flutter_plugin_test_new_example E/onDetachedFromActivity: onDetachedFromActivity
2019-12-19 18:05:48.052 24809-24809/com.shuyu.flutter_plugin_test_new_example E/onDetachedFromEngine: onDetachedFromEngine
複製程式碼

另外,如果你外掛是想要更好相容模式對於舊版 Flutter Plugin 執行,registerWith 靜態方法其實需要調整為如下程式碼所示:

  public static void registerWith(Registrar registrar) {
    channel = new MethodChannel(registrar.messenger(), "flutter_plugin_test_new");
    channel.startListening(registrar.messenger());
  }
複製程式碼

當然,如果是 Kotlin 外掛,可能會是如下圖所示類似的更改。

Flutter 升級 1.12 適配教程

2、如果條件允許可以修改主專案的 MainActivity 物件,將繼承的 FlutterActivity 從 io.flutter.app.FlutterActivity 替換為 io.flutter.embedding.android.FlutterActivity,之後 外掛就可以自動註冊; 如果條件不允許不繼承 FlutterActivity 的需要自己手動呼叫 GeneratedPluginRegistrant.registerWith 方法 ,當然到此處可能會提示 registerWith 方法呼叫不正確,不要急忽略它往下走。

/// 這個方法如果在下面的 3 中 AndroidManifest.xml 不開啟 flutterEmbedding v2 的配置,就需要手動呼叫
@Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    GeneratedPluginRegistrant.registerWith(flutterEngine);
  }
複製程式碼

如果按照 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 適配教程

這裡還要注意,如上圖所示需要在 application 節點內配置 flutterEmbedding 才能生效新的外掛載入邏輯。

    <meta-data
        android:name="flutterEmbedding"
        android:value="2" />
複製程式碼

4、之後就可以執行 flutter packages get 去生成了新的 GeneratedPluginRegistrant 檔案,如下程式碼所示,新的 FlutterPlugin 將被 flutterEngine.getPlugins().add 直接載入,而舊的外掛實現方法會通過 ShimPluginRegistry 被相容載入到 v2 的實現當中。

@Keep
public final class GeneratedPluginRegistrant {
  public static void registerWith(@NonNull FlutterEngine flutterEngine) {
    ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine);
    flutterEngine.getPlugins().add(new io.flutter.plugins.androidintent.AndroidIntentPlugin());
    flutterEngine.getPlugins().add(new io.flutter.plugins.connectivity.ConnectivityPlugin());
    flutterEngine.getPlugins().add(new io.flutter.plugins.deviceinfo.DeviceInfoPlugin());
      io.github.ponnamkarthik.toast.fluttertoast.FluttertoastPlugin.registerWith(shimPluginRegistry.registrarFor("io.github.ponnamkarthik.toast.fluttertoast.FluttertoastPlugin"));
    flutterEngine.getPlugins().add(new io.flutter.plugins.packageinfo.PackageInfoPlugin());
    flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
      com.baseflow.permissionhandler.PermissionHandlerPlugin.registerWith(shimPluginRegistry.registrarFor("com.baseflow.permissionhandler.PermissionHandlerPlugin"));
    flutterEngine.getPlugins().add(new io.flutter.plugins.share.SharePlugin());
    flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
      com.tekartik.sqflite.SqflitePlugin.registerWith(shimPluginRegistry.registrarFor("com.tekartik.sqflite.SqflitePlugin"));
    flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
    flutterEngine.getPlugins().add(new io.flutter.plugins.webviewflutter.WebViewFlutterPlugin());
  }
}
複製程式碼

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

android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true

複製程式碼

2、需要在忽略檔案增加 .flutter-plugins-dependencies

3、更新之後如果對 iOS 包變大有疑問,可以查閱 #47101 ,這裡已經很好的描述了這段因果關係;另外如果發現 iOS13 真機無法輸入 log 的問題,可以檢視 #41133

Flutter 升級 1.12 適配教程

4、如下圖所示,1.12.x 的升級中 iOS 的 Podfile 檔案也進行了調整,如果還使用舊檔案可能會到相應的警告,相關配置也在下方貼出。

Flutter 升級 1.12 適配教程

# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug' => :debug,
  'Profile' => :release,
  'Release' => :release,
}

def parse_KV_file(file, separator='=')
  file_abs_path = File.expand_path(file)
  if !File.exists? file_abs_path
    return [];
  end
  generated_key_values = {}
  skip_line_start_symbols = ["#", "/"]
  File.foreach(file_abs_path) do |line|
    next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
    plugin = line.split(pattern=separator)
    if plugin.length == 2
      podname = plugin[0].strip()
      path = plugin[1].strip()
      podpath = File.expand_path("#{path}", file_abs_path)
      generated_key_values[podname] = podpath
    else
      puts "Invalid plugin specification: #{line}"
    end
  end
  generated_key_values
end

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  # Flutter Pod

  copied_flutter_dir = File.join(__dir__, 'Flutter')
  copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
  copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
  unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
    # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
    # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
    # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.

    generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
    unless File.exist?(generated_xcode_build_settings_path)
      raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
    end
    generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
    cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];

    unless File.exist?(copied_framework_path)
      FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
    end
    unless File.exist?(copied_podspec_path)
      FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
    end
  end

  # Keep pod path relative so it can be checked into Podfile.lock.
  pod 'Flutter', :path => 'Flutter'

  # Plugin Pods

  # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
  # referring to absolute paths on developers' machines.
  system('rm -rf .symlinks')
  system('mkdir -p .symlinks/plugins')
  plugin_pods = parse_KV_file('../.flutter-plugins')
  plugin_pods.each do |name, path|
    symlink = File.join('.symlinks', 'plugins', name)
    File.symlink(path, symlink)
    pod name, :path => File.join(symlink, 'ios')
  end
end

# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system.
install! 'cocoapods', :disable_input_output_paths => true

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['ENABLE_BITCODE'] = 'NO'
    end
  end
end

複製程式碼

好了,暫時就到這了。

Flutter 文章彙總地址:

Flutter 完整實戰實戰系列文章專欄

Flutter 番外的世界系列文章專欄

資源推薦

Flutter 升級 1.12 適配教程

相關文章