Flutter - iOS 混合开发初探

作者: Lcr111 | 来源:发表于2022-01-04 14:53 被阅读0次

    前言

    熟悉flutter开发的人都知道,Flutter和iOS端原生混合开发的方式有两种:

    • Flutter调用原生的部分功能(Flutter为主项目)
    • Flutter作为一个模块嵌入到原生的项目中(iOS为主项目)

    今天我们简单介绍下两种情况的简单使用。

    Flutter调用原生的部分功能

    就拿之前敲的微信的项目的我的界面来说,切换头像功能,在MinePageState中添加以下代码:

      late File _avatarFile = File('');
      final MethodChannel _methodChannel = MethodChannel('mine_page/method');
    
      @override
      void initState() {
        // TODO: implement initState
        super.initState();
        _methodChannel.setMethodCallHandler((call) {
          if (call.method == 'imagePath') {
            print(call.arguments.toString());
            String imagePath = call.arguments.toString();
            setState(() {
              _avatarFile = File(imagePath);
            });
          }
          return Future(() {});
        });
      }
    

    给头像添加一个点击事件:

    GestureDetector(
              onTap: () {
                      print('点击了头像');
                      _methodChannel.invokeMapMethod('picture');
                    },
                    child: Container(
                      width: 70,
                      height: 70,
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(12),
                        image: DecorationImage(
                            image: _avatarFile.path == ''
                                ? const AssetImage('images/tabbar_mine_hl.png')
                                    as ImageProvider
                                : FileImage(_avatarFile),
                            fit: BoxFit.cover),
                      ),
                    ),
                  ),
                      
    

    找到项目中的ios文件,打开Runner.xcworkspace,添加以下代码:

    import UIKit
    import Flutter
    
    @UIApplicationMain
    @objc class AppDelegate: FlutterAppDelegate,UIImagePickerControllerDelegate,UINavigationControllerDelegate {
        
    
        var methodChannel:FlutterMethodChannel?
      override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
      ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
          let vc = self.window.rootViewController as! FlutterViewController
          self.methodChannel = FlutterMethodChannel.init(name: "mine_page/method", binaryMessenger: vc.binaryMessenger)
          let imageVc = UIImagePickerController.init()
          imageVc.delegate = self
          self.methodChannel!.setMethodCallHandler { (call , result) in
              if(call.method == "picture"){
                  vc.present(imageVc, animated: true, completion: nil)
              }
          }
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
      }
        
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            picker.dismiss(animated: true, completion: nil)
            if #available(iOS 11.0, *) {
                let url = info[UIImagePickerController.InfoKey.imageURL] as! NSURL
                let imagePath = url.path;
                self.methodChannel!.invokeMethod("imagePath", arguments: imagePath)
            } else {
                // Fallback on earlier versions
            }
        }
    }
    
    

    这样就实现了头像切换的功能了,通过MethodChannel来实现。
    就像h5和原生交互一样,MethodChannel 所对应的name(标识mine_page/method)要一一对应,不然就找不到要处理的事件,以及相互传参时候的标识也要对应,picture(Flutter需要原生去干的标识), imagePath(iOS需要传给Flutter的标识)。

    Flutter作为一个模块嵌入到原生的项目中

    此时创建Flutter就为module类型的,取名dededemo,项目创建完成,继续在dededemo项目同层级下创建原生项目NativeDemo

    交互项目

    接下来利用Cocoapods来将flutter_module作为一个插件集成到原生项目中。创建PodFile文件:

    platform :ios, '10.0'
    
    # flutter module 文件路径
    flutter_application_path = '../dededemo' 
    load File.join(flutter_application_path, '.IOS', 'Flutter', 'podhelper.rb')
    
    target 'NativeDemo' do
      install_all_flutter_pods(flutter_application_path)
      use_frameworks!
    
    end
    

    pod install,集成结束之后,打开原生项目,查看Pods文件

    集成flutter_module

    编译原生项目,发现并不报错。
    接着在Main中添加两个按钮:

    添加按钮
    分别在ViewController中添加相应的点击事件,代码如下:
    #import "ViewController.h"
    #import <Flutter/Flutter.h>
    
    @interface ViewController ()
    
    @property (nonatomic, strong) FlutterEngine *flutterEngine;
    @property (nonatomic, strong) FlutterViewController *flutterVc;
    @property (nonatomic, strong) FlutterBasicMessageChannel *msgChannel;
    
    @end
    
    @implementation ViewController
    
    -(FlutterEngine *)flutterEngine{
        if (!_flutterEngine) {
            FlutterEngine *engine = [[FlutterEngine alloc]initWithName:@"lcr"];
            if (engine.run) {
                _flutterEngine = engine;
            }
        }
        return _flutterEngine;
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.flutterVc = [[FlutterViewController alloc]initWithEngine:self.flutterEngine nibName:nil bundle:nil];
        
        self.msgChannel = [FlutterBasicMessageChannel messageChannelWithName:@"messageChannel" binaryMessenger:self.flutterVc.binaryMessenger];
        
        [self.msgChannel setMessageHandler:^(id  _Nullable message, FlutterReply  _Nonnull callback) {
            NSLog(@"收到Flutter的:%@",message);
        }];
        
    }
    - (IBAction)pushFlutter:(id)sender {
        
        self.flutterVc.modalPresentationStyle = UIModalPresentationFullScreen;
        
        FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:@"one_page" binaryMessenger:self.flutterVc.binaryMessenger];
        
        [methodChannel invokeMethod:@"one" arguments:nil];
        
        [self presentViewController:self.flutterVc animated:YES completion:nil];
        
        [methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
            if ([call.method isEqualToString:@"exit"]) {
                [self.flutterVc dismissViewControllerAnimated:YES completion:nil];
            }
        }];
        
    }
    
    - (IBAction)pushFlutter2:(id)sender {
        
        self.flutterVc.modalPresentationStyle = UIModalPresentationFullScreen;
        
        FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:@"two_page" binaryMessenger:self.flutterVc.binaryMessenger];
        
        [methodChannel invokeMethod:@"two" arguments:nil];
        
        [self presentViewController:self.flutterVc animated:YES completion:nil];
        
        [methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
            if ([call.method isEqualToString:@"exit"]) {
                [self.flutterVc dismissViewControllerAnimated:YES completion:nil];
            }
        }];
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        static int a = 0;
        [self.msgChannel sendMessage:[NSString stringWithFormat:@"%d",a++]];
    }
    
    @end
    

    原生项目中添加完代码,Flutter中也需要添加相应的响应代码。
    main.dart文件中输入如下代码:

    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatefulWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      _MyAppState createState() => _MyAppState();
    }
    
    class _MyAppState extends State<MyApp> {
      final MethodChannel _oneChannel = const MethodChannel('one_page');
      final MethodChannel _twoChannel = const MethodChannel('two_page');
      final BasicMessageChannel _messageChannel =
          const BasicMessageChannel('messgaeChannel', StandardMessageCodec());
    
      String pageIndex = 'one';
    
      @override
      void initState() {
        // TODO: implement initState
        super.initState();
        _oneChannel.setMethodCallHandler((call) {
          pageIndex = 'one';
          print(call.method);
          setState(() {});
          return Future(() {});
        });
    
        _twoChannel.setMethodCallHandler((call) {
          pageIndex = 'two';
          print(call.method);
          setState(() {});
          return Future(() {});
        });
    
        _messageChannel.setMessageHandler((message) {
          print('收到来自iOS的$message');
          return Future(() {});
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primaryColor: Colors.blue,
          ),
          home: _rootPage(pageIndex),
        );
      }
    
      Widget _rootPage(String pageIndex) {
        switch (pageIndex) {
          case 'one':
            return Scaffold(
              appBar: AppBar(
                title: Text(pageIndex),
              ),
              body: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                      onPressed: () {
                        _oneChannel.invokeListMethod('exit');
                      },
                      child: Text(pageIndex)),
                  TextField(
                    onChanged: (String str) {
                      print(str);
                      _messageChannel.send(str);
                    },
                  )
                ],
              ),
            );
          case 'two':
            return Scaffold(
                appBar: AppBar(
                  title: Text(pageIndex),
                ),
                body: Center(
                  child: ElevatedButton(
                      onPressed: () {
                        _twoChannel.invokeListMethod('exit');
                      },
                      child: Text(pageIndex)),
                ));
          default:
            return Scaffold(
              appBar: AppBar(
                title: Text(pageIndex),
              ),
              body: Center(
                child: ElevatedButton(
                  onPressed: () {
                    MethodChannel('default_page').invokeListMethod('exit');
                  },
                  child: Text(pageIndex),
                ),
              ),
            );
        }
      }
    }
    

    如上代码,利用两个MethodChannel实例,分别对应一个原生需要Flutter处理的事件(one_pagetwo_page),_MyAppState 中所做对应的处理就是,当_oneChannel唤起时改变pageIndex,生成界面one,当_twoChannel被唤起时对应的重新渲染,生成界面two,两个界面中分别添加一个按钮,可以在原生点击按钮时候可以退出界面。BasicMessageChannel 可以实现原生和Flutter之间的数据信息传输。

    注意
    按以上方式运行,会以下错误:

    Failed to find assets path for "Frameworks/App.framework/flutter_assets"
    Engine run configuration was invalid
    Could not launch engine with configuration.

    解决方法就是在项目中添加下面脚本:

    "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
    "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed ${SOURCE_ROOT}/Flutter/App.framework
    
    添加脚本

    其中还需确认项目中Build Settings 中的User-Defined 中是否有FLUTTER_ROOT,如果没有的话,需要添加,这个也就是你电脑里flutter安装的位置。我的是装在家目录下:

    FLUTTER_ROOT配置

    但是,这个报错并不是一定会发生的,如果发生了,也可以多次pod install ,多尝试一下,说不定下次运行就不报错了。怪怪~~

    这样就能正常交互了,也就不报错了。

    相关文章

      网友评论

        本文标题:Flutter - iOS 混合开发初探

        本文链接:https://www.haomeiwen.com/subject/erpqcrtx.html