概述
Flutter是Google在2015年推出的移动UI框架,可快速在iOS和Android上构建高质量的原生用户界面;第一次亮相于2015年5月Dart开发者峰会上,初始名字叫做“Sky”,后更名为Flutter,并于2018年12月5日正式发布1.0版本;截止到目前Flutter已经支持web、桌面、嵌入式等多个平台。
开发环境
Flutter支持多个系统平台环境开发,且官方教程较为详尽,此处以Windows平台为例简单扼要一些常见的问题。
首先我们需要配置以下国内镜像到环境变量中,以便后续可以正常访问Flutter;
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
复制代码
接着我们需要下载安装Flutter SDK
,这里我们尽量从官网下载(需翻墙),因为虽然官方文档说明可以从Flutter Github下载,但是经本人测试,虽然可以正常解压并找到flutter_console.bat
,但是运行flutter doctor
会直接闪退,为了避免不必要的麻烦,建议翻墙从官网下载;下载完成后解压(尽量不要放在高权限系统盘下),然后在环境变量path条目下添加flutter\bin
的全路径,此时打开命令窗口执行flutter doctor
会自动检测依赖项并编译;
第一项显示了Flutter的版本信息,说明我们的Flutter已经安装成功了,我们来看第二项提示命令行插件没有安装,我们打开Android studio SDK Tools下载并安装最新的插件;
再次执行flutter doctor
命令会发现插件已经安装成功,但是还提示许可没有接受,根据提示执行flutter doctor --android-licenses
,如有询问y/N
直接y
到底,关于工具链的配置就全部完成了;
我们接着看会发现提示Visual Studio和Android Studio没有安装,不对啊,Android Studio我安装了啊,应该是没有识别到,我们使用命令flutter config --android-studio-dir "此处为Android Studio路径"
进行配置,就会发现Android Studio已经变绿了,Visual Studio同理,因为我们采用Android Studio进行开发,故忽略Visual Studio的安装配置;
此时我们会发现最后一项HTTP HOST好像提示连接超时,这个其实不用搭理,如果你实在有强迫症,翻墙后再次执行下就会发现它连接成功,这个应该是为了测试是否能正常连接到Google的。
创建项目
开发环境已经配置好了,先创建个项目把Hello World跑起来看看,为了更好的支持Flutter开发我们需要下载Flutter
插件和dart
插件,重启后你会惊喜的发现File-New-New Flutter Project
直接就可以创建Flutter项目,创建的时候可能会要求配置Flutter SDK路径,很简单,这里就不再多做描述;
分析项目
观察创建完的界面,居然不是Hello World
,而是默认添加了一个计数器,好家伙,熟悉默认创建项目的门槛都变高了;我们先来看下目录结构,android
很明显就是我们与Android原生开发交互使用的,而ios
则是与ios原生开发交互使用的,最重要的就是这个lib-main.dart
文件了,因为它就是我们的主要页面代码,pubspec.yaml
是依赖项配置文件,其他的都是一些简单配置文件,不做描述;
接下来我们来看看main.dart
文件(Dart SDK已捆绑在Flutter SDK中,不熟悉Dart语言的可参考官方文档),文件中第一个就是main函数
,然后在函数中又执行了runApp( Widget app)
,而Flutter Widget
采用现代响应式框架构建,中心思想是用widget构建你的UI;因此我们可以大概知道界面启动是从main函数
开始,而runApp函数
可以理解为setContent
;
void main() {
runApp(const MyApp());
}
复制代码
接着再看MyApp
类,最终继承的是Widget
,然后通过重写build
方法不停的调用Widget
组件最终返回Widget
,这里也可以看出Flutter就是通过不停的嵌套调用Widget来构建UI的,熟悉Jetpack Compose的是不是感觉似曾相识,都是通过不停的嵌套构建UI函数来实现的。
基础使用
实践是检验真理的唯一标准,我们来写个简单的界面快速上手Flutter,比如我们要实现如下功能:添加一个输入框和一个添加按钮,每次点击添加按钮的时候输入框内容会被添加到一个带图片的列表中,然后点击列表又会将此条数据从列表中删除。
首先我们回过头看下初始创建的项目,加载的MyApp组件继承自StatelessWidget
,通过名称也可以发现这是一个无状态组件,其实就是加载一些静态且不需改变内部数据状态的组件,通过实现build()
方法返回所需要的widget,它的返回值是一个MaterialApp
组件,它封装了应用程序实现Material Design所需要的一些Widget;
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
复制代码
接着看MaterialApp的home属性加载了一个MyHomePage
组件,这里基本就是我们要编辑的页面,值得注意的是它继承自StatefulWidget
,可以理解为有状态的组件,用于加载一些内部数据状态需要变化的组件,它也是一个抽象类,有一个抽象方法createState()
,然后在这个抽象方法中调用自定义的_MyHomePageState()
类,它需要继承State
类,最后在这个类中实现build()
方法来返回真正的Widget;它的返回值是一个Scaffold
组件,它是Material Design布局结构的基本实现,包含drawer、snackbar和底部sheet等;
class MyHomePage extends StatefulWidget {
@override
State<MyHomePage> createState() => _MyHomePageState();
class _MyHomePageState extends State<MyHomePage> {
return Scaffold();
复制代码
基本结构我们已经分析完了,Scaffold
组件的body属性就是为了显示主要内容的,所以接下来们看看如何实现我们一开始的需求,分析界面有一个输入框和一个列表,所以整体应该是纵向排列;先定义一个Column组件用于子组件纵向排列,然后在children
属性添加我们需要的TextField和ListView组件,注意悬浮按钮是属于Scaffold
组件中的floatingActionButton
属性,另外可以使用mainAxisAlignment
属性来展示主轴子元素的显示位置;
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
TextField(),
ListView()
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Add',
child: const Icon(Icons.add),
),
复制代码
接下来我们先看输入框,既然是输入框,肯定需要获取到输入的内容,TextEditingController
作为TextField
的controller
属性,用户输入时,controller的text
和selection
属性不断的更新,就可以获取到用户输入的内容,当然你也可以使用onChanged
回调,另外为了体验我们使用decoration
属性添加未输入时的提示语;
final TextEditingController _controller = TextEditingController();
TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Please input content',
),
),
复制代码
最后我们来看下比较复杂的列表组件,可能熟悉Android开发的会知道Recyclerview做了很多优化机制,可以使得item复用来减少内存,其实Flutter也有提供类似的组件,因此我们这里使用ListView.Builder,它会自动回收列表元素,我们来看下两个主要的属性:itemCount
是列表的长度,而itemBuilder
需要一个函数,我们可以通过从此函数添加列表的单条数据结构,不过这里要注意,为了使得listview的高度可以正常被计算我们需要使用Expanded
组件去包裹;
List listWidgets = [];
Expanded(
child: ListView.builder(
itemCount: listWidgets.length,
itemBuilder: (BuildContext context, int position) {
return getItem(position);
}))
复制代码
我们继续看listview所需要的item布局,这里我们打算使用一张图片、一个标题和一个内容组成,先使用Row
组件使得图片和文字内容横向展示,然后再用一个Column
组件使得标题和内容文本纵向显示,为了美观我们可以添加一些间距或者分割线之类的属性,但是只是展示是没有意义的,我们通过GestureDetector
组件包裹去实现item的点击事件,这里我们让它点击后删除一条数据;
Widget getItem(int index) {
return GestureDetector(
child: Row(
Image.asset("images/ic_launcher.png",height: 50,width: 50,fit: BoxFit.cover,)
```
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("标题${index + 1}",style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.w600,),),
Text("这里是具体描述:${_controller.text}",style: const TextStyle( fontSize: 16,),
),
),
onTap: () {
setState(() {
listWidgets.removeAt(index);
});
},
],
),
复制代码
不过需要注意的是,图片直接这样写实显示不了的,需要在pubspec.yaml
配置文件中添路径,当然配置文件很贴心,还举例说明如何添加,但是images目录需要我们手动创建在根目录下,并把图片放进去,当然也可以使用其他名称,与配置文件中保持一致即可,其他属性就相对简单,这里不做描述;
flutter:
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
assets:
- images/
复制代码
现在我们发现好像列表数据只有点击移除,但是并没有初始数据,还记得一开始悬浮按钮中的onPressed
属性吗,我们可以在这里写一个方法添加一条数据到列表中,有没有发现不管是添加还是删除数据都用到了setState()
方法,这个方法就是用来更新有状态组件StatefulWidget
中的数据状态并且让子组件重绘的;
onPressed: () {
setState(() {
listWidgets.add(getItem(listWidgets.length + 1));
});
},
复制代码
好了,到这里我们就基本完成了一开始打算实现的一个小需求,虽然整体比较简单,但是基本涉及到了布局、列表、文本、图片以及数据处理等基本组件和逻辑;不过有些不是很熟悉声明式UI的可能不知道界面到底要怎么构建,下边我们以列表的item简单做一个widget嵌套的流程图并且展示下我们完成的效果图;
总结
以上只是Flutter的简单入门,其实还有很多有趣的玩法和高级使用,虽然和原生Android的命令式构建UI有很大区别,而且组件不停嵌套导致括号地狱,可读性很差,但是代码结构更容易去理解,而且Flutter目前已经支持web、桌面等多个平台,最重要的是它内置了自己的一套Widget组件,虽然借鉴了React的中心思想,但是相比其他跨平台完全不会有性能问题,快来体验下吧!