本文是给第一次接触flutter的原生开发iOS/android同学快速入门的攻略,高手请绕路,轻拍哈。
对于原生开发的同学,对于flutter会比较感兴趣,也许会从网上零星获得一些学习资源,但是比较零散,不构成学习路径,可能也会踩一些坑,为了避免少走弯路,又能快速的入门flutter,现将个人的一些实践经历分享一下,供大家批评指正补充。
1. 安装flutter
1.1 先下载flutter编辑器 android studio,最新版本,解压,安装。。。(以下简称AS)
1.2 安装flutter 已iOS为例
- mac版下载地址 flutter.dev/docs/get-st…
- 解压
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
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
原创声明
欢迎大家批评指正补充,争取做最好最精的入门教程,持续更新中...
原创不易,如需转载请注明来源,共田君