-
Notifications
You must be signed in to change notification settings - Fork 1
在Dart层兼容iOS和Android平台的特定实现
依托于与Skia的深度定制及优化,Flutter给我们提供了很多关于渲染的控制和支持,能够实现绝对的跨平台应用层渲染一致性。但对于一个应用而言,除了应用层视觉显示和对应的交互逻辑处理之外,有时还需要原生操作系统(Android、iOS)提供的底层能力支持。比如,我们前面提到的数据持久化,以及推送、摄像头硬件调用等。
由于Flutter只接管了应用渲染层,因此这些系统底层能力是无法在Flutter框架内提供支持的;而另一方面,Flutter还是一个相对年轻的生态,因此原生开发中一些相对成熟的Java、C++或Objective-C代码库,比如图片处理、音视频编解码等,可能在Flutter中还没有相关实现。
因此,为了解决调用原生系统底层能力以及相关代码库复用问题,Flutter为开发者提供了一个轻量级的解决方案,即逻辑层的方法通道(Method Channel)机制。基于方法通道,我们可以将原生代码所拥有的能力,以接口形式暴露给Dart,从而实现Dart代码与原生代码的交互,就像调用了一个普通的Dart API一样。
接下来,我就与你详细讲述Flutter的方法通道机制吧。
Flutter作为一个跨平台框架,提供了一套标准化的解决方案,为开发者屏蔽了操作系统的差异。但,Flutter毕竟不是操作系统,因此在某些特定场景下(比如推送、蓝牙、摄像头硬件调用时),也需要具备直接访问系统底层原生代码的能力。为此,Flutter提供了一套灵活而轻量级的机制来实现Dart和原生代码之间的通信,即方法调用的消息传递机制,而方法通道则是用来传递通信消息的信道。
一次典型的方法调用过程类似网络调用,由作为客户端的Flutter,通过方法通道向作为服务端的原生代码宿主发送方法调用请求,原生代码宿主在监听到方法调用的消息后,调用平台相关的API来处理Flutter发起的请求,最后将处理完毕的结果通过方法通道回发至Flutter。调用过程如下图所示:
图1 方法通道示意图
从上图中可以看到,方法调用请求的处理和响应,在Android中是通过FlutterView,而在iOS中则是通过FlutterViewController进行注册的。FlutterView与FlutterViewController为Flutter应用提供了一个画板,使得构建于Skia之上的Flutter通过绘制即可实现整个应用所需的视觉效果。因此,它们不仅是Flutter应用的容器,同时也是Flutter应用的入口,自然也是注册方法调用请求最合适的地方。
接下来,我通过一个例子来演示如何使用方法通道实现与原生代码的交互。
在实际业务中,提示用户跳转到应用市场(iOS为App Store、Android则为各类手机应用市场)去评分是一个高频需求,考虑到Flutter并未提供这样的接口,而跳转方式在Android和iOS上各不相同,因此我们需要分别在Android和iOS上实现这样的功能,并暴露给Dart相关的接口。
我们先来看看作为客户端的Flutter,怎样实现一次方法调用请求。
首先,我们需要确定一个唯一的字符串标识符,来构造一个命名通道;然后,在这个通道之上,Flutter通过指定方法名“openAppMarket”来发起一次方法调用请求。
可以看到,这和我们平时调用一个Dart对象的方法完全一样。因为方法调用过程是异步的,所以我们需要使用非阻塞(或者注册回调)来等待原生代码给予响应。
//声明MethodChannel
const platform = MethodChannel('samples.chenhang/utils');
//处理按钮点击
handleButtonClick() async{
int result;
//异常捕获
try {
//异步等待方法通道的调用结果
result = await platform.invokeMethod('openAppMarket');
}
catch (e) {
result = -1;
}
print("Result:$result");
}
需要注意的是,与网络调用类似,方法调用请求有可能会失败(比如,Flutter发起了原生代码不支持的API调用,或是调用过程出错等),因此我们需要把发起方法调用请求的语句用try-catch包装起来。
调用方的实现搞定了,接下来就需要在原生代码宿主中完成方法调用的响应实现了。由于我们需要适配Android和iOS两个平台,所以我们分别需要在两个平台上完成对应的接口实现。
首先,我们来看看Android端的实现方式。在上一小结最后我提到,在Android平台,方法调用的处理和响应是在Flutter应用的入口,也就是在MainActivity中的FlutterView里实现的,因此我们需要打开Flutter的Android宿主App,找到MainActivity.java文件,并在其中添加相关的逻辑。
调用方与响应方都是通过命名通道进行信息交互的,所以我们需要在onCreate方法中,创建一个与调用方Flutter所使用的通道名称一样的MethodChannel,并在其中设置方法处理回调,响应openAppMarket方法,打开应用市场的Intent。同样地,考虑到打开应用市场的过程可能会出错,我们也需要增加try-catch来捕获可能的异常:
protected void onCreate(Bundle savedInstanceState) {
...
//创建与调用方标识符一样的方法通道
new MethodChannel(getFlutterView(), "samples.chenhang/utils").setMethodCallHandler(
//设置方法处理回调
new MethodCallHandler() {
//响应方法请求
@Override
public void onMethodCall(MethodCall call, Result result) {
//判断方法名是否支持
if(call.method.equals("openAppMarket")) {
try {
//应用市场URI
Uri uri = Uri.parse("market://details?id=com.hangchen.example.flutter_module_page.host");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//打开应用市场
activity.startActivity(intent);
//返回处理结果
result.success(0);
} catch (Exception e) {
//打开应用市场出现异常
result.error("UNAVAILABLE", "没有安装应用市场", null);
}
}else {
//方法名暂不支持
result.notImplemented();
}
}
});
}
现在,方法调用响应的Android部分已经搞定,接下来我们来看一下iOS端的方法调用响应如何实现。
在iOS平台,方法调用的处理和响应是在Flutter应用的入口,也就是在Applegate中的rootViewController(即FlutterViewController)里实现的,因此我们需要打开Flutter的iOS宿主App,找到AppDelegate.m文件,并添加相关逻辑。
与Android注册方法调用响应类似,我们需要在didFinishLaunchingWithOptions:方法中,创建一个与调用方Flutter所使用的通道名称一样的MethodChannel,并在其中设置方法处理回调,响应openAppMarket方法,通过URL打开应用市场:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//创建命名方法通道
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"samples.chenhang/utils" binaryMessenger:(FlutterViewController *)self.window.rootViewController];
//往方法通道注册方法调用处理回调
[channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
//方法名称一致
if ([@"openAppMarket" isEqualToString:call.method]) {
//打开App Store(本例打开微信的URL)
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"itms-apps://itunes.apple.com/xy/app/foo/id414478124"]];
//返回方法处理结果
result(@0);
} else {
//找不到被调用的方法
result(FlutterMethodNotImplemented);
}
}];
...
}
这样,iOS端的方法调用响应也已经实现了。
接下来,我们就可以在Flutter应用里,通过调用openAppMarket方法,实现打开不同操作系统提供的应用市场功能了。
需要注意的是,在原生代码处理完毕后将处理结果返回给Flutter时,我们在Dart、Android和iOS分别用了三种数据类型:Android端返回的是java.lang.Integer、iOS端返回的是NSNumber、Dart端接收到返回结果时又变成了int类型。这是为什么呢?
这是因为在使用方法通道进行方法调用时,由于涉及到跨系统数据交互,Flutter会使用StandardMessageCodec对通道中传输的信息进行类似JSON的二进制序列化,以标准化数据传输行为。这样在我们发送或者接收数据时,这些数据就会根据各自系统预定的规则自动进行序列化和反序列化。看到这里,你是不是对这样类似网络调用的方法通道技术有了更深刻的印象呢。
对于上面提到的例子,类型为java.lang.Integer或NSNumber的返回值,先是被序列化成了一段二进制格式的数据在通道中传输,然后当该数据传递到Flutter后,又被反序列化成了Dart语言中的int类型的数据。
关于Android、iOS和Dart平台间的常见数据类型转换,我总结成了下面一张表格,帮助你理解与记忆。你只要记住,像null、布尔、整型、字符串、数组和字典这些基本类型,是可以在各个平台之间以平台定义的规则去混用的,就可以了。
图2 Android、iOS和Dart平台间的常见数据类型转换
方法通道解决了逻辑层的原生能力复用问题,使得Flutter能够通过轻量级的异步方法调用,实现与原生代码的交互。一次典型的调用过程由Flutter发起方法调用请求开始,请求经由唯一标识符指定的方法通道到达原生代码宿主,而原生代码宿主则通过注册对应方法实现、响应并处理调用请求,最后将执行结果通过消息通道,回传至Flutter。
需要注意的是,方法通道是非线程安全的。这意味着原生代码与Flutter之间所有接口调用必须发生在主线程。Flutter是单线程模型,因此自然可以确保方法调用请求是发生在主线程(Isolate)的;而原生代码在处理方法调用请求时,如果涉及到异步或非主线程切换,需要确保回调过程是在原生系统的UI线程(也就是Android和iOS的主线程)中执行的,否则应用可能会出现奇怪的Bug,甚至是Crash。
别急,在下结论之前,我们先按照四象限分析法,把能力和渲染分解成四个维度,分析构建一个复杂App都需要什么。
图1 四象限分析法
经过分析,我们终于发现,原来构建一个App需要覆盖那么多的知识点,通过Flutter和方法通道只能搞定应用层渲染、应用层能力和底层能力,对于那些涉及到底层渲染,比如浏览器、相机、地图,以及原生自定义视图的场景,自己在Flutter上重新开发一套显然不太现实。
在这种情况下,使用混合视图看起来是一个不错的选择。我们可以在Flutter的Widget树中提前预留一块空白区域,在Flutter的画板中(即FlutterView与FlutterViewController)嵌入一个与空白区域完全匹配的原生视图,就可以实现想要的视觉效果了。
但是,采用这种方案极其不优雅,因为嵌入的原生视图并不在Flutter的渲染层级中,需要同时在Flutter侧与原生侧做大量的适配工作,才能实现正常的用户交互体验。
幸运的是,Flutter提供了一个平台视图(Platform View)的概念。它提供了一种方法,允许开发者在Flutter里面嵌入原生系统(Android和iOS)的视图,并加入到Flutter的渲染树中,实现与Flutter一致的交互体验。
这样一来,通过平台视图,我们就可以将一个原生控件包装成Flutter控件,嵌入到Flutter页面中,就像使用一个普通的Widget一样。
接下来,我就与你详细讲述如何使用平台视图。
如果说方法通道解决的是原生能力逻辑复用问题,那么平台视图解决的就是原生视图复用问题。Flutter提供了一种轻量级的方法,让我们可以创建原生(Android和iOS)的视图,通过一些简单的Dart层接口封装之后,就可以将它插入Widget树中,实现原生视图与Flutter视图的混用。
一次典型的平台视图使用过程与方法通道类似:
- 首先,由作为客户端的Flutter,通过向原生视图的Flutter封装类(在iOS和Android平台分别是UIKitView和AndroidView)传入视图标识符,用于发起原生视图的创建请求;
- 然后,原生代码侧将对应原生视图的创建交给平台视图工厂(PlatformViewFactory)实现;
- 最后,在原生代码侧将视图标识符与平台视图工厂进行关联注册,让Flutter发起的视图创建请求可以直接找到对应的视图创建工厂。
至此,我们就可以像使用Widget那样,使用原生视图了。整个流程,如下图所示:
图2 平台视图示例
接下来,我以一个具体的案例,也就是将一个红色的原生视图内嵌到Flutter中,与你演示如何使用平台视图。这部分内容主要包括两部分:
- 作为调用发起方的Flutter,如何实现原生视图的接口调用?
- 如何在原生(Android和iOS)系统实现接口?
接下来,我将分别与你讲述这两个问题。
在下面的代码中,我们在SampleView的内部,分别使用了原生Android、iOS视图的封装类AndroidView和UIkitView,并传入了一个唯一标识符,用于和原生视图建立关联:
class SampleView extends StatelessWidget {
@override
Widget build(BuildContext context) {
//使用Android平台的AndroidView,传入唯一标识符sampleView
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(viewType: 'sampleView');
} else {
//使用iOS平台的UIKitView,传入唯一标识符sampleView
return UiKitView(viewType: 'sampleView');
}
}
}
可以看到,平台视图在Flutter侧的使用方式比较简单,与普通Widget并无明显区别。而关于普通Widget的使用方式,你可以参考第12、13篇的相关内容进行复习。
调用方的实现搞定了。接下来,我们需要在原生代码中完成视图创建的封装,建立相关的绑定关系。同样的,由于需要同时适配Android和iOS平台,我们需要分别在两个系统上完成对应的接口实现。
首先,我们来看看Android端的实现。在下面的代码中,我们分别创建了平台视图工厂和原生视图封装类,并通过视图工厂的create方法,将它们关联起来:
//视图工厂类
class SampleViewFactory extends PlatformViewFactory {
private final BinaryMessenger messenger;
//初始化方法
public SampleViewFactory(BinaryMessenger msger) {
super(StandardMessageCodec.INSTANCE);
messenger = msger;
}
//创建原生视图封装类,完成关联
@Override
public PlatformView create(Context context, int id, Object obj) {
return new SimpleViewControl(context, id, messenger);
}
}
//原生视图封装类
class SimpleViewControl implements PlatformView {
private final View view;//缓存原生视图
//初始化方法,提前创建好视图
public SimpleViewControl(Context context, int id, BinaryMessenger messenger) {
view = new View(context);
view.setBackgroundColor(Color.rgb(255, 0, 0));
}
//返回原生视图
@Override
public View getView() {
return view;
}
//原生视图销毁回调
@Override
public void dispose() {
}
}
将原生视图封装类与原生视图工厂完成关联后,接下来就需要将Flutter侧的调用与视图工厂绑定起来了。与上一篇文章讲述的方法通道类似,我们仍然需要在MainActivity中进行绑定操作:
protected void onCreate(Bundle savedInstanceState) {
...
Registrar registrar = registrarFor("samples.chenhang/native_views");//生成注册类
SampleViewFactory playerViewFactory = new SampleViewFactory(registrar.messenger());//生成视图工厂
registrar.platformViewRegistry().registerViewFactory("sampleView", playerViewFactory);//注册视图工厂
}
完成绑定之后,平台视图调用响应的Android部分就搞定了。
接下来,我们再来看看iOS端的实现。
与Android类似,我们同样需要分别创建平台视图工厂和原生视图封装类,并通过视图工厂的create方法,将它们关联起来:
//平台视图工厂
@interface SampleViewFactory : NSObject<FlutterPlatformViewFactory>
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messager;
@end
@implementation SampleViewFactory{
NSObject<FlutterBinaryMessenger>*_messenger;
}
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger> *)messager{
self = [super init];
if (self) {
_messenger = messager;
}
return self;
}
-(NSObject<FlutterMessageCodec> *)createArgsCodec{
return [FlutterStandardMessageCodec sharedInstance];
}
//创建原生视图封装实例
-(NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args{
SampleViewControl *activity = [[SampleViewControl alloc] initWithWithFrame:frame viewIdentifier:viewId arguments:args binaryMessenger:_messenger];
return activity;
}
@end
//平台视图封装类
@interface SampleViewControl : NSObject<FlutterPlatformView>
- (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;
@end
@implementation SampleViewControl{
UIView * _templcateView;
}
//创建原生视图
- (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger{
if ([super init]) {
_templcateView = [[UIView alloc] init];
_templcateView.backgroundColor = [UIColor redColor];
}
return self;
}
-(UIView *)view{
return _templcateView;
}
@end
然后,我们同样需要把原生视图的创建与Flutter侧的调用关联起来,才可以在Flutter侧找到原生视图的实现:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSObject<FlutterPluginRegistrar>* registrar = [self registrarForPlugin:@"samples.chenhang/native_views"];//生成注册类
SampleViewFactory* viewFactory = [[SampleViewFactory alloc] initWithMessenger:registrar.messenger];//生成视图工厂
[registrar registerViewFactory:viewFactory withId:@"sampleView"];//注册视图工厂
...
}
需要注意的是,在iOS平台上,Flutter内嵌UIKitView目前还处于技术预览状态,因此我们还需要在Info.plist文件中增加一项配置,把内嵌原生视图的功能开关设置为true,才能打开这个隐藏功能:
<dict>
...
<key>io.flutter.embedded_views_preview</key>
<true/>
....
</dict>
经过上面的封装与绑定,Android端与iOS端的平台视图功能都已经实现了。接下来,我们就可以在Flutter应用里,像使用普通Widget一样,去内嵌原生视图了:
Scaffold(
backgroundColor: Colors.yellowAccent,
body: Container(width: 200, height:200,
child: SampleView(controller: controller)
));
如下所示,我们分别在iOS和Android平台的Flutter应用上,内嵌了一个红色的原生视图:
图3 内嵌原生视图示例
在上面的例子中,我们将原生视图封装在一个StatelessWidget中,可以有效应对静态展示的场景。如果我们需要在程序运行时动态调整原生视图的样式,又该如何处理呢?
与基于声明式的Flutter Widget,每次变化只能以数据驱动其视图销毁重建不同,原生视图是基于命令式的,可以精确地控制视图展示样式。因此,我们可以在原生视图的封装类中,将其持有的修改视图实例相关的接口,以方法通道的方式暴露给Flutter,让Flutter也可以拥有动态调整视图视觉样式的能力。
接下来,我以一个具体的案例来演示如何在程序运行时动态调整内嵌原生视图的背景颜色。
在这个案例中,我们会用到原生视图的一个初始化属性,即onPlatformViewCreated:原生视图会在其创建完成后,以回调的形式通知视图id,因此我们可以在这个时候注册方法通道,让后续的视图修改请求通过这条通道传递给原生视图。
由于我们在底层直接持有了原生视图的实例,因此理论上可以直接在这个原生视图的Flutter封装类上提供视图修改方法,而不管它到底是StatelessWidget还是StatefulWidget。但为了遵照Flutter的Widget设计理念,我们还是决定将视图展示与视图控制分离,即:将原生视图封装为一个StatefulWidget专门用于展示,通过其controller初始化参数,在运行期修改原生视图的展示效果。如下所示:
//原生视图控制器
class NativeViewController {
MethodChannel _channel;
//原生视图完成创建后,通过id生成唯一方法通道
onCreate(int id) {
_channel = MethodChannel('samples.chenhang/native_views_$id');
}
//调用原生视图方法,改变背景颜色
Future<void> changeBackgroundColor() async {
return _channel.invokeMethod('changeBackgroundColor');
}
}
//原生视图Flutter侧封装,继承自StatefulWidget
class SampleView extends StatefulWidget {
const SampleView({
Key key,
this.controller,
}) : super(key: key);
//持有视图控制器
final NativeViewController controller;
@override
State<StatefulWidget> createState() => _SampleViewState();
}
class _SampleViewState extends State<SampleView> {
//根据平台确定返回何种平台视图
@override
Widget build(BuildContext context) {
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'sampleView',
//原生视图创建完成后,通过onPlatformViewCreated产生回调
onPlatformViewCreated: _onPlatformViewCreated,
);
} else {
return UiKitView(viewType: 'sampleView',
//原生视图创建完成后,通过onPlatformViewCreated产生回调
onPlatformViewCreated: _onPlatformViewCreated
);
}
}
//原生视图创建完成后,调用control的onCreate方法,传入view id
_onPlatformViewCreated(int id) {
if (widget.controller == null) {
return;
}
widget.controller.onCreate(id);
}
}
Flutter的调用方实现搞定了,接下来我们分别看看Android和iOS端的实现。
程序的整体结构与之前并无不同,只是在进行原生视图初始化时,我们需要完成方法通道的注册和相关事件的处理;在响应方法调用消息时,我们需要判断方法名,如果完全匹配,则修改视图背景,否则返回异常。
Android端接口实现代码如下所示:
class SimpleViewControl implements PlatformView, MethodCallHandler {
private final MethodChannel methodChannel;
...
public SimpleViewControl(Context context, int id, BinaryMessenger messenger) {
...
//用view id注册方法通道
methodChannel = new MethodChannel(messenger, "samples.chenhang/native_views_" + id);
//设置方法通道回调
methodChannel.setMethodCallHandler(this);
}
//处理方法调用消息
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
//如果方法名完全匹配
if (methodCall.method.equals("changeBackgroundColor")) {
//修改视图背景,返回成功
view.setBackgroundColor(Color.rgb(0, 0, 255));
result.success(0);
}else {
//调用方发起了一个不支持的API调用
result.notImplemented();
}
}
...
}
iOS端接口实现代码:
@implementation SampleViewControl{
...
FlutterMethodChannel* _channel;
}
- (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger{
if ([super init]) {
...
//使用view id完成方法通道的创建
_channel = [FlutterMethodChannel methodChannelWithName:[NSString stringWithFormat:@"samples.chenhang/native_views_%lld", viewId] binaryMessenger:messenger];
//设置方法通道的处理回调
__weak __typeof__(self) weakSelf = self;
[_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
[weakSelf onMethodCall:call result:result];
}];
}
return self;
}
//响应方法调用消息
- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
//如果方法名完全匹配
if ([[call method] isEqualToString:@"changeBackgroundColor"]) {
//修改视图背景色,返回成功
_templcateView.backgroundColor = [UIColor blueColor];
result(@0);
} else {
//调用方发起了一个不支持的API调用
result(FlutterMethodNotImplemented);
}
}
...
@end
通过注册方法通道,以及暴露的changeBackgroundColor接口,Android端与iOS端修改平台视图背景颜色的功能都已经实现了。接下来,我们就可以在Flutter应用运行期间,修改原生视图展示样式了:
class DefaultState extends State<DefaultPage> {
NativeViewController controller;
@override
void initState() {
controller = NativeViewController();//初始化原生View控制器
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
...
//内嵌原生View
body: Container(width: 200, height:200,
child: SampleView(controller: controller)
),
//设置点击行为:改变视图颜色
floatingActionButton: FloatingActionButton(onPressed: ()=>controller.changeBackgroundColor())
);
}
}
运行一下,效果如下所示:
图4 动态修改原生视图样式
平台视图解决了原生渲染能力的复用问题,使得Flutter能够通过轻量级的代码封装,把原生视图组装成一个Flutter控件。
Flutter提供了平台视图工厂和视图标识符两个概念,因此Dart层发起的视图创建请求可以通过标识符直接找到对应的视图创建工厂,从而实现原生视图与Flutter视图的融合复用。对于需要在运行期动态调用原生视图接口的需求,我们可以在原生视图的封装类中注册方法通道,实现精确控制原生视图展示的效果。
需要注意的是,由于Flutter与原生渲染方式完全不同,因此转换不同的渲染数据会有较大的性能开销。如果在一个界面上同时实例化多个原生控件,就会对性能造成非常大的影响,所以我们要避免在使用Flutter控件也能实现的情况下去使用内嵌平台视图。
因为这样做,一方面需要分别在Android和iOS端写大量的适配桥接代码,违背了跨平台技术的本意,也增加了后续的维护成本;另一方面毕竟除去地图、WebView、相机等涉及底层方案的特殊情况外,大部分原生代码能够实现的UI效果,完全可以用Flutter实现。