将Flutter Module添加到原生项目中(单个Flutter实例、多个Flutter实例)

662 阅读3分钟

参考Dart中文文档翻译:将 Flutter 集成到现有应用

Github官方Demo:github.com/flutter/sam…


1、创建Flutter ModuleiOSAndroid项目

① 创建文件夹multiple_demo

② 在文件夹multiple_demo下创建flutter Module:flutter create -t module multiple_flutters_module

③ 创建iOS项目MultipleFluttersIos 添加Podfile文件,执行pod install,内容为:

platform :ios, '9.0'

flutter_application_path='../multiple_flutters_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'MultipleFluttersIos' do

use_frameworks!
install_all_flutter_pods(flutter_application_path)

end

④ 创建Android项目

2、单个Flutter实例

iOS项目中,Command + B编译项目,生成FlutterFlutterPluginRegistrant

② 创建单个Flutter实例

//
//  AppDelegate.swift
//  MultipleFluttersIos
//
//  Created by yuanzhiying on 2021/9/6.
//

import UIKit
import Flutter // 1
import FlutterPluginRegistrant // 2

@main
class AppDelegate: FlutterAppDelegate  { // 3 - 替换为FlutterAppDelegate
    lazy var flutterEngine = FlutterEngine(name: "my flutter engine") // 4

    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        flutterEngine.run() // 5
        GeneratedPluginRegistrant.register(with: self.flutterEngine) // 6
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions) // 7
    }

    // MARK: UISceneSession Lifecycle

    override func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    override func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }


}

//
//  ViewController.swift
//  MultipleFluttersIos
//
//  Created by yuanzhiying on 2021/9/6.
//

import UIKit
import Flutter // 1

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // 2
        let button = UIButton(type: UIButton.ButtonType.custom)
        button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside)
        button.setTitle("Show Flutter!", for: UIControl.State.normal)
        button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
        button.backgroundColor = UIColor.blue
        view.addSubview(button)
    }

    @objc func showFlutter() {
        // 3
        let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
        let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
        present(flutterViewController, animated: true, completion: nil)
    }
}

image.png

3、多个Flutter实例

AppDelegate中使用FlutterEngineGroup初始化FlutterEngine

import Flutter // 1
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    let engines = FlutterEngineGroup(name: "nultiple-flutters", project: nil) // 2

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }
}

② 定义Flutter页面入口按钮

import UIKit

class HostViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    @IBAction func gotoSingleFlutterPage(_ sender: Any) {
        let vc = SingleFlutterViewController(withEntrypoint: nil)
        navigationController?.pushViewController(vc, animated: true)
    }

    @IBAction func gotoMultipleFlutterPage(_ sender: Any) {
        let vc = DoubleFlutterViewController()
        navigationController?.pushViewController(vc, animated: true)
    }
}

③ 单Flutter实例页面

import UIKit
import Flutter

class SingleFlutterViewController: FlutterViewController {
    init(withEntrypoint entryPoint: String?) {
        let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate
        let newEngine = appDelegate.engines.makeEngine(withEntrypoint: entryPoint, libraryURI: nil)
        super.init(engine: newEngine, nibName: nil, bundle: nil)
    }
    
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    deinit {
        
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.backgroundColor = .white
    }
    

    /*
    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destination.
        // Pass the selected object to the new view controller.
    }
    */

}

④ 多Flutter实例页面

import UIKit

class DoubleFlutterViewController: UIViewController {
    // topMain bottomMain 对应Flutter中的 vm:entry-point
    private let topFlutter: SingleFlutterViewController = SingleFlutterViewController(withEntrypoint: "topMain")
    private let bottomFlutter: SingleFlutterViewController = SingleFlutterViewController(withEntrypoint: "bottomMain")

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.backgroundColor = .white

        addChild(topFlutter)
        addChild(bottomFlutter)

        let safeFrame = view.safeAreaLayoutGuide.layoutFrame
        let halfHeight = safeFrame.height / 2.0

        topFlutter.view.frame = CGRect(x: safeFrame.minX, y: safeFrame.minY, width: safeFrame.width, height: halfHeight)
        bottomFlutter.view.frame = CGRect(x: safeFrame.minX, y: topFlutter.view.frame.maxY, width: safeFrame.width, height: halfHeight)

        view.addSubview(topFlutter.view)
        view.addSubview(bottomFlutter.view)

        topFlutter.didMove(toParent: self)
        bottomFlutter.didMove(toParent: self)
    }
}

Flutter中定义FlutterViewController所需的EntryPoint

import 'package:flutter/material.dart';

void main() => runApp(MyApp(color: Colors.blue));

@pragma('vm:entry-point')
void topMain() => runApp(MyApp(color: Colors.orange));

@pragma('vm:entry-point')
void bottomMain() => runApp(MyApp(color: Colors.green));

class MyApp extends StatelessWidget {
  // This widget is the root of your application.

  final MaterialColor color;

  const MyApp({Key? key, required this.color}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: color,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}
4、原生与Flutter实例之间的数据传递示例

Flutter侧创建method channel和原生调用方法的回调

// 创建 MethodChannel
_channel = const MethodChannel('multiple-flutters');
// 添加原生调用方法的接收操作
_channel.setMethodCallHandler((call) async {
  if (call.method == "setCount") {
    // A notification that the host platform's data model has been updated.
    setState(() {
      _counter = call.arguments as int?;
    });
  } else {
    throw Exception('not implemented ${call.method}');
  }
});

② Flutter调用原生方法

_channel.invokeMethod<void>("incrementCount", _counter);

③ 使用FlutterEngine创建MethodChannel

private var channel: FlutterMethodChannel?
// 创建FlutterMethodChannel
channel = FlutterMethodChannel(name: "multiple-flutters", binaryMessenger: self.engine!.binaryMessenger)
        
// 创建Flutter调用原生方法的回调
channel?.setMethodCallHandler({ (call: FlutterMethodCall, result: FlutterResult) in
       if call.method == "incrementCount" {
            DataModel.shared.count = DataModel.shared.count + 1
            result(nil)
       }
})
// 原生调用Flutter方法
channel?.invokeMethod("setCount", arguments: DataModel.shared.count)

image.png

image.png

image.png