Flutter Interact
除了帶來各類新的開發工具以外,最大的亮點莫過於 1.12 穩定版本的發佈。java
不一樣於以前的版本,1.12.x 版本對 Flutter Framework
作了較多的不兼容性升級,例如在 Dart 層: ImageProvider
的 load
增長了 DecoderCallback
參數、TextField's minimum height 從 40 調整到了 48 、PageView 開始使用 SliverLayoutBuilder 而棄用 RenderSliverFillViewport 等相關的不兼容升級。android
可是上述的問題都不致命,由於只須要調整相關的 Dart 代碼即可以直接解決問題,而這次涉及最大的調整,應該是 Android 插件的改進 Android plugins APIs 的相關變化,該調整須要用戶從新調整 Flutter 項目中 Android 模塊和插件的代碼進行適配。ios
在 Flutter 1.12 開始 Flutter 團隊調整了 Android 插件的實現代碼,在 1.12 以後 Android 開始使用新的插件 API ,基於的舊的 PluginRegistry.Registrar
不會當即被棄用,但官方建議遷移到基於的新API FlutterPlugin
,另外新版本官方建議插件直接使用 Androidx
支持,官方提供的插件也已經全面升級到 Androidx
。git
與舊的 API 相比,新 API 的優點在於:爲插件所依賴的生命週期提供了一套更解耦的使用方法,例如之前 PluginRegistry.Registrar.activity()
在使用時,若是 Flutter 尚未添加到 Activity
上時可能返回 null
,同時插件不知道本身什麼時候被引擎加載使用,而新的 API 上這些問題都獲得了優化。github
在新 API 上 Android 插件須要使用 FlutterPlugin
和 MethodCallHandler
進行實現,同時還提供了 ActivityAware
用於 Activity
的生命週期管理和獲取,提供 ServiceAware
用於 Service
的生命週期管理和獲取,具體遷移步驟爲:web
一、更新主插件類(*Plugin.java
)用於實現 FlutterPlugin
, 也就是正常狀況下 Android 插件須要繼承 FlutterPlugin
和 MethodCallHandler
這兩個接口,若是須要用到 Activity
有須要繼承 ActivityAware
接口。api
之前的 Flutter 插件都是直接繼承 MethodCallHandler
而後提供 registerWith
靜態方法;而升級後以下代碼所示,這裏還保留了 registerWith
靜態方法,是由於還須要針對舊版本作兼容支持,同時新版 API 中 MethodCallHandler
將在 onAttachedToEngine
方法中被初始化和構建,在 onDetachedFromEngine
方法中釋放;同時 Activity
相關的四個實現方法也提供了相應的操做邏輯。xcode
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
中相似的初始化。bash
運行後的插件在正常狀況下調用的輸入以下所示:app
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 插件,可能會是以下圖所示相似的更改。
二、若是條件容許能夠修改主項目的 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 。
三、以後還須要調整 AndroidManifest.xml
文件,以下圖所示,須要將本來的 io.flutter.app.android.SplashScreenUntilFirstFrame
這個 meta-data
移除,而後增長爲 io.flutter.embedding.android.SplashScreenDrawable
和 io.flutter.embedding.android.NormalTheme
這兩個 meta-data
,主要是用於應用打開時的佔位圖樣式和進入應用後的主題樣式。
這裏還要注意,如上圖所示須要在 application
節點內配置 flutterEmbedding
才能生效新的插件加載邏輯。
<meta-data
android:name="flutterEmbedding"
android:value="2" />
複製代碼
四、以後就能夠執行 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());
}
}
複製代碼
五、最後是可選升級,在 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'
。
一、若是以前的項目尚未啓用 Androidx
,那麼能夠在 android/
目錄下的 gradle.properties
添加以下代碼打開 Androidx
。
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true
複製代碼
二、須要在忽略文件增長 .flutter-plugins-dependencies
。
三、更新以後若是對 iOS 包變大有疑問,能夠查閱 #47101 ,這裏已經很好的描述了這段因果關係;另外若是發現 iOS13 真機沒法輸入 log 的問題,能夠查看 #41133 。
四、以下圖所示,1.12.x 的升級中 iOS 的 Podfile
文件也進行了調整,若是還使用舊文件可能會到相應的警告,相關配置也在下方貼出。
# 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
複製代碼