原生转flutter快速入门

7,975 阅读10分钟

本文是给第一次接触flutter的原生开发iOS/android同学快速入门的攻略,高手请绕路,轻拍哈。

对于原生开发的同学,对于flutter会比较感兴趣,也许会从网上零星获得一些学习资源,但是比较零散,不构成学习路径,可能也会踩一些坑,为了避免少走弯路,又能快速的入门flutter,现将个人的一些实践经历分享一下,供大家批评指正补充。

1. 安装flutter

1.1 先下载flutter编辑器 android studio,最新版本,解压,安装。。。(以下简称AS)

1.2 安装flutter 已iOS为例

 cd ~/development
 unzip ~/Downloads/flutter_macos_v1.9.1+hotfix.2-stable.zip
  • 设置PATH环境变量

先编辑bash_profile文件(默认情况下,macOS Mojave(及更早版本)使用Bash shell,因此编辑$HOME/.bashrc)

$vim ~/.bash_profile

添加以下路径

export PATH="$PATH:`pwd`/flutter/bin"

完整的如下

export PATH="/bin:/usr/bin:/usr/local/bin:$PATH"
export PATH="/Users/boob/Documents/flutter/bin:$PATH"
export PATH="/usr/local/opt/openssl/bin:$PATH"
export PATH="/Users/boob/Documents/depot_tools:$PATH"
export PATH="/Users/boob/Library/Android/sdk/platform-tools:$PATH"
export PATH

编辑完成使用wq退出,即可执行flutter命令了

  • 完成了,可以试试在终端执行 flutter --version看看版本号, which flutter可以查看flutter安装的路径

2. 创建工程并运行

ios的同学默认安装了xcode,没有的话去安装一个吧,安卓同学需要下载android studio,后续开发都是需要用as进行开发和调试的。 为了第一次能直接运行flutter,我们先开一个模拟器,并且把其他真机设备移除,防止后面操作找不到运行目标,或不知道如何选择设备

前提操作 打开模拟器命令

open -a Simulator

检测pod的版本号是否高于1.6.0

pod --version

flutter默认最低支持的pod版本是1.6.0,如果使用到plugin时就会提示版本过低,导致pod失败了

创建工程的命令

flutter create testflutter

注意工程名字都要小写,否则会提示你命名出错。

运行

$ cd myapp
$ flutter run

此时flutter会编译dart代码,并且签名运行

3. flutter产物介绍

我们进入到flutter源码目录

ios目录存放ios工程,android则存放android工程 对于ios来说,编译的产物在ios/Flutter文件夹中 包含了

  • App.framework 这是flutter工程编译出来的ios产物,对于debug编译来说,flutter_assets包含了所有的可执行产物和资源 对于release编译来说,可执行的部分在APP文件中,资源存放在flutter_assets中

  • flutter.framework 俗称flutter engine/flutter 引擎,支持上层flutter运行的底层库。

  • xxx.xcconfig 工程配置,flutter命令自动生成的,为了配置flutter路径,flutterframework的路径

  • flutter_export_environment.sh 1.9新增的脚本,配置flutter常用的环境变量

4. 热重载 hot reload

这是flutter的吹嘘的几大特性之一,跨平台一致性,热重载。。。 即写完代码可以立即执行。 编写flutter代码我们使用android studio,官方推荐3.0以上的版本 developer.android.com/studio

我们可以使用最新的,因为已经使用了最新的flutter插件功能,包括断点调试,attach,性能查看分析等。

用AS打开testflutter工程

找到lib路径,这是我们dart代码存放的路径,flutter项目是用dart语言开发。

现在可以点运行按钮️,直接启动flutter,这个跟在终端启动效果一样。

修改一下源码,把标题改成我的第一个flutter项目,如下

然后按下 cmd+s 保存,即可在模拟器上看到运行结果

5. flutter的默认UI库

默认提供两套ui库,一套是android风格的Material Design 和ios风格的 cupertino (链接是传送门)

下面感受一下差别

我们使用一个button试一下 在main.dart的scaffold的中添加代码

 CupertinoButton(
              child: Text('CLICK ME'),
              color: Theme.of(context).accentColor,
              onPressed: (){
                print("点击了按钮");
              },
              disabledColor: Theme.of(context).disabledColor,
            )

6. 开发思路的转变

到了源码级别,原生的编程思路就需要开始转变了,由于原生开发都是命令式编程,然而前端和flutter是声明式编程的。

对于命令式,是指如果我们要对一个文本内容和文本颜色改变,我们就去取到这个文本的text和textcolor 然后对text和textcolor进行赋值。

然而对于声明式,要改文本内容,需要将文本的内容text和文本的控件分别先声明

所有布局的控件都写到 Widget build(BuildContext context) { ... } 方法中,但是 但是控件需要用到是内容并且可能改变的,则使用一个变量记下来.

  • 声明写法
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

final String title; 就是声明了一个title的字符串。

或者

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
    ...
}
  • 控件写法与使用
AppBar(
        title: Text(widget.title),
      ),
  Text(
      '$_counter',
      style: Theme.of(context).textTheme.display1,
    ),
  • 改变文本内容 setState

这是和原生最大的差别,需要改变文本的内容则需要使用setState中生效,告诉flutter这时候状态变化了,需要重新刷新。

例子中单击+ 数字+1,可以看到界面上的数字立即更新了

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

值得注意的是,state频繁刷新也会带来性能问题,不可滥用哦。 其他代码大家可以自行研究,都是声明式编程的运用。

7. 万物皆widget

widget在ui层面相当于原生的uiview,但是不仅仅局限于显示视图UIView,也有用于布局相关的。

  • 基础 Widgets

Container、Button、Row和Column、Text、Scaffold、Icon、Image、Stack、TabBar+TabBarView、Widget-输入框TextField

  • 用于布局的 Widgets

Align、Center、Expended、LayoutBuilder、Padding、Wrap

  • 可滚动 Widgets

CustomScrollView、GridView、ListView、PageView、SingleChildScrollView

  • 装饰 Widgets

BoxDecoration、Clip系列、Opacity、SafeArea、高斯模糊BackdropFilter

参考: github.com/chenBingX/C…

8. FLEX布局

我们知道横向布局使用Row 纵向布局使用Column Wiget 布局对其方式分为主轴和交叉轴,如果是Row布局主轴mainAxisAlignment就是横向,而其交叉轴就是纵轴, 主轴排列方式有6中,start,end,center,spaceAround,spaceBetween,spaceEvenly

  • spaceBetween 左右item靠边,中间等间距
Padding(
              padding: const EdgeInsets.all(0.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: <Widget>[
                  Container(color: Colors.red, width: 50, height: 50,),
                  Container(color: Colors.blue, width: 50, height: 50,),
                  Container(color: Colors.red, width: 50, height: 50,),
                  Container(color: Colors.blue, width: 50, height: 50,)
                ],
              ),
            )
            

效果如下

  • spaceEvenly 表示所有item间距都相等,包括左右item距离边界
 Padding(
              padding: const EdgeInsets.all(0.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: <Widget>[
                  Container(color: Colors.red, width: 50, height: 50,),
                  Container(color: Colors.blue, width: 50, height: 50,),
                  Container(color: Colors.red, width: 50, height: 50,),
                  Container(color: Colors.blue, width: 50, height: 50,)
                ],
              ),

效果图如下

  • spaceAround 表示将可用空间均匀地放在孩子之间,以及其中一半,第一个和最后一个孩子之前和之后的空间。
Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: <Widget>[
                  Container(color: Colors.red, width: 50, height: 50,),
                  Container(color: Colors.blue, width: 50, height: 50,),
                  Container(color: Colors.red, width: 50, height: 50,),
                  Container(color: Colors.blue, width: 50, height: 50,)
                ],
              ),

效果图如下

start 表示将子级放置在尽可能靠近主轴起点的位置。如果此值沿水平方向使用,则[TextDirection]必须为可用于确定起点是左侧还是右侧。 如果在垂直方向上使用此值,则[VerticalDirection]必须为可用于确定起点是顶部还是底部。

Row(
               mainAxisAlignment: MainAxisAlignment.start,
               crossAxisAlignment: CrossAxisAlignment.center,
               children: <Widget>[
                 Container(color: Colors.red, width: 50, height: 50,),
                 Container(color: Colors.blue, width: 50, height: 50,),
                 Container(color: Colors.red, width: 50, height: 50,),
                 Container(color: Colors.blue, width: 50, height: 50,)
               ],
             ),

效果图如下:

  • center 居中
 Row(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: <Widget>[
                  Container(color: Colors.red, width: 50, height: 50,),
                  Container(color: Colors.blue, width: 50, height: 50,),
                  Container(color: Colors.red, width: 50, height: 50,),
                  Container(color: Colors.blue, width: 50, height: 50,)
                ],
              ),

效果如下

  • end 横向布局则表示靠近主轴的终点,纵向布局则表示靠近纵轴的终点
 Row(
                mainAxisAlignment: MainAxisAlignment.end,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: <Widget>[
                  Container(color: Colors.red, width: 50, height: 50,),
                  Container(color: Colors.blue, width: 50, height: 50,),
                  Container(color: Colors.red, width: 50, height: 50,),
                  Container(color: Colors.blue, width: 50, height: 50,)
                ],
              ),

9 原生工程如何支持flutter

混合工程方案网上有不少,但是真正的核心只有一个,就是将flutter工程的构建产物加入到原生工程中来。

我们已经知道了flutter的构建产物有App.framework 其他的就是Flutter.framework,还有就是dart依赖的第三方库,可能是plugin也可能是dart库,可以在.symbol文件夹中找到(.xxx开头的文件默认是隐藏的,使用shift+cmd+. 将其显示出来)

要想加入到主工程,无法就是将三个东西做成pod的形式,在pod install/update的时候将他pod进来。这样就能做原生开发人员无感知的使用flutter工程。

业界有一个主流的方向,安装产物的本地和远程来划分,本地依赖方式和远程依赖方式。无法还是把产物放哪的问题。

提供另一个视角看混合工程可能更好。我们按照角色划分混合方案,最多三种角色

  • 一、原生开发
  • 二、flutter开发
  • 三、混合开发

这三种角色的诉求分别是,

原生开发无需安装flutter,但是会用到flutter的产物。

其他人员都需要安装flutter,所以需求差不多。 a、安装了flutter的同学,可能也不想编译flutter产物而是直接使用,b、flutter开发的同学可能只想编译debug的产物,但是不想上传,他们想debug构建并attach到 c、对于构建的需求是需要使用最新的代码构建release的包。

使用官方的混合方式无法解决以上所有的需求。自己开发混合工程脚本,需要从以上角度考虑。

然而使用方法仅仅是简单的一句话

flutter_application_path = 'xxxflutter'
load File.join(flutter_application_path,  'IOSFlutterConfig', 'start.rb') 
def GirFlutter     
    puts "自动检测flutter是否存在,自动执行不同的脚本" 
    install_flutter_engine_pod
end

之后pod update即可!

10 混合工程与原生工程通信

分为两个部分,flutter调用native的代码、native调用flutter的功能 官方提供了两种方式 methodchannel、eventchannel。

流程图如下,

method channel

另外plugin就是一种native和flutter通信的最好的例子,我们可以直接看他给的例子。

终端执行

 flutter create --org com.example --template=plugin myplugin

进入myplugin/ios/Classes目录 我们看到 SwiftMypluginPlugin.swift

import Flutter
import UIKit

public class SwiftMypluginPlugin: NSObject, FlutterPlugin {
  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "myplugin", binaryMessenger: registrar.messenger())
    let instance = SwiftMypluginPlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    result("iOS " + UIDevice.current.systemVersion)
  }
}

可以看到这个插件创建了一个名为myplugin的FlutterMethodChannel,并且通过registar注册到了methodcalldelegate里面了。

在dart那边可以使用调起该方法。

class Myplugin {
  static const MethodChannel _channel =
      const MethodChannel('myplugin');

  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

那么对于native如何调用flutter的代码呢 在methodchannel这个例子中,我们看到已经给到了一个result过来了,我们可以拿到这个result result: @escaping FlutterResult,如果有新的事件想发出去,就不断回调这个result也可以。

self.result("第二次回调给flutter")

eventchannel

使用流程类似,先注册-> 发送事件

 FlutterEventChannel *evenChannal =  [FlutterEventChannel eventChannelWithName:@"flutter_hummer_event" binaryMessenger:[registrar messenger]];
    FlutterEventChannelHandel *handle = [FlutterEventChannelHandel new];
    instance.eventHandel = handle;
    [evenChannal setStreamHandler:handle];
    
。。。
 self.eventHandel.eventsBlock(dic);

dart层使用方式

先注册一个通知--> 监听回调

  // 注册一个通知
  static const EventChannel eventChannel =
      const EventChannel('flutter_hummer_event');
    eventChannel
        .receiveBroadcastStream(12345)
        .listen(_onEvent, onError: _onError);
        

  // 回调事件
  void _onEvent(Object event) {
      ...
  }
  
  // 错误返回
  void _onError(Object error) {}

!! 提示,plugin默认生成swift版的plugin,如果想要选择ios可以使用-i objc选项,其他配置选项可以-h查询

 flutter create --org com.example --template=plugin -i objc  myplugin22 

原创声明

欢迎大家批评指正补充,争取做最好最精的入门教程,持续更新中...

原创不易,如需转载请注明来源,共田君