Flutter 开发 (1)iOS 下超详细集成 Flutter

iOS 客户端接入 Flutter 实践

官方混编文档

https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps#ios

目录

  • 介绍
  • 搭建 Flutter-iOS 开发环境
  • iOS现有项目接入flutter
  • 改造iOS工程
  • 运行进行测试
  • 相关文档

背景

  • 本篇文章是系列文章,会涉及到Flutter初步了解,Flutter组件化混编方案,Flutter实战开发,Flutter Native 交互等系列文章。
  • 本篇 主要是 整理了目前如何集成Flutter的步骤和实践,还属于很初级的阶段,让大家了解下什么是Flutter,Flutter是如何集成的。

一、介绍

Flutter是一款移动应用程序SDK,一份代码可以同时生成iOS和Android两个高性能、高保真的应用程序。

Flutter目标是使开发人员能够交付在不同平台上都感觉自然流畅的高性能应用程序。

目前使用Flutter的APP并不算很多,相关资料并不丰富,介绍现有工程引入Flutter的相关文章也比较少。

Flutter架构

二、搭建 Flutter-iOS 开发环境

1. 获取 Flutter 工程

2. 配置 Flutter 环境变量

(1)说明

  • 由于在国内访问Flutter有时可能会受到限制,Flutter官方为中国开发者搭建了临时镜像,可以把镜像地址添加到环境变量中。

  • 为了方便后续使用,需要将项目根目录下bin路径加入环境变量PATH中,打开~/.bash_profile文件,修改环境变量即可。

(2)添加环境变量(确保路径指向没问题)

  • 执行命令 open ~/.bash_profile 在底部添加环境变量。
export PATH=$HOME/flutter/bin:$PATH
export FLUTTER_ROOT=$HOME/flutter

export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
  • 然后生效环境变量,终端 执行 source ~/.bash_profile

(3)注意

  • 如果你使用的是zsh,终端启动时 ~/.bash_profile 将不会被加载,解决办法就是修改 ~/.zshrc ,在其中添加:source ~/.bash_profile,执行命令 open ~/.zshrc,底部添加如下:
source ~/.bash_profile

3. 配置基本环境依赖

brew install --HEAD libimobiledevice
brew install ideviceinstaller ios-deploy cocoapods

4. flutter doctor 检测本机环境

1. 说明
  • 因为flutter依赖的东西比较多,如果我们想要保证flutter环境没问题,需要执行 flutter doctor 检测确保当前环境。
  • 在终端中执行 flutter doctor 命令,如下图:
2. flutter doctor 检查失败原因
  • flutter doctor 检测失败的原因会有很多,例如以下
    • 没有安装 Android Studio。
    • Android Studio 配置有问题。
    • Android Studio 没有安装Flutter插件。
    • 没有安装Xcode,或Xcode版本过低。
    • 没有安装CocoaPods
    • 没有安装 libimobiledevice
    • 没有安装 ideviceinstaller
    • 没有安装 ios-deploy
  • 一步一步按照提示进行修复问题
    • 安装或修改需要的地方,直到 flutter doctor 没有错误提示为止。
3. 安卓SDK相关环境变量设置
  • 这是作者本机的环境变量,如果遇到问题,可对比一下区别。
  # android sdk目录,替换为你自己的即可。
  export ANDROID_HOME="/Users/用户名/Documents/android_sdk" 
  export PATH=${PATH}:${ANDROID_HOME}/tools
  export PATH=${PATH}:${ANDROID_HOME}/platform-tools

三、iOS现有项目接入flutter

(1)说明
  • Flutter的工程结构比较特殊,由Flutter目录再分别包含Native工程的目录(即 iOS 和Android 两个目录)组成。
  • 默认情况下,引入了 Flutter 的 Native 工程无法脱离父目录进行独立构建和运行,因为它会反向依赖于 Flutter 相关的库和资源。
  • 如果已经现有工程,那么我们需要在同级目录创建flutter模块。
(2)创建Flutter模块
  • 假设当前工程是 Flutter_iOS ,那么 cd到项目同级目录,执行flutter命令创建。
cd /Users/sen/Desktop/Flutter工程/Flutter_iOS
flutter create -t module flutter_library
(3)创建iOS项目的 Config 文件
  • Config文件(管理Xcode工程的配置衔接文件) 里面包含分别创建 Flutter.xcconfigDebug.xcconfigRelease.xcconfig 三个配置文件。
  • 其中 Flutter.xcconfig 是指向外目录 flutter module 的 Generated.xcconfig 文件路径引用文件,其他两个代表Xcode的环境配置文件。
(4)Config 文件 内容
  • Flutter.xcconfig 内容
#include "../flutter_library/.ios/Flutter/Generated.xcconfig"
ENABLE_BITCODE=NO
  • Debug.xcconfig 内容 (对应的名字换成自己)
#include "Flutter.xcconfig"

// 如果使用了Cocoapods,那么需要引入 cocoapods 的config文件,因为如果自定义了config,那么cocoapods 的 config 就不会自动指定了。
#include "Pods/Target Support Files/Pods-Flutter_iOS/Pods-Flutter_iOS.debug.xcconfig"
  • Release.xcconfig 内容(对应的名字换成自己)
#include "Flutter.xcconfig"
FLUTTER_BUILD_MODE=release

// 如果使用了Cocoapods,那么需要引入 cocoapods 的config文件,因为如果自定义了config,那么cocoapods 的 config 就不会自动指定了。
#include "Pods/Target Support Files/Pods-Flutter_iOS/Pods-Flutter_iOS.release.xcconfig"
(4)项目中指定使用 config
  • 指定 config 文件,Debug 对应 Debug,Release 对应 Release
(5)设置 Flutter 的脚本
  • 在 Run Script 中增加:
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
(6)修改Flutter脚本
  • 默认自己的Xcode Run Script编译好的framework并不在项目中,而在你创建flutter module文件夹下。
  • 代码中有判断,进行生成的目录,需要注释代码让其生成在当前项目目录。
  • 终端执行命令
    • open $FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh
  • 注释代码


  local derived_dir="${SOURCE_ROOT}/Flutter"
#  if [[ -e "${project_path}/.ios" ]]; then
#    derived_dir="${project_path}/.ios/Flutter"
#  fi
  RunCommand mkdir -p -- "$derived_dir"
  AssertExists "$derived_dir"
  • 配置好,Cmd+B,Build工程编译后,会生成Flutter 编译产物在项目目录下。


(7)引入Flutter编译产物
  • 把编译产物,拖入项目中


  • 注意:flutter_assets 并不能使用 Create groups 的方式添加,只能使用 Creat folder references的方式添加进Xcode项目内,否则跳转flutter会页面渲染失败(页面空白)。需要先删除引用。

  • 然后 文件夹再Add Files to 'xxx',选择Creat folder references

  • 最终如下图


  • 然后还需要添加文件夹下的两个framework添加到Embeded Binaries里。


四、改造iOS工程

(1)AppDelegate.h 改造
  • 使其继承 FlutterAppDelegate 。
  • 删除 @property (strong, nonatomic) UIWindow *window; ,因为集成的delegate里面已经有了。
#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>

@interface AppDelegate : FlutterAppDelegate <UIApplicationDelegate, FlutterAppLifeCycleProvider>

@end
(2)AppDelegate.m 改造
  • 改造AppDelegate.m,转发代理消息。
  • 把使用到的代理,都改为以下方式,使用_lifeCycleDelegate调用传递一次。
#import "AppDelegate.h"

@interface AppDelegate ()
    
@end

@implementation AppDelegate
{
  FlutterPluginAppLifeCycleDelegate *_lifeCycleDelegate;
}
    
- (instancetype)init {
    if (self = [super init]) {
        _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
    }
    return self;
}
    
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
    return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}
    
- (void)applicationDidEnterBackground:(UIApplication*)application {
    [_lifeCycleDelegate applicationDidEnterBackground:application];
}
    
- (void)applicationWillEnterForeground:(UIApplication*)application {
    [_lifeCycleDelegate applicationWillEnterForeground:application];
}
    
- (void)applicationWillResignActive:(UIApplication*)application {
    [_lifeCycleDelegate applicationWillResignActive:application];
}
    
- (void)applicationDidBecomeActive:(UIApplication*)application {
    [_lifeCycleDelegate applicationDidBecomeActive:application];
}
    
- (void)applicationWillTerminate:(UIApplication*)application {
    [_lifeCycleDelegate applicationWillTerminate:application];
}
    
- (void)application:(UIApplication*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
    [_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}
    
- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
    [_lifeCycleDelegate application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
    
- (void)application:(UIApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    [_lifeCycleDelegate application:application
       didReceiveRemoteNotification:userInfo
             fetchCompletionHandler:completionHandler];
}
    
- (BOOL)application:(UIApplication*)application
            openURL:(NSURL*)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
    return [_lifeCycleDelegate application:application openURL:url options:options];
}
    
- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
    return [_lifeCycleDelegate application:application handleOpenURL:url];
}
    
- (BOOL)application:(UIApplication*)application
            openURL:(NSURL*)url
  sourceApplication:(NSString*)sourceApplication
         annotation:(id)annotation {
    return [_lifeCycleDelegate application:application
                                   openURL:url
                         sourceApplication:sourceApplication
                                annotation:annotation];
}
    
- (void)application:(UIApplication*)application
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
  completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {
    [_lifeCycleDelegate application:application
       performActionForShortcutItem:shortcutItem
                  completionHandler:completionHandler];
}
    
- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(nonnull NSString*)identifier
  completionHandler:(nonnull void (^)(void))completionHandler {
    [_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
                  completionHandler:completionHandler];
}
    
- (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}
    
- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
    [_lifeCycleDelegate addDelegate:delegate];
}

#pragma mark - Flutter
    // Returns the key window's rootViewController, if it's a FlutterViewController.
    // Otherwise, returns nil.
- (FlutterViewController*)rootFlutterViewController {
    UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
    if ([viewController isKindOfClass:[FlutterViewController class]]) {
        return (FlutterViewController*)viewController;
    }
    return nil;
}
    
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    [super touchesBegan:touches withEvent:event];
    
    // Pass status bar taps to key window Flutter rootViewController.
    if (self.rootFlutterViewController != nil) {
        [self.rootFlutterViewController handleStatusBarTouches:event];
    }
}
@end
(3)主工程调用Flutter 进行测试
#import "ViewController.h"
#import <Flutter/FlutterViewController.h>
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    FlutterViewController* flutterViewController = [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
    flutterViewController.navigationItem.title = @"Flutter Demo";
    
    [self presentViewController:flutterViewController animated:YES completion:nil];
}
@end

四、运行进行测试

(1)使用 Android Studio 打开 Flutter 模块
  • 选择main.dart,flutter代码主文件,在终端中进行 flutter attact 等待连接。


(2)运行iOS工程。
  • flutter attact后,改变flutter代码,然后输入R 可进行刷新重载。


Demo 地址

https://github.com/bigsen/Flutter_iOS

注意
  • 如果编译不过,可以先cd到 flutter_library 下 执行 flutter build ios
下一篇:

Flutter 开发 (2)优雅的 Flutter 组件化 混编方案

五、相关文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容