作者:真丶深红骑士
链接:
https://juejin.im/user/597247ad5188255aed1fbba6
本文由作者授权发布。
1.什么是Flutter
上周我的一位微信好友问我有没有学Flutter
,我回答说还没真正学,他说应该要接触一下。对于新技术的诞生,我始终保持敬畏之心,和另一位大学舍友聊了当时如何入坑Android
的经历,才发现自己的学习方式和路线有很多的问题,知识点很零乱,知识没有系统化,不多说了,后面学习新的知识一定要从“碎片化”到“整体化”。2018年2月,在世界移动大会上,Google发布了Flutter的第一个beta版本,2108年6月11日发布首个预览版,在2018年12月05日北京时间凌晨1点45分,在Flutter Live上,谷歌Flutter团队推出Flutter1.0,Flutter1.0版本是UI工具包的第一个稳定版本,在2019年2月27日世界移动通信大会上Google推出1.2版本,带来全新的Web开发工具。另外今日头条团队即将开源让Flutter真正支持View级别的混合开发(上层Flutter Framework引入Widget/LayerTree等概念自己实现了界面描述框架,下层Flutter Engine把LayerTree用OpenGL渲染成用户界面),闲鱼团队也开源了基于 Redux数据管理的组装式Flutter应用框架。什么是Flutter呢?Flutter是一个跨平台的免费开源的移动UI框架,是Google的移动应用SDK,用于在极短时间内在iOS和Android平台上创建高质量的原生体验,简而言之就是在iOS下和Android下共用一套代码,一套代码就能在两个操作系统下运行,其官方编程语言是是Dart,学习这门语言很快就上手。Flutter提供很多丰富的UI组件库,开发者可以快速开发出灵活的UI界面,另外Flutter已经加入Material Design组件大家庭中,也就是说Flutter可以使用Material Theming和Material 组件,可以相信未来会有更多的创意设计UI风格会涌现。
2.Flutter的特性
1、快速开发,Flutter的热重载可帮助快速轻松试验,构建UI,添加功能和快速修复错误,在iOS和Android的模拟器和硬件上体验亚秒级重载同时不会丢失状态,这里直接引用官方的图:
2、机具表现力和美观UI,Flutter内置的Material Design和Cupertino(iOS风格)的部件、丰富的手势API、自然平滑的滑动和不同的平台表现来提升用户体验,直接看下图:
3、现代化响应式框架,使用现代化响应式框架和丰富的平台、布局和基础组件来构建用户界面,使用功能强大且灵活的API(针对2D,动画,手势,动效等)解决复杂的用户界面设计。
1class CounterState extends State<Counter> { 2 int counter = 0; 3 4 void increment() { 5 // Tells the Flutter framework that state has changed, 6 // so the framework can run build() and update the display. 7 setState(() { 8 counter++; 9 });10 }1112 Widget build(BuildContext context) {13 // This method is rerun every time setState is called.14 // The Flutter framework has been optimized to make rerunning15 // build methods fast, so that you can just rebuild anything that16 // needs updating rather than having to individually change17 // instances of widgets.18 return new Row(19 children: <Widget>[20 new RaisedButton(21 onPressed: increment,22 child: new Text('Increment'),23 ),24 new Text('Count: $counter'),25 ],26 );27 }28}
4、使用平台原生功能及SDK,通过平台API,第三方SDK和原生代码让英语具有强大的扩展性,Flutter允许重复使用现有的Java,Swift和Object代码,并访问iOS和Android上的原生功能和SDK,例如下图获取手机电量数值:
1Future<Null> getBatteryLevel() async { 2 var batteryLevel = 'unknown'; 3 try { 4 int result = await methodChannel.invokeMethod('getBatteryLevel'); 5 batteryLevel = 'Battery level: $result%'; 6 } on PlatformException { 7 batteryLevel = 'Failed to get battery level.'; 8 } 9 setState(() {10 _batteryLevel = batteryLevel;11 });12}
5、统一应用开发体验:Flutter丰富的工具库可以让开发者轻松在iOS和Android设备上实现自己的想法,可以充分利用现有的大部分Java、Object-C或者Swift代码。
3.2019年Flutter产品线
2019年一月底,Flutter团队公布了2019年Flutter的产品线,为以下几点确定了明确的计划:
核心基础:Bug修复,性能调优,内存诊断工具优化,改进测试流程。
生态系统:更好的C/C++库支持,推进官方开发/维护的Packages达到与核心框架相同质量和完整性。
动态更新:在国内,这是必不可少的,提供Android上的动态修复:代码更新从服务器推送到Android应用里,也就是热更新。
支持开发移动端
web
应用。 并且Flutte UX研究团队会定期根据用户反馈来助力打造更优的Flutter,根据用户反馈来调整开发重点,可见Flutter UX团队的用心和付出程度,今年拭目以待。
1.架构图
Flutter框架从到下包含三部分:函数式响应的Framework(Dart),Engine(C++),Embedder(Platform Specific)。下面直接上图:
FrameWork是用Dart实现,提供了Material风格的小部件、Cupertino风格的小部件(用于iOS)、文本图像按钮组件、动画、手势等。
Engine时用C++实现的,上图详细了,实际包含三部分:Skia、Dart、Text。Skia是一个开源的2D图形库,为硬件和软件平台提供API。Dart包括Dart运行时和垃圾回收。Flutter在调试模式下运行,由JIT(Just in Time,运行时编译,边运行边编译)支持,如果在发布模式下,则是通过AOT(Ahead of Time,运行前编译,普通的静态编译)将Dart代码编译原生的"arm"代码。Text是指文本渲染库。
Embedder是指嵌入器,就是可将Flutter嵌入到各种平台中,它的主要任务是渲染Surface设置、线程设置和插件。
2.渲染过程
这里结合上面两张图来看,可以知道,当需要更新UI的时候,Framework通知Engine,Engine会等到下个Vsync信号到达的时候,会通知Framework,然后Framework会进行Animate,Build,Layout,Paint,最后生成Layer。Compositor 把所有的Layer组合成Scene,再通过Skia进行处理,最后Engine通过GPU GI接口提交数据给GPU,GPU最后经过处理后在显示器显示。上面简单了解什么Flutter,Flutter特性和框架,有了初步的了解,下面就开始一步一步实现第一个Flutter应用。
03Flutter环境搭建
1.获取Flutter SDK
务必电脑安装git,本文是在Windows环境下配置的,MAC下配置环境流程是一致的。首先要获取Flutter SDK,使用git去克隆仓库然后添加Flutter工具到自己的环境变量,运行flutter doctor来显示剩下需要安装的依赖,国内用户最好配置一下两个环境变量:
1PUB_HOSTED_URL=https://pub.flutter-io.cn
如下图所示:
1FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
如图所示:
因为这次我第一次在电脑上安装Flutter,所以就要克隆这个远程仓库,我在电脑F盘下执行下面git命令:
1 git clone -b beta https://github.com/flutter/flutter.gitgit clone -b beta https://github.com/flutter/flutter.git
如图所示:
下载比较慢,等了十多分钟才下载完。下载完看到F盘果然有一个名字叫flutter的文件夹,接着将克隆下来的项目bin目录配置到环境变量Path去,因为我是下载到F盘,所以下图路径是:
如果需要更新flutter的sdk,在命令行执行flutter upgrade命令即可。
2.运行flutter doctor
打开一个新的命令提示符窗口,运行下面命令flutter doctor,看是否需要安装任何依赖项来完成安装,这个命令会检测环境和在终端生成报告,Dart SDK和Flutter捆绑在一起,没必要单独去安装Dart。初次运行它会下载自己的依赖库并且自行编译,可能比较慢。后续运行flutter命令就会很快。这里注意,如果CMD窗口显示乱码问题,下面是解决方案:
Win+R进入进入CMD命令行输入regedit进入注册表
找到
HKEY_CURRENT_USER\Console\%SystemRoot%_system32_cmd.exe
如果该项下已存在CodePage项,则把值改为十进制”65001”,如果不存在,在该项下新建一个 DWORD(32位值),命名为“CodePage”,值设为“65001”重启cmd后生效
输入flutter doctor运行结果截图所示:
注意看到Android toolchain选项 意思是得要运行flutter doctor --android-licenses同意协议才可以安装Android工具链。输入
flutter doctor --android-licenses
一直按y即可。最后重新输入flutter doctor
,结果如下图:从上面运行结果可以知道一下信息:
Flutter的版本和渠道已经安装
Flutter运行需要的Android工具链已安装
Android studio开发工具已经安装
flutter插件和Dart插件没有安装(在Android stdio安装下面会说)
没有连接手机
3.搭建Android Studio开发环境
在Android Studio--File--Settings --plugins搜索Flutter即可,如下图:
点击安装的时候会弹出一下提示框:
大概意思是安装Flutter插件需要Dart插件的支持,就是需要和Dart插件一起安装,安装完成后,重启开发工具,就可以新建和开发程序了。注意:如果提示cannot download xxxx的话去plugins.jetbrains.com/ 搜索插件名字,下载对应的插件后解压到对应的位置,如下图:
或者点击插件本地安装:
最后重启自己的编译器即可。
1.New Flutter Application
因为现在是要创建应用程序,因此在Create New Flutter Project
下选择Flutter Application
:
2.配置项目信息
在下一个页面输入项目名字,配置Flutter SDK目录,项目存放的路径,项目的描述,公司的域名,项目的包名如图所示:
最后点击finish等待Android Studio完成所要创建的Flutter项目了。
3.项目目录
项目建立完成后,编辑器给我们生成的目录如下:
android:Andorid相关代码目录,里面代码配置和单独创建Andorid项目有些不一样
ios:iOS相关代码目录,存放Flutter与ios原生交互的一些代码
lib:应用源文件,dart文件,核心文件,可以创建不同的文件夹,存放不同文件
test:测试文件
.gitignore:忽略文件,记录一些不需要关注变更记录的文件,就是不添加到版本记录里面
.metadata:记录一些Flutter project一些基本信息,如版本,项目类型
.packages:记录一些lib文件的路径
.iml:是由IntelliJ IDEA创建的模块文件,用于开发Java应用程序的IDE。它存储有关开发模块的信息,该模块可能是Java,Plugin,Android或Maven组件; 保存模块路径,依赖关系和其他设置
pubspec.lock:这是根据当前项目依赖所生成的文件,记录当前使用的依赖版本
pubspec.yaml:包含Flutter应用程序的包数据,这个是配置依赖项的文件,比如配置远程public仓库的依赖项,或者本地资源(图片,音视频)
README.md:根据意思就是“读我”,这里会记录一些项目结构,详细信息,帮助信息,了解一个项目都是通常通过这个文件入手。
总体来看和原生Android的工程结构不一样了,因为代码都是在lib目录完成的,所以不能用Android多module多lib结构去创建module和lib,除非要用到原生交互的代码,可以在android目录里面去写,然后在lib目录里面去引用。相对Android开发者而言,多了ios目录,ios开发者而言,多了android目录,其他文件上面有具体详细说明。因为lib目录是开发者最主要关注的,打开lib目录,发现有个后缀是dart的文件,里面内容都是用dart语法来写的,还发现有void main() => runApp(MyApp());
main()函数对于学过java而言都非常清晰,这个应该是入口函数,另外发现StatelessWidget,StatefulWidget一些小控件,这里想应该是UI组件吧。这里先不管那么多,直接点击运行图标,运行效果图如下:
1.编写“Hello world”
现在还是按照新手走,先自己撸个“Hello world”出来吧。把main.dart中所有代码去掉,替换下面代码,屏幕中心显示Hello World
:
1import 'package:flutter/material.dart'; 2 3//这个是Dart中单行函数或者方法的简写 4void main() => runApp(MyApp()); 5 6//程序继承StatelessWidget,该应用程序成为一个widget,在Flutter中,大多数东西都是widget 7class MyApp extends StatelessWidget { 8 // 这个是应用的根widget 9 @override10 Widget build(BuildContext context) {11 //注意:一个app只能有一个MaterialApp12 return MaterialApp(13 //标题栏的名字14 title: 'Hello Flutter',15 //这个是Material library提供的一个widget,它提供了默认的导航栏、标题栏16 //包含主屏幕的widget树的body属性17 home:new Scaffold(18 appBar:new AppBar(19 title:const Text("Weclome to Flutter"),20 ),21 body:const Center(22 child:const Text("Hello World"),23 ),24 ),25 );26 }27}import 'package:flutter/material.dart';
2
3//这个是Dart中单行函数或者方法的简写
4void main() => runApp(MyApp());
5
6//程序继承StatelessWidget,该应用程序成为一个widget,在Flutter中,大多数东西都是widget
7class MyApp extends StatelessWidget {
8 // 这个是应用的根widget
9 @override
10 Widget build(BuildContext context) {
11 //注意:一个app只能有一个MaterialApp
12 return MaterialApp(
13 //标题栏的名字
14 title: 'Hello Flutter',
15 //这个是Material library提供的一个widget,它提供了默认的导航栏、标题栏
16 //包含主屏幕的widget树的body属性
17 home:new Scaffold(
18 appBar:new AppBar(
19 title:const Text("Weclome to Flutter"),
20 ),
21 body:const Center(
22 child:const Text("Hello World"),
23 ),
24 ),
25 );
26 }
27}
Android模拟器运行效果如下:
iOS模拟器运行效果如下:
感觉在iOS运行效果比Android上好多了。看到这里发现Android并不是沉浸式状态栏,而iOS默认就是沉浸式了,那有没有办法也能让Android版本也做成沉浸式呢,答案是肯定有的,一开始我的思路是应该是在main方法里判断是不是Android版本,如果是就设置,网上找里找,这种方法果然可行:
首先先导包:
1import 'dart:io';2import 'package:flutter/services.dart';import 'dart:io';
2import 'package:flutter/services.dart';
在mian方法根据Android版本做设置:
1//入口函数 2void main() { 3 //MaterialApp组件渲染后 4 runApp(MyApp()); 5 //判断如果是Android版本的话 设置Android状态栏透明沉浸式 6 if(Platform.isAndroid){ 7 //写在组件渲染之后,是为了在渲染后进行设置赋值,覆盖状态栏,写在渲染之前对MaterialApp组件会覆盖这个值。 8 SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent); 9 SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);10 }11}//入口函数
2void main() {
3 //MaterialApp组件渲染后
4 runApp(MyApp());
5 //判断如果是Android版本的话 设置Android状态栏透明沉浸式
6 if(Platform.isAndroid){
7 //写在组件渲染之后,是为了在渲染后进行设置赋值,覆盖状态栏,写在渲染之前对MaterialApp组件会覆盖这个值。
8 SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent);
9 SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
10 }
11}
还有另外一种方法,因为Flutter主入口只有一个MainActivity,也就是说所有的Flutter页面都会运行这个MainActivity,那我们只需要在这个主入口判断一下版本号然后将状态栏颜色设置成透明,具体位置在android->app->src->main->xxx->MainActivity
:
1public class MainActivity extends FlutterActivity { 2 @Override 3 protected void onCreate(Bundle savedInstanceState) { 4 super.onCreate(savedInstanceState); 5 //设置状态栏透明 6 if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP) 7 {//API>21,设置状态栏颜色透明 8 getWindow().setStatusBarColor(0); 9 }10 GeneratedPluginRegistrant.registerWith(this);11 }12}public class MainActivity extends FlutterActivity {
2 @Override
3 protected void onCreate(Bundle savedInstanceState) {
4 super.onCreate(savedInstanceState);
5 //设置状态栏透明
6 if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP)
7 {//API>21,设置状态栏颜色透明
8 getWindow().setStatusBarColor(0);
9 }
10 GeneratedPluginRegistrant.registerWith(this);
11 }
12}
最终效果Android和iOS下运行如下:
这样效果好很多了。另外发现,Android下的标题栏是左边对齐的,那怎么做成iOS那样标题栏在中间呢?很简单,在AppBar加上Center widget就可以了,代码如下:
1appBar:new AppBar(2 //ios和android标题栏统一在中间3 title:new Center(child :const Text("Weclome to Flutter")),45 ),new AppBar(
2 //ios和android标题栏统一在中间
3 title:new Center(child :const Text("Weclome to Flutter")),
4
5 ),
最终效果如下:
这样真正做到了iOS和Android版本统一了。上面代码app继承了StatelessWidget,这样本身也成为了widget,在Flutter中,很多时候一切都看作是widget,layout和padding等等。上面例子结构很清晰明了,就是StatelessWidget类包含了(应用栏)Appbar,和构成主页面widget树结构的body属性。而body的widget又包含了一个Center widget,Center widget又包含一个Text子widget,Center widget可以将子widget对齐到屏幕中心。
2.使用外部package
现在,通过在pubspec.yaml文件配置依赖项,去依赖一个提供英文单词的包,在 pub.dartlang.org/flutter/ 这个网站可以找到很多开源软件包,可以搜索指定的软件包查看对应版本。
pubspec文件管理着Flutter应用程序的静态资源文件,那在pubspec.yaml文件,将english_words(3.1.5版本)添加到依赖项,这里要注意:和^之间是有空格的,不能会出错。如下所示:
1dependencies:2 flutter:3 sdk: flutter45 # The following adds the Cupertino Icons font to your application.6 # Use with the CupertinoIcons class for iOS style icons.7 cupertino_icons: ^0.1.28 english_words: ^3.1.5dependencies:
2 flutter:
3 sdk: flutter
4
5 # The following adds the Cupertino Icons font to your application.
6 # Use with the CupertinoIcons class for iOS style icons.
7 cupertino_icons: ^0.1.2
8 english_words: ^3.1.5
这时候点击编辑器的右上角的Packages get,就是把package拉取到项目中去,并且可以看到控制台输出:Running
flutter packages get
in flutter_demo...在lib/main.dart下,将english_words导入,显示灰色证明你导入的库没有使用,如下图所示:
改用英文单词的package来生成文本,代码改为如下:
1//程序继承StatelessWidget,该应用程序成为一个widget,在Flutter中,大多数东西都是widget 2class MyApp extends StatelessWidget { 3 // 这个是应用的根widget 4 @override 5 Widget build(BuildContext context) { 6 //随机生成函数 7 var wordPair = new WordPair.random(); 8 return MaterialApp( 9 //标题栏的名字10 title: 'Hello Flutter',11 //这个是Material library提供的一个widget,它提供了默认的导航栏、标题栏12 //包含主屏幕的widget树的body属性13 home:new Scaffold(14 appBar:new AppBar(15 //ios和android标题栏统一在中间16 title: new Center(child: const Text("Weclome to Flutter")),17 ),18 body:new Center(19 //使用随机生成的英文单词 为什么不能const呢 因为 内容发生变化 const是用来修饰常量的20 child:new Text(wordPair.asPascalCase),21 ),22 ),23 );24 }25}//程序继承StatelessWidget,该应用程序成为一个widget,在Flutter中,大多数东西都是widget
2class MyApp extends StatelessWidget {
3 // 这个是应用的根widget
4 @override
5 Widget build(BuildContext context) {
6 //随机生成函数
7 var wordPair = new WordPair.random();
8 return MaterialApp(
9 //标题栏的名字
10 title: 'Hello Flutter',
11 //这个是Material library提供的一个widget,它提供了默认的导航栏、标题栏
12 //包含主屏幕的widget树的body属性
13 home:new Scaffold(
14 appBar:new AppBar(
15 //ios和android标题栏统一在中间
16 title: new Center(child: const Text("Weclome to Flutter")),
17 ),
18 body:new Center(
19 //使用随机生成的英文单词 为什么不能const呢 因为 内容发生变化 const是用来修饰常量的
20 child:new Text(wordPair.asPascalCase),
21 ),
22 ),
23 );
24 }
25}
注意的是Center widget 和 Text widget要用new
来创建,因为内容修改了,并不是常量了,这时候按保存就可以看到新的单词出现在屏幕中间文本了。
3.添加有状态的widget
上面MyApp是继承StatelessWidget,StatelessWidget是无状态的也是不可控的,意思是其属性是不能改变的,所有的值都是最终的。在平时开发中,很多控件都需要根据特定场景改变自身的状态,那么Flutter有没有提供有状态的widget呢?答案肯定是有的,Statefulwidget是有状态的,在其生命周期保持的状态可能会变化,实现一个有状态的widget至少需要两个类:StatefulWidgets类和State类。
添加RandomWordsWidget类,这个类继承StatefulWidget类,这个类添加到
main.dart
文件最底下
1//创建有状态的widget2class RandomWordsWidget extends StatefulWidget{34 @override5 State<StatefulWidget> createState() {6 return new RandomWordsState();7 }8}//创建有状态的widget
2class RandomWordsWidget extends StatefulWidget{
3
4 @override
5 State<StatefulWidget> createState() {
6 return new RandomWordsState();
7 }
8}
添加RandomWordsState了,这个类会保存RandomWordsWidget的状态,后面会保存一些特殊不同状态下的词组,并且在里面添加build方法,不然会报错,把生成单词的代码放到这个build类中去,如下图:
1//用来保存RandomWords widget的状态2class RandomWordsState extends State<RandomWordsWidget>{3 @override4 Widget build(BuildContext buildContext){5 var wordPair = new WordPair.random();6 return new Text(wordPair.asPascalCase);78 }9}//用来保存RandomWords widget的状态
2class RandomWordsState extends State<RandomWordsWidget>{
3 @override
4 Widget build(BuildContext buildContext){
5 var wordPair = new WordPair.random();
6 return new Text(wordPair.asPascalCase);
7
8 }
9}MyApp下改成创建RandomWordsWidget即可:
1//程序继承StatelessWidget,该应用程序成为一个widget,在Flutter中,大多数东西都是widget 2class MyApp extends StatelessWidget { 3 // 这个是应用的根widget 4 @override 5 Widget build(BuildContext context) { 6 return MaterialApp( 7 //标题栏的名字 8 title: 'Hello Flutter', 9 //这个是Material library提供的一个widget,它提供了默认的导航栏、标题栏10 //包含主屏幕的widget树的body属性11 home:new Scaffold(12 appBar:new AppBar(13 //ios和android标题栏统一在中间14 title: new Center(child: const Text("Weclome to Flutter")),15 ),16 body:new Center(17 child:new RandomWordsWidget()18 ),19 ),20 );21 }//程序继承StatelessWidget,该应用程序成为一个widget,在Flutter中,大多数东西都是widget
2class MyApp extends StatelessWidget {
3 // 这个是应用的根widget
4 @override
5 Widget build(BuildContext context) {
6 return MaterialApp(
7 //标题栏的名字
8 title: 'Hello Flutter',
9 //这个是Material library提供的一个widget,它提供了默认的导航栏、标题栏
10 //包含主屏幕的widget树的body属性
11 home:new Scaffold(
12 appBar:new AppBar(
13 //ios和android标题栏统一在中间
14 title: new Center(child: const Text("Weclome to Flutter")),
15 ),
16 body:new Center(
17 child:new RandomWordsWidget()
18 ),
19 ),
20 );
21 }
这时候运行的效果还是跟之前一样,不过实现方式不一样。
4.创建ListView
下面创建一个滑动组件ListView,开发者接触最多应该是这个滑动组件,下面实现当用户滑动列表的时候,ListView会不断增长,不断显示新的单词。
在RandomWordsState这个类创建一个数组列表,用来保存词组,这个变量以下划线为开头,Dart语言中下划线前缀是表示强制私有:
1final _normalWords = <WordPair>[];final _normalWords = <WordPair>[];
在RandomWordsState类添加
_buildNormalWords
方法,用于构建一个ListView,ListView提供了itemBuilder属性,这是一个工厂builder作为匿名函数进行回调,这个函数需要传入两个参数,一个是BuildContext上下文和行迭代器。对于ListView每一行都会执行这个函数调用,这里想想好像是平时Android开发中ListView创建添加item的方法。1//创建填充单词的ListView 2 Widget _buildNormalWorlds() { 3 //内容上下16dp 4 return new ListView.builder( 5 padding: const EdgeInsets.fromLTRB(0, 16, 0, 16), 6 //每个单词都会条约一次itemBuilder,然后将单词添加到ListTile中 7 itemBuilder: (context, i) { 8 //首先创建10条单词 9 if (i >= _normalWords.length) {10 //接着再生成10个单词,添加到列表上11 _normalWords.addAll(generateWordPairs().take(10));12 }13 return _buildItem(_normalWords[i]);14 });15 }//创建填充单词的ListView
2 Widget _buildNormalWorlds() {
3 //内容上下16dp
4 return new ListView.builder(
5 padding: const EdgeInsets.fromLTRB(0, 16, 0, 16),
6 //每个单词都会条约一次itemBuilder,然后将单词添加到ListTile中
7 itemBuilder: (context, i) {
8 //首先创建10条单词
9 if (i >= _normalWords.length) {
10 //接着再生成10个单词,添加到列表上
11 _normalWords.addAll(generateWordPairs().take(10));
12 }
13 return _buildItem(_normalWords[i]);
14 });
15 }因为ListView是需要设置Item项的样式,这里同样在RandomWordsState里添加_buildItem方法,用来加载指定数据,这里指定内容居中显示:
1 //设置每个item项的内容和样式2 Widget _buildItem(WordPair pair) {3 return new ListTile(4 title: new Text(5 pair.asPascalCase,6 textAlign: TextAlign.center,7 ),8 );9 }//设置每个item项的内容和样式
2 Widget _buildItem(WordPair pair) {
3 return new ListTile(
4 title: new Text(
5 pair.asPascalCase,
6 textAlign: TextAlign.center,
7 ),
8 );
9 }更改RandomWordsState的
build
方法,增加_buildNormalWords
方法的调用,不是直接用单生成库,代方法如下:1 Widget build(BuildContext buildContext) { 2// var wordPair = new WordPair.random();删掉 3// return new Text(wordPair.asPascalCase);删掉 4 return new Scaffold( 5 appBar: new AppBar( 6 title: new Center(child: const Text("Weclome to Flutter")), 7 ), 8 body:_buildNormalWorlds(), 9 );10 }Widget build(BuildContext buildContext) {
2// var wordPair = new WordPair.random();删掉
3// return new Text(wordPair.asPascalCase);删掉
4 return new Scaffold(
5 appBar: new AppBar(
6 title: new Center(child: const Text("Weclome to Flutter")),
7 ),
8 body:_buildNormalWorlds(),
9 );
10 }更改MyApp的
build
方法,因为标题的设置放在了RandomWordsState里了,所以不需要再额外添加,并将home变成RandomWords widget,代码一下子简洁很多如下:1//程序继承StatelessWidget,该应用程序成为一个widget,在Flutter中,大多数东西都是widget 2class MyApp extends StatelessWidget { 3 // 这个是应用的根widget 4 @override 5 Widget build(BuildContext context) { 6 return MaterialApp( 7 home:new RandomWordsWidget(), 8 ); 9 }10}//程序继承StatelessWidget,该应用程序成为一个widget,在Flutter中,大多数东西都是widget
2class MyApp extends StatelessWidget {
3 // 这个是应用的根widget
4 @override
5 Widget build(BuildContext context) {
6 return MaterialApp(
7 home:new RandomWordsWidget(),
8 );
9 }
10}最终效果如图所示
下面添加分割线,让UI更美观,这里通过因为计算机里循环开始都是从0开始的,我
_buildNormalWorlds
方法里判断是奇数项的话就构造出分割线添加到ListView,这里注意分割线也是一项,那么生成单词的时候要稍微处理下,具体代码如下:
5.添加交互
在RandomWordsState里添加一个
_collected
的Set集合,这个集合用来存放收藏后的单词,用Set的原因是Set本身的特性不允许元素有重复值:
1final Set<WordPair> _collected = new Set<WordPair>();Set<WordPair> _collected = new Set<WordPair>();
在
_buildItem
方法中添加isCollected
来检查单词是否添加到收藏里
1final bool isCollected = _collected.contains(pair);final bool isCollected = _collected.contains(pair);
在
_buildItem
以后置属性来添加一个❤️图标到ListTiles,matrial包下有默认的❤️图标,这里就设置下颜色就可以,代码如下:
1return new ListTile( 2 title: new Text( 3 pair.asPascalCase, 4 textAlign: TextAlign.center, 5 ), 6 //trailing 是后置图标属性 7 trailing: new Icon( 8 //material 包下 icons.dart 9 isCollected ? Icons.favorite : Icons.favorite_border,10 //图标颜色设置11 color:isCollected ? Colors.red : null,12 ),13 );return new ListTile(
2 title: new Text(
3 pair.asPascalCase,
4 textAlign: TextAlign.center,
5 ),
6 //trailing 是后置图标属性
7 trailing: new Icon(
8 //material 包下 icons.dart
9 isCollected ? Icons.favorite : Icons.favorite_border,
10 //图标颜色设置
11 color:isCollected ? Colors.red : null,
12 ),
13 );
运行结果后发现❤️型图标添加到每一行最右边处。
下面增加点击事件,当点击列表时,如果单词没被收藏,那么就添加收藏,并将心形图标改为收藏后的图标,如果单词被收藏了,那么就移除处收藏,并将心形图标变为默认的心形图标,代码如下:
1 //trailing 是后置图标属性 2 trailing: new Icon( 3 //material 包下 icons.dart 4 isCollected ? Icons.favorite : Icons.favorite_border, 5 //图标颜色设置 6 color:isCollected ? Colors.red : null, 7 ), 8 9 //item的点击事件属性10 onTap:(){11 //状态设置12 setState(() {13 //如果收藏了14 if(isCollected){15 //那就将set集合里移除16 _collected.remove(pair);17 } else {18 //添加19 _collected.add(pair);20 }21 });2223 }//trailing 是后置图标属性
2 trailing: new Icon(
3 //material 包下 icons.dart
4 isCollected ? Icons.favorite : Icons.favorite_border,
5 //图标颜色设置
6 color:isCollected ? Colors.red : null,
7 ),
8
9 //item的点击事件属性
10 onTap:(){
11 //状态设置
12 setState(() {
13 //如果收藏了
14 if(isCollected){
15 //那就将set集合里移除
16 _collected.remove(pair);
17 } else {
18 //添加
19 _collected.add(pair);
20 }
21 });
22
23 }
效果如下:
下面实现跳转新页面,首先添加一个收藏单词页面,在Flutter中,导航器管理应用程序的路由栈,什么是路由栈呢?路由栈就是用栈来管理路由(页面)。就是我现在要打开一个新的页面,那么这个新的页面就会压入路由栈中,如果我现在在新的页面,点击返回到上一个页面,那么路由栈会弹出这个新页面路由,将显示返回到前一个页面,这里想想和Android原生用栈管理页面的方法一样,首先在首页面增加跳转新页面图标,代码如下:
1Widget build(BuildContext buildContext) { 2 return new Scaffold( 3 appBar: new AppBar( 4 title: new Center(child: const Text("Weclome to Flutter")), 5 //增加图标和点击事件动作 Icons.list是icon的类型 onPressed添加点击事件 点击会执行_collectWordsPage方法 6 actions:<Widget>[ 7 new IconButton(icon: const Icon(Icons.list),onPressed: _collectWordsPage), 8 ], 910 ),11 body:_buildNormalWorlds(),12 );13 }Widget build(BuildContext buildContext) {
2 return new Scaffold(
3 appBar: new AppBar(
4 title: new Center(child: const Text("Weclome to Flutter")),
5 //增加图标和点击事件动作 Icons.list是icon的类型 onPressed添加点击事件 点击会执行_collectWordsPage方法
6 actions:<Widget>[
7 new IconButton(icon: const Icon(Icons.list),onPressed: _collectWordsPage),
8 ],
9
10 ),
11 body:_buildNormalWorlds(),
12 );
13 }
实际运行后,AppBar导航栏最右边的位置添加了一个图标,这时候点击没有任何反应,因为_collectWordsPage没有任何代码,下面添加跳转到新页面的实现。
在_collectWordsPage添加Navigator.push,这个简单易懂,就是会把页面入栈
1 //跳转新页面开始2 void _collectWordsPage(){3 Navigator.of(context).push(456 );7 }//跳转新页面开始
2 void _collectWordsPage(){
3 Navigator.of(context).push(
4
5
6 );
7 }
下面添加MaterialPageRoute和builder,添加构建ListTile行的代码,并添加项与项之间的分割线,最后通过
toList
来转换。MaterialPageRoute
是一种模态路由,可以通过平台自适应来切换屏幕,Android而言,页面推送过渡向上滑动页面,淡入淡出,弹出过渡则是向下滑动页面。IOS而言,页面从右侧滑入,反向弹出,当另一个页面进入覆盖时,该页面向左移动。
1 //跳转新页面开始 2 void _collectWordsPage() { 3 Navigator.of(context).push( 4 new MaterialPageRoute<void>( 5 builder: (BuildContext context) { 6 //传递收藏后的单词 7 final Iterable<ListTile> tiles = _collected.map((WordPair pair) { 8 return new ListTile( 9 title: new Text(10 pair.asPascalCase, //单词11 ),12 );13 },14 );15 },1617 ),18 );19 }//跳转新页面开始
2 void _collectWordsPage() {
3 Navigator.of(context).push(
4 new MaterialPageRoute<void>(
5 builder: (BuildContext context) {
6 //传递收藏后的单词
7 final Iterable<ListTile> tiles = _collected.map((WordPair pair) {
8 return new ListTile(
9 title: new Text(
10 pair.asPascalCase, //单词
11 ),
12 );
13 },
14 );
15 },
16
17 ),
18 );
19 }
创建Scaffold,其中包含标题栏,body由ListView组成,每一个Item项之间有一条分割线,运行发现,新的路由会自动添加返回按钮,这是因为Navigator会在应用栏自动添加一个“返回”按钮,不需要调用
Navigator.pop
,点击返回按钮会返回到主界面,代码如下:
1//跳转新页面开始 2 void _collectWordsPage() { 3 Navigator.of(context).push( 4 new MaterialPageRoute<void>( 5 builder: (BuildContext context) { 6 //传递收藏后的单词 7 final Iterable<ListTile> tiles = _collected.map((WordPair pair) { 8 return new ListTile( 9 title: new Text(10 pair.asPascalCase, //单词11 ),12 );13 },14 );15 //添加分割线开始16 final List<Widget> divided = ListTile.divideTiles(17 tiles: tiles,18 context:context,19 ).toList();20 return new Scaffold(21 appBar: new AppBar(22 title: new Center(child: const Text("Collect Words")),23 ),24 body: new ListView(children: divided),25 );26 },2728 ),29 );30 }//跳转新页面开始
2 void _collectWordsPage() {
3 Navigator.of(context).push(
4 new MaterialPageRoute<void>(
5 builder: (BuildContext context) {
6 //传递收藏后的单词
7 final Iterable<ListTile> tiles = _collected.map((WordPair pair) {
8 return new ListTile(
9 title: new Text(
10 pair.asPascalCase, //单词
11 ),
12 );
13 },
14 );
15 //添加分割线开始
16 final List<Widget> divided = ListTile.divideTiles(
17 tiles: tiles,
18 context:context,
19 ).toList();
20 return new Scaffold(
21 appBar: new AppBar(
22 title: new Center(child: const Text("Collect Words")),
23 ),
24 body: new ListView(children: divided),
25 );
26 },
27
28 ),
29 );
30 }
最终运行效果发现标题并不是居中对齐,因为默认的有边距,这时候需要自定义AppBar就可以,代码如下:
1 return new Scaffold( 2 appBar: new AppBar( 3 titleSpacing: 0.0, 4 //MaterialPageRoute这个自带了返回键 下面的属性设置取消返回键 5 automaticallyImplyLeading:false, 6 title: new Container(decoration: new BoxDecoration(color: new Color(0x00000000), 7 ), 8 child:new Stack( 9 children: <Widget>[10 new Container(11 //左边位置12 alignment: Alignment.centerLeft,13 child:new IconButton(14 //图标样式15 icon:new Icon(Icons.arrow_back),16 //点击事件17 onPressed: (){18 Navigator.pop(context);19 }20 ),21 ),22 new Center(child:new Text("Collect Words")),23 ],24 ),)25 ),26 body: new ListView(children: divided),27 );return new Scaffold(
2 appBar: new AppBar(
3 titleSpacing: 0.0,
4 //MaterialPageRoute这个自带了返回键 下面的属性设置取消返回键
5 automaticallyImplyLeading:false,
6 title: new Container(decoration: new BoxDecoration(color: new Color(0x00000000),
7 ),
8 child:new Stack(
9 children: <Widget>[
10 new Container(
11 //左边位置
12 alignment: Alignment.centerLeft,
13 child:new IconButton(
14 //图标样式
15 icon:new Icon(Icons.arrow_back),
16 //点击事件
17 onPressed: (){
18 Navigator.pop(context);
19 }
20 ),
21 ),
22 new Center(child:new Text("Collect Words")),
23 ],
24 ),)
25 ),
26 body: new ListView(children: divided),
27 );
最终运行效果如下,左边是IOS,右边是Android:
六、总结
入门Flutter的第一天,简单知道了一下几点:
Flutter的特性和框架构成,它和大多数构建移动应用的工具不一样,它是用自己的渲染引擎来绘制
Widget
,它中间层只有C/C++代码,Flutter
使用Dart语言实现系统的绝大部分功能(布局,动画,手势)。开发环境的搭建。
体验Flutter添加UI的步骤。
路由(页面)的跳转。
若有错误,欢迎指正~
推荐阅读:
扫一扫 关注我的公众号
新号希望大家能够多多支持我~