iOS/Flutter 编译问题总结

编译工具

  • Xcode
  • Android Studio 3.2及以上 (强烈建议)

编译环境

Flutter Doctor

AT6028-DLP1:~ zhangmo$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, v1.12.13+hotfix.5, on Mac OS X 10.14.5 18F132,
    locale zh-Hans-CN)
 
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 11.2)
[✓] Android Studio (version 3.2)
[✓] VS Code (version 1.41.0)
[✓] Connected device (1 available)

• No issues found!

添加Flutter页面到车小妹

启动FlutterEngine

现在官方建议我们使用 FlutterEngine 充当 Dart VM 和 Flutter 运行时的主机; FlutterViewController 依附于 FlutterEngine,给 Flutter 传递 UIKit 的输入事件,并展示被 FlutterEngine 渲染的每一帧画面。具体代码如下:

AppDelegate.h

@interface AppDelegate : FlutterAppDelegate // More on the FlutterAppDelegate below.
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end

AppDelegate.m

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  self.flutterEngine = [[FlutterEngine alloc] initWithName:@"CarMaidFlutterEngine"];
  // Runs the default Dart entrypoint with a default Flutter route.
  [self.flutterEngine run];
  [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

以上方式优点:

  • 当展示 FlutterViewController 时,第一帧画面将会更快展现,有效避免白屏;
  • 你的 Flutter 和 Dart 状态将比一个FlutterViewController 存活更久;
  • 在展示 UI 前,你的应用和 plugins 可以与 Flutter 和 Dart 逻辑交互,所以在此处注册一次[GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine]即可,具体Flutter页面无需再次注册。

缺点:
* 此方式没法通过传递routeName来进行分发,进而展示不同的Flutter页面,那应该如何做呢?

原生跳转FlutterViewController

在 上调用,默认将会调用你的文件里的函数。为了能接收不同的原生页面通过方式传递过来的参数,进而在中进行参数解析,分发展示不同的,固需要将函数的状态由改变成,至此通过创建传递路由名称和参数的问题得以解决。

以车小妹开票详情页为例,代码如下:

原生部分

CMOpenBillListViewController.m

@implementation CMOpenBillListViewController

- (void)showFlutterViewController {
  NSDictionary *params = @{@"billingApplyId": [NSNumber numberWithInteger:itemModel.Id],
                   @"routeName": @"billing_apply_detail"
                   };

  FlutterEngine *flutterEngine = ((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine;
  CMFlutterController *flutterViewController = [[CMFlutterController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil params:params];
  [[[AppDelegate sharedAppDelegate] currentNavController] pushViewController:flutterViewController animated:YES];
}

@end

CMFlutterController.m

@implementation CMFlutterController

- (instancetype)initWithEngine:(FlutterEngine *)engine nibName:(nullable NSString *)nibName bundle:(nullable NSBundle *)nibBundle params:(nullable NSDictionary *)params {
    self = [super initWithEngine:engine nibName:nibName bundle:nibBundle];
    if (self) {
        //传值
        NSString *channelName = @"com.che168.carmaid/common";
        FlutterMethodChannel *messageChannel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:[self binaryMessenger]];
        [messageChannel invokeMethod:@"initRoute" arguments:params];
    }
    return self;
}

@end
main.dart

class MainPage extends StatefulWidget {
  @override
  State createState() {
    return MainState();
  }
}

class MainState extends State {
  var arguments;

  @override
  void initState() {
    super.initState();
    arguments = {"routeName": "/"};
    Common.platform.setMethodCallHandler((handler) {
      return Future(() {
        switch (handler.method) {
          case "initRoute":
            print(handler.arguments.toString());
            setState(() {
              arguments = handler.arguments;
            });
            break;
        }
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    print("arguments:${arguments.toString()}");
    String routeName = arguments["routeName"];
    switch (routeName) {
      case "dev":
      //开发者选项页
        return SettingPage();
      case "subscribe_money_detail":
      //认款详情页
        int payinid = arguments['payinid'] as int;
        int remitState = arguments['remitState'] as int;
        int refundState = arguments['refundState'] as int;
        return SubscribeMoneyDetail(payinid, remitState, refundState);
      case "billing_apply_detail":
      //开票申请详情页
        int billingApplyId = arguments['billingApplyId'] as int;
        return BillApplyDetail(billingApplyId);
      default:
      //404
        return _createDefaultPage(routeName);
    }
  }
}

热重载 & 热重启

Xcode运行车小妹主程序,Android Studio打开car_maid_flutter,当主程序运行到手机的时候,在Android Studio->Terminal中输入 flutter attach,命令运行完成后,打开相应的Flutter页面进行修改,press save 或者 press "r" 即可进行热重载。一般代码修改通过热重载就能达到实时更新目的。
如果代码更改会影响应用程序(或其依赖)的状态,则应用程序必须使用的数据可能与它从头开始执行的数据不完全一致。在热重载和完全重启之后,结果可能是不同的行为。 例如,如果您将某个StatelessWidget类改为StatefulWidget(或相反),则在热重载之后,应用程序的以前状态将保留。但是,该状态可能与新的更改不兼容。这次考虑热重启。
Android Studio 很好的兼容和 Xcode 在运行时的状态,不用停止Xcode的运行,即可实现热重载,原生和Flutter能同时断点调试程序,这是推荐 Android Studio 的重要原因之一,同时 Android Studio 有很多优秀的插件,期待大家的发掘,目前推荐 ,直接将json转成bean.dart文件。

问题总结(持续更新)

问题1、原生多次通过上述方案进入同一个flutter页面后,参数没有更新。经测试发现,第二次启动flutter页面后StatefulWidget的createState()方法没有执行导致UI没有刷新?

回答:当多次创建同一个StatefulWidget,只有第一次会执行createState()方法。但是我们会发现,之后会执行State中的didUpdateWidget。通过该方法我们可以拿到当前StatefulWidget对象的属性,然后调用自己的setState(() {参数赋值});达到刷新UI的目的。

问题2、dart中声明的数字类型变量,如果未附默认值,会是0吗?

回答:如果未附默认值,会是空,这点跟原生不太一样,注意。

优秀项目参考

站在巨人的肩膀上成长

  • The Flutter Vignettes
  • alibaba/flutter-go
  • 豆瓣
  • 知乎

参考文献

  • Flutter中文网
  • Android问题总结

你可能感兴趣的:(iOS/Flutter 编译问题总结)