前言
2020年注定是Flutter疯狂的一年,从19年开始,Flutter的热度就一直高居不下,不管是前端开发者还是移动端的开发者,都对这门新的技术产生了浓厚的兴趣。无独有偶,我也在自己尝试了一番之后,决定做一个Flutter相关系列的文章分享。希望通过我的学习记录的方式,带大家一步步的去探索Flutter。 本次demo地址:https://github.com/Spr1ngHall/FlutterDemo
创建并运行项目
首先我们在创建项目之前,为了确保环境是ok的,我们在命令行敲
flutter doctor
用来检察一下环境是否配置ok。
薛立恒@xuelihengdeMacBookPro ~/Desktop flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, v1.5.4-hotfix.2, on Mac OS X 10.14.5 18F132, locale
zh-Hans-CN)
[✓] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
[✓] iOS toolchain - develop for iOS devices (Xcode 10.2.1)
[✓] Android Studio (version 3.4)
[✓] VS Code (version 1.35.1)
[✓] Connected device (1 available)
界面显示这个打印信息才能说明你的所有环境配置都是ok的,如果不ok,就参考我的前一篇文章。
这给打击介绍两种创建项目的方法,一种是命令行的形式,一种是手动。
1、命令行形式创建项目
首先cd到你想要创建项目的路径文件夹
cd /Users/xueliheng/Desktop/Flutter/FlutterDemo
接下来我们运行一句命令
flutter create hello_flutter
那么我们就在相应的文件夹里面能看到我们创建的hello_flutter。这里可能有一些人就要问了,为什么这里创建工程的名字不用大写?这里就要说明一下,Flutter在创建项目的时候,是跟iOS的命名规则不一样的,他们所有的文件夹,包括项目中创建的文件名都不是不能含有大写字母的,如果含有大写字母,会报下面的错误。
接着我们来认识一下创建的项目:
- android和ios就分别是不同的两个工程的工程文件,这个一般不会动,需要动的时候,大概也是的到项目混合开发的阶段,到时候再说吧。
- 然后lib文件就是装.dart为结尾的代码文件的。这里面也就是我们需要写的flutter工程源码。
- 然后test是自动化测试用的。
- 这里的pubspec.lock和pubspec.yaml这两个文件,大家可以直接联想成为iOS里面的podspec和podfile.lock文件,功能很类似。
接下来我们进入到hello_flutter这个Demo里面去:
cd hello_flutter
继续敲:
flutter run
那么项目就会自动打开。这里还是要说明一下,如果你同时开启了两个模拟器,那么这个时候敲击上面这个命令的时候,flutter会报一个错误,会让你选择一个具体的模拟器去运行项目。同时也把模拟器的信息都打印出来了,这个时候你只需要选择一个执行就行了:
flutter run -d 'iPhone Xʀ'
或者是运行到所有的模拟器上面:
flutter run -d all
我们再运行项目之后,模拟器的打印如下:
薛立恒@xuelihengdeMacBookPro ~/Desktop/Flutter/FlutterDemo/hello_flutter flutter run
Launching lib/main.dart on iPhone Xʀ in debug mode...
Running Xcode build...
├─Assembling Flutter resources... 1.3s
└─Compiling, linking and signing... 3.7s
Xcode build done. 6.4s
Syncing files to device iPhone Xʀ... 1,650ms
🔥 To hot reload changes while running, press "r". To hot restart (and rebuild
state), press "R".
An Observatory debugger and profiler on iPhone Xʀ is available at:
http://127.0.0.1:62574/dpTTmbSopM8=/
For a more detailed help message, press "h". To detach, press "d"; to quit,
press "q".
这里有一个提示,让你敲r或者R,其实意思就是如果你需要重新build一下项目,就输入R,如果你敲r的意思就是,启动热重载。热重载是flutter的特色功能,能在不build项目的同时也能看到模拟器上面的东西在变动,所见即所得,这个真是iOS开发的一个福音啊!按q的话就是退出。
2、手动形式创建项目
- 打开Android Studio会发现这里多了一个选项
这里会有下面几个选项
分别来介绍一下选项吧
- Flutter Application:顾名思义就是创建一个
- Flutter的项目,这里不用说肯定选他;
- Flutter Plugin:如果说你开发出来的项目既要用到iOS原生也要用到Android原生,那么这个时候你就要选择给他们开发一个插件;
- Flutter Package:如果说你开发出来的项目是只给Dart语言使用的,那么这个项目就可以创建一个package,其实plugin和package都差不太多,只是创建不同模式而已。
- Flutter Module:这个是混合开发的时候会用到的,这里先不讲,后面研究研究在来说。 点击第一个选项,然后一步步的填写项目名称就ok了,这里太简单就不赘述了。
但是这里是有一个坑的,如果你在创建项目的时候,如果选择了一个中文路径的话,AS会报错,这里AS是不支持在中文路径下面创建项目的,如果你非要在中文路径下面创建项目,就只能用第一种命令行的形式去创建项目了。
编写项目
介绍了这么多前戏,终于来到正题了。删掉main.dart里面所有的代码。我们重头开始。
首先引入基础组件库:
import 'package:flutter/material.dart';
我们可以看作就是UIKit。
创建main函数:
void main() {
runApp(Center(
child: Text(
'Hello',
textDirection: TextDirection.ltr,
),
));
}
这个main函数跟iOS中的main函数其实是一个道理,runApp就相当于UIApplication。Center里面的意思就是,其中的child组件按照居中对齐的方式排列。child当然就好理解了,就是iOS中的subView的意思。所以就可以得出,runApp后面的这一段代码,其实就是在设置一个根控制器。然后设置一下Text组件的一些属性。
什么是Widget
讲到这里,我们都知道了又一个child是指的subView的意思,那么UIView是什么呢?
那么这里,我们就要说到Widget。Widget翻译过来就是小部件的意思。我们可以理解为一个小控件。就像一个UIView一样。 然后Widget分为两种。一种是Stateful(有状态的),一种是Stateless(无状态的)。他们分别有什么用呢?无状态的就表示这个Widget创建出来是什么样子就是什么样子,状态是不可改变的。相反,有状态的其实也是一个特殊的无状态的Widget,但是这个Widget带有一个状态类,去标识这个widget的一些状态。有状态的Widget在渲染的时候,也是渲染成了一个无状态的Widget。
创建一个Widget类
我们现在创建一个MyWidget类,也就是一个widget控件:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return null;
}
}
这里重写了一个build方法,这个方法是干嘛的呢? 实际上,这个方法就是将你现在自定义的这个小控件放到控件的渲染的树中去。这个return返回的是什么,那么这个控件就是什么。他会从你的main函数中的runApp中的第一个控件去渲染,然后逐步的去渲染里面内部的控件。 (tips:这里创建的时候跟前面创建文件名是不一样的,这里创建类名是需要运用驼峰命名法的,并且首字母是大写。这里注意区分一下。) 那么我们现在可以把runApp中的Text控件换成我们的自己自定义的控件了,但是前提是我们要重写一下build方法。
void main() {
runApp(Center(
child: MyWidget(),
));
}
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Center(
child: Text(
'Hello Flutter',
textDirection: TextDirection.ltr,
),
);
}
}
当然,其实我们也可以在main方法中去自定义一个function,然后function返回的是一个组件,这种方式也是可以的,代码如下:
Widget func () {
return Text('Hello');
}
但是我个人觉得,如果说是比较复杂的控件的话,还是定义一个类去封装控件比较好,因为可以把控件分装到不同的文件中,供别人使用。
tips:这里我们发现MyWidget方法返回的也是一个Center (),那么我们其实是可以把runApp中的Center方法省略掉。并且如果一个方法里面,只有一句代码,dart语言是可以简写成如下的:
void main() => runApp(MyWidget());
这个是不是很熟悉,这就是我们刚开始创建项目时,默认的工程里面,main函数的代码就是这样写的。这个在JS ES6里面好像也有。前端同学估计会熟悉一些。 我们点到Text里面去看源码的时候,能看到如下简化代码:
class Text extends StatelessWidget {
const Text(
this.data, {
Key key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
}) : assert(
data != null,
'A non-null String must be provided to a Text widget.',
),
textSpan = null,
super(key: key);
const Text.rich(
this.textSpan, {
Key key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
}) : assert(
textSpan != null,
'A non-null TextSpan must be provided to a Text.rich widget.',
),
data = null,
super(key: key);
final String data;
final TextSpan textSpan;
final TextStyle style;
final StrutStyle strutStyle;
final TextAlign textAlign;
final TextDirection textDirection;
final Locale locale;
final bool softWrap;
...
this后面的很好理解,就是这个类的可选参数,那么下面的final定义的是什么呢?也好理解,就是属性呗。 为什么用final定义呢? 原因是Text是一个Stateless的Widget,那么创建出来之后就是固定了的,属性也是同样的道理。那么这里就肯定是final修饰,而不是var修饰。这个final其实可以类比Swift或者JS里的let。 接下来,我们创建一个_textStyle对象,去设置一些我们需要设置的Style:
final _textStyle = TextStyle(
color: Colors.red,
fontSize: 40.0,
);
然后把这个_textStyle赋值给Text里面的style。
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final _textStyle = TextStyle(
color: Colors.red,
fontSize: 40.0,
);
return Center(
child: Text(
'Hello Flutter',
textDirection: TextDirection.ltr,
style: _textStyle,
),
);
}
}
这种方式同样只是一种技巧,可以把Style里面的东西提取出来。这就跟CSS有一些类似了。
认识MaterialApp
这一次直接上代码吧
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
body: MyWidget(),
),
theme: ThemeData(
primaryColor: Colors.yellow,
),
);
}
}
...
运行结果是这样的:
这里MaterialApp其实上就是Flutter封装的一些便于我们去搭建APP的一系列组件。Scaffold实际上我们可以理解为UINavigationControllre。其中也包含了AppBar,也就是导航条,body就是实际显示在手机中的内容。theme就是一些主题,可以让我们自己去设置导航栏的颜色啊等等东西。这一点上来说比iOS确实是方便了很多。
创建一个Model
我们再创建一个名叫animal.dart的文件,然后敲入如下代码:
class Animal {
// 构造函数
const Animal({
this.name,
this.imageUrl,
});
final String name;
final String imageUrl;
}
这里定义一个Animal的类,const Animal()就是构造函数,下面的final定义的都是属性,在构造函数里面赋值name和imageUrl。这就构成了一个Animal的模型。
创建数据源
我们创建完模型之后,才应该创建一下数据源。我们在animal.dart文件中定义一下模型数组:
//定义一个模型数组
final List<Animal> datas = [
Animal(
name: '兔子',
imageUrl:
'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1561905982563&di=c69bd273942564d09f5eb8ca4eaa1943&imgtype=0&src=http%3A%2F%2Fs15.sinaimg.cn%2Fmw690%2F00328H1Nzy74f5vBmKG8e%26690',
),
Animal(
name: '鸭子',
imageUrl:
'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1561906404967&di=80e4b6c937176ff9a17bcd8bc377de28&imgtype=0&src=http%3A%2F%2Fphotocdn.sohu.com%2F20120305%2FImg336680797.jpg',
),
Animal(
name: '金钱豹',
imageUrl:
'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1561906421196&di=ba764154104591d2f9da67c89d6fd36b&imgtype=0&src=http%3A%2F%2Fphotocdn.sohu.com%2F20130611%2FImg378599972.jpg',
),
Animal(
name: '狮子',
imageUrl:
'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1561906438918&di=e2202a99c9931aa0d76a3e2de25e435b&imgtype=0&src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F615f13c5ff460d568c7b632846a2b04f00cf6509b47e-NhJ9FI_fw658',
),
Animal(
name: '老虎',
imageUrl:
'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1561906452597&di=766795e10f0d9afc7d11c173f08aaf9c&imgtype=0&src=http%3A%2F%2Fimg18.3lian.com%2Fd%2Ffile%2F201710%2F09%2F02b420dddc4db52a75f7cbbaed83644b.jpg',
),
Animal(
name: '袋鼠',
imageUrl:
'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1561906467233&di=c31bd84ae874f2ad767ca0a76287a8eb&imgtype=0&src=http%3A%2F%2Fimages.china.cn%2Fattachement%2Fjpg%2Fsite1000%2F20130319%2F001aa0ba5c7712b1f5005e.jpg',
),
Animal(
name: '大象',
imageUrl:
'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1561906481939&di=16bf1c9ea3c78bc46dff56e30919da53&imgtype=0&src=http%3A%2F%2Fphotocdn.sohu.com%2F20130702%2FImg380495405.jpg',
),
Animal(
name: '公鸡',
imageUrl:
'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=3634424173,2840985996&fm=26&gp=0.jpg',
),
];
按快捷键Option+return快速导入Animal的头文件。
创建ListView
我们创建一个新的类名叫Home,然后在App中将home中的Scaffold替换成新建的类。
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
theme: ThemeData(
primaryColor: Colors.yellow,
),
);
}
}
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
...
接着我们重写Home的build方法,并且返回的是一个Scaffold,然后设置一下标题:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
body: ListView.builder(
itemCount: datas.length,
itemBuilder: _cellForRow,
),
);
}
这里就是创建一个ListView,这里的itemCount很明显,就跟iOS中的numberOfRowsInSection方法是一个道理,意思就是这个ListView有多少行。itemBuilder很显然也就是cellForRowAtIndexPath,既然iOS里面我们用的是代理去实现的,这里我们为了更加的贴心iOS,我们把这里的实现抽离出来:
Widget _cellForRow (BuildContext context, int index) {
return Text('123');
}
我们定义了一个_cellForRow的Widget,这个Widget返回的就是一个row所对应显示的内容。 tips:这里说明一下:
我们在定义一个属性的时候,如果加了前缀“_”,就标识这是一个私有的。外面死不能使用的,如果没有加,那么说明外面是可以使用的。
ListView中是没有section这个概念的,我们在需要需要分组的时候,必须得自己去一行行的实现了(这一点我觉得iOS做的要好很多,当然目前还不好说,后面慢慢来看)。
我们运行一下项目,就能看到如下的显示
在Row中添加视图
我们上面只在row里面添加了一个Text,这在实际开发过程中是远远不够的,那么我们要怎么去添加别的视图在row中呢? 这里就要用到Container了。话不多说,线上代码:
Widget _cellForRow(BuildContext context, int index) {
return Container(
color: Colors.grey[100],
margin: EdgeInsets.all(10),
child: Image.network(datas[index].imageUrl),
);
}
这里的Container就是指的容器,这一点上来说,我们可以类比前端的div,也可以类比iOS中的UIView。包括其中的布局方式也跟FlexBox很类似,这一点我们再下一篇文章中会来针对性的讲一下。Container里面的实现就不用多说了,设置颜色为100度灰、设置外边距统一为10、设置子视图为一张Image并且是网络请求的,请求的url是从datas的数组中取得。
这里如果要多加一个Text到row怎么办呢?这里也可以的:
Widget _cellForRow(BuildContext context, int index) {
return Container(
color: Colors.grey[100],
margin: EdgeInsets.all(10),
child: Column(//这里还有Row可以Stack布局
children: <Widget>[
Image.network(datas[index].imageUrl),
Text(datas[index].name),
],
),
);
}
我们把Image替换成一个children就行了,这个children里面是一个Widget的数组,那么理论上我们就可以无限制的往里面添加Widget了。并且谁最先执行,哪个控件就在最上面。
执行的结果是这样的
除了Column布局之外,还有Row布局和Stack布局,我们分别看看效果
上面是Row布局的,其实就是横向的从左至右的布局方式,这里图片太长了,已经把文字都挤出去了。
上面是Stack布局的,意思就是说把各个控件层叠起来摆放。
如果你现在要在文字和图片之间弄一个间距,我们可以直接加一个SizeBox
SizedBox(
height: 20,
),
把SizeBox也加入到children中去,并且加到文字个图片之间,这样文字跟图片之间就会有间距了。
child: Column(
//这里还有Row可以Stack布局
children: <Widget>[
Image.network(datas[index].imageUrl),
SizedBox(
height: 20,
),
Text(
datas[index].name,
style: TextStyle(
fontWeight: FontWeight.w800,
fontSize: 18.0,
fontStyle: FontStyle.values[1],
color: Colors.blue,
),
),
SizedBox(
height: 20,
),
],
),
那么到这里,我们的简单的项目就算是完成了。接下来我们来简单认识几个常用的Widget。
常用Widget
Text
我们前面介绍了这个控件,那么如果我们想要拼接字符串怎么弄呢?上代码:
class TextDemo extends StatelessWidget {
final TextStyle _textStyle = TextStyle(
fontSize: 16.0,
);
final String _title = '这是一个标题';
final String _detail = '这是一个内容';
@override
Widget build(BuildContext context) {
return Text(
'《${_title}》-- $_detail。最近Flutter已经疯狂的刷屏了各个技术博客、技术网站,完全有一统天下的气势。所以最近也决定开始尝尝鲜,从零开始一步步的来探索Flutter的世界。就从环境搭建开始,记录一下自己探索Flutter的过程。',
textAlign: TextAlign.center,
style: _textStyle,
);
}
}
这里我们看到这个标题是我们拼接到这个字符串上面的,所以说拼接的语法就是:
$_title
//或者
${_title}
在Text中,我们除了可以设置textAlign以外,我们还可以设置maxLines,就是限制最大行数。设置了最大行数之后,如果字数超过了行数,接下来的是不显示的,如图所示
Text(
'《${_title}》-- $_detail。最近Flutter已经疯狂的刷屏了各个技术博客、技术网站,完全有一统天下的气势。所以最近也决定开始尝尝鲜,从零开始一步步的来探索Flutter的世界。就从环境搭建开始,记录一下自己探索Flutter的过程。',
textAlign: TextAlign.center,
style: _textStyle,
maxLines: 3,
overflow: TextOverflow.ellipsis,
);
后面就多了…的符号。
富文本
直接线上代码吧:
RichText(
text: TextSpan(
text: '<这是一个标题>',
style: TextStyle(
fontSize: 30,
color: Colors.blue,
),
children: <TextSpan>[
TextSpan(
text: 'xueliheng500@vip.qq.com',
style: TextStyle(
fontSize: 16,
color: Colors.red,
)
),
TextSpan(
text: '☺',
style: TextStyle(
fontSize: 16,
color: Colors.red,
)
),
TextSpan(
text: 'xueliheng500@vip.qq.com',
style: TextStyle(
fontSize: 16,
color: Colors.red,
)
),
],
),
);
}
最后呈现的效果:
这里可以总结一下:
富文本用的Widget就是RichText;
我们需要添加富文本只需要添加children就可以了;
children是一个TextSpan的数组;
我们可以添加很多的TextSpan,并且自定义相应的TextSpan,来完成富文本的要求。
作者:薛立恒【滴滴出行高级研发开发工程师】