前言
2020年注定是Flutter疯狂的一年,从19年开始,Flutter的热度就一直高居不下,不管是前端开发者还是移动端的开发者,都对这门新的技术产生了浓厚的兴趣。无独有偶,我也在自己尝试了一番之后,决定做一个Flutter相关系列的文章分享。希望通过我的学习记录的方式,带大家一步步的去探索Flutter。
本次demo地址:https://github.com/Spr1ngHall/FlutterDemo
创建并运行项目
首先我们在创建项目之前,为了确保环境是ok的,我们在命令行敲
1 2 |
flutter doctor |
用来检察一下环境是否配置ok。
1 2 3 4 5 6 7 8 9 10 11 |
薛立恒@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到你想要创建项目的路径文件夹
1 2 |
cd /Users/xueliheng/Desktop/Flutter/FlutterDemo |
接下来我们运行一句命令
1 2 |
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里面去:
1 2 |
cd hello_flutter |
继续敲:
1 2 |
flutter run |
那么项目就会自动打开。这里还是要说明一下,如果你同时开启了两个模拟器,那么这个时候敲击上面这个命令的时候,flutter会报一个错误,会让你选择一个具体的模拟器去运行项目。同时也把模拟器的信息都打印出来了,这个时候你只需要选择一个执行就行了:
1 2 |
flutter run -d 'iPhone Xʀ' |
或者是运行到所有的模拟器上面:
1 2 |
flutter run -d all |
我们再运行项目之后,模拟器的打印如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
薛立恒@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
里面所有的代码。我们重头开始。
首先引入基础组件库:
1 2 |
import 'package:flutter/material.dart'; |
我们可以看作就是UIKit
。
创建main函数:
1 2 3 4 5 6 7 8 9 |
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控件,:
1 2 3 4 5 6 7 8 |
class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { // TODO: implement build return null; } } |
这里重写了一个build
方法,这个方法是干嘛的呢?
实际上,这个方法就是将你现在自定义的这个小控件放到控件的渲染的树中去。这个return
返回的是什么,那么这个控件就是什么。他会从你的main
函数中的runApp
中的第一个控件去渲染,然后逐步的去渲染里面内部的控件。
(tips:这里创建的时候跟前面创建文件名是不一样的,这里创建类名是需要运用驼峰命名法的,并且首字母是大写。这里注意区分一下。)
那么我们现在可以把runApp
中的Text
控件换成我们的自己自定义的控件了,但是前提是我们要重写一下build
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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返回的是一个组件,这种方式也是可以的,代码如下:
1 2 3 4 |
Widget func () { return Text('Hello'); } |
但是我个人觉得,如果说是比较复杂的控件的话,还是定义一个类去封装控件比较好,因为可以把控件分装到不同的文件中,供别人使用。
tips:这里我们发现MyWidget
方法返回的也是一个Center ()
,那么我们其实是可以把runApp
中的Center
方法省略掉。并且如果一个方法里面,只有一句代码,dart语言是可以简写成如下的:
1 2 |
void main() => runApp(MyWidget()); |
这个是不是很熟悉,这就是我们刚开始创建项目时,默认的工程里面,main
函数的代码就是这样写的。这个在JS ES6里面好像也有。前端同学估计会熟悉一些。
我们点到Text
里面去看源码的时候,能看到如下简化代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
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:
1 2 3 4 5 |
final _textStyle = TextStyle( color: Colors.red, fontSize: 40.0, ); |
然后把这个_textStyle
赋值给Text
里面的style
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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
这一次直接上代码吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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
的文件,然后敲入如下代码:
1 2 3 4 5 6 7 8 9 10 11 |
class Animal { // 构造函数 const Animal({ this.name, this.imageUrl, }); final String name; final String imageUrl; } |
这里定义一个Animal
的类,const Animal()
就是构造函数,下面的final
定义的都是属性,在构造函数里面赋值name
和imageUrl
。这就构成了一个Animal
的模型。
创建数据源
我们创建完模型之后,才应该创建一下数据源。我们在animal.dart
文件中定义一下模型数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
//定义一个模型数组 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
替换成新建的类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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
,然后设置一下标题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Home extends StatelessWidget { Widget _cellForRow (BuildContext context, int index) { return Text('123'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Flutter Demo'), ), ); } } |
这里都不用多说。然后我们就需要设置我们的body
了,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
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,我们把这里的实现抽离出来:
1 2 3 4 |
Widget _cellForRow (BuildContext context, int index) { return Text('123'); } |
我们定义了一个_cellForRow
的Widget,这个Widget返回的就是一个row所对应显示的内容。
tips:这里说明一下:
- 我们在定义一个属性的时候,如果加了前缀“_”,就标识这是一个私有的。外面死不能使用的,如果没有加,那么说明外面是可以使用的。
- ListView中是没有section这个概念的,我们在需要需要分组的时候,必须得自己去一行行的实现了(这一点我觉得iOS做的要好很多,当然目前还不好说,后面慢慢来看)。
我们运行一下项目,就能看到如下的显示
在Row中添加视图
我们上面只在row里面添加了一个Text
,这在实际开发过程中是远远不够的,那么我们要怎么去添加别的视图在row中呢?
这里就要用到Container
了。话不多说,线上代码:
1 2 3 4 5 6 7 8 |
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怎么办呢?这里也可以的:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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
1 2 3 4 |
SizedBox( height: 20, ), |
把SizeBox
也加入到children
中去,并且加到文字个图片之间,这样文字跟图片之间就会有间距了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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
我们前面介绍了这个控件,那么如果我们想要拼接字符串怎么弄呢?上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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, ); } } |
这里我们看到这个标题是我们拼接到这个字符串上面的,所以说拼接的语法就是:
1 2 3 4 |
$_title //或者 ${_title} |
在Text
中,我们除了可以设置textAlign
以外,我们还可以设置maxLines
,就是限制最大行数。设置了最大行数之后,如果字数超过了行数,接下来的是不显示的,如图所示
1 2 3 4 5 6 7 8 |
Text( '《${_title}》-- $_detail。最近Flutter已经疯狂的刷屏了各个技术博客、技术网站,完全有一统天下的气势。所以最近也决定开始尝尝鲜,从零开始一步步的来探索Flutter的世界。就从环境搭建开始,记录一下自己探索Flutter的过程。', textAlign: TextAlign.center, style: _textStyle, maxLines: 3, overflow: TextOverflow.ellipsis, ); |
后面就多了…的符号。
富文本
直接线上代码吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
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
,来完成富文本的要求。
作者:薛立恒
– 现在注册滴滴云,得10000元立减红包
– 8月特惠,1C2G1M云服务器 9.9元/月限时抢
– 滴滴云使者专属特惠,云服务器低至68元/年
– 输入大师码【7886】,GPU全线产品9折优惠