上周我的一位微信好友问我有没有学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风格会涌现。
class CounterState extends State<Counter> {
int counter = 0;
void increment() {
// Tells the Flutter framework that state has changed,
// so the framework can run build() and update the display.
setState(() {
counter++;
});
}
Widget build(BuildContext context) {
// This method is rerun every time setState is called.
// The Flutter framework has been optimized to make rerunning
// build methods fast, so that you can just rebuild anything that
// needs updating rather than having to individually change
// instances of widgets.
return new Row(
children: <Widget>[
new RaisedButton(
onPressed: increment,
child: new Text('Increment'),
),
new Text('Count: $counter'),
],
);
}
}
Future<Null> getBatteryLevel() async {
var batteryLevel = 'unknown';
try {
int result = await methodChannel.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level: $result%';
} on PlatformException {
batteryLevel = 'Failed to get battery level.';
}
setState(() {
_batteryLevel = batteryLevel;
});
}
2019年一月底,Flutter团队公布了2019年Flutter的产品线,为以下几点确定了明确的计划:
web
应用。Flutter框架从到下包含三部分:函数式响应的Framework(Dart),Engine(C++),Embedder(Platform Specific)。下面直接上图:
这里结合上面两张图来看,可以知道,当需要更新UI的时候,Framework通知Engine,Engine会等到下个Vsync信号到达的时候,会通知Framework,然后Framework会进行Animate,Build,Layout,Paint,最后生成Layer。Compositor 把所有的Layer组合成Scene,再通过Skia进行处理,最后Engine通过GPU GI接口提交数据给GPU,GPU最后经过处理后在显示器显示。上面简单了解什么Flutter,Flutter特性和框架,有了初步的了解,下面就开始一步一步实现第一个Flutter应用。
务必电脑安装git,本文是在Windows环境下配置的,MAC下配置环境流程是一致的。首先要获取Flutter SDK,使用git去克隆仓库然后添加Flutter工具到自己的环境变量,运行flutter doctor来显示剩下需要安装的依赖,国内用户最好配置一下两个环境变量:
PUB_HOSTED_URL=https://pub.flutter-io.cn
如下图所示:
FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
如图所示:
因为这次我第一次在电脑上安装Flutter,所以就要克隆这个远程仓库,我在电脑F盘下执行下面git命令:
$ git clone -b beta https://github.com/flutter/flutter.git
如图所示:
下载比较慢,等了十多分钟才下载完。下载完看到F盘果然有一个名字叫flutter的文件夹,接着将克隆下来的项目bin目录配置到环境变量Path去,因为我是下载到F盘,所以下图路径是:
如果需要更新flutter的sdk,在命令行执行flutter upgrade命令即可。
打开一个新的命令提示符窗口,运行下面命令flutter doctor,看是否需要安装任何依赖项来完成安装,这个命令会检测环境和在终端生成报告,Dart SDK和Flutter捆绑在一起,没必要单独去安装Dart。初次运行它会下载自己的依赖库并且自行编译,可能比较慢。后续运行flutter命令就会很快。这里注意,如果CMD窗口显示乱码问题,下面是解决方案:
HKEY_CURRENT_USER\Console\%SystemRoot%_system32_cmd.exe
如果该项下已存在CodePage项,则把值改为十进制”65001”,如果不存在,在该项下新建一个 DWORD(32位值),命名为“CodePage”,值设为“65001”flutter doctor --android-licenses
一直按y即可。最后重新输入flutter doctor
,结果如下图:在Android Studio–File–Settings --plugins搜索Flutter即可,如下图:
点击安装的时候会弹出一下提示框:
大概意思是安装Flutter插件需要Dart插件的支持,就是需要和Dart插件一起安装,安装完成后,重启开发工具,就可以新建和开发程序了。
注意:如果提示cannot download xxxx的话去https://plugins.jetbrains.com/搜索插件名字,下载对应的插件后解压到对应的位置,如下图:
或者点击插件本地安装:
最后重启自己的编译器即可。
因为现在是要创建应用程序,因此在Create New Flutter Project
下选择Flutter Application
:
在下一个页面输入项目名字,配置Flutter SDK目录,项目存放的路径,项目的描述,公司的域名,项目的包名如图所示:
最后点击finish等待Android Studio完成所要创建的Flutter项目了。
项目建立完成后,编辑器给我们生成的目录如下:
总体来看和原生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组件吧。这里先不管那么多,直接点击运行图标,运行效果图如下:
走到这里,说明项目环境配置成功,并成功运行第一个简单的程序。
现在还是按照新手走,先自己撸个“Hello world”出来吧。把main.dart中所有代码去掉,替换下面代码,屏幕中心显示Hello World
:
import 'package:flutter/material.dart';
//这个是Dart中单行函数或者方法的简写
void main() => runApp(MyApp());
//程序继承StatelessWidget,该应用程序成为一个widget,在Flutter中,大多数东西都是widget
class MyApp extends StatelessWidget {
// 这个是应用的根widget
@override
Widget build(BuildContext context) {
//注意:一个app只能有一个MaterialApp
return MaterialApp(
//标题栏的名字
title: 'Hello Flutter',
//这个是Material library提供的一个widget,它提供了默认的导航栏、标题栏
//包含主屏幕的widget树的body属性
home:new Scaffold(
appBar:new AppBar(
title:const Text("Weclome to Flutter"),
),
body:const Center(
child:const Text("Hello World"),
),
),
);
}
}
Android模拟器运行效果如下:
iOS模拟器运行效果如下:
感觉在iOS运行效果比Android上好多了。看到这里发现Android并不是沉浸式状态栏,而iOS默认就是沉浸式了,那有没有办法也能让Android版本也做成沉浸式呢,答案是肯定有的,一开始我的思路是应该是在main方法里判断是不是Android版本,如果是就设置,网上找里找,这种方法果然可行:
import 'dart:io';
import 'package:flutter/services.dart';
//入口函数
void main() {
//MaterialApp组件渲染后
runApp(MyApp());
//判断如果是Android版本的话 设置Android状态栏透明沉浸式
if(Platform.isAndroid){
//写在组件渲染之后,是为了在渲染后进行设置赋值,覆盖状态栏,写在渲染之前对MaterialApp组件会覆盖这个值。
SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent);
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
}
}
还有另外一种方法,因为Flutter主入口只有一个MainActivity,也就是说所有的Flutter页面都会运行这个MainActivity,那我们只需要在这个主入口判断一下版本号然后将状态栏颜色设置成透明,具体位置在android->app->src->main->xxx->MainActivity
:
public class MainActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//设置状态栏透明
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP)
{//API>21,设置状态栏颜色透明
getWindow().setStatusBarColor(0);
}
GeneratedPluginRegistrant.registerWith(this);
}
}
最终效果Android和iOS下运行如下:
这样效果好很多了。另外发现,Android下的标题栏是左边对齐的,那怎么做成iOS那样标题栏在中间呢?很简单,在AppBar加上Center widget就可以了,代码如下:
appBar:new AppBar(
//ios和android标题栏统一在中间
title:new Center(child :const Text("Weclome to Flutter")),
),
最终效果如下:
这样真正做到了iOS和Android版本统一了。上面代码app继承了StatelessWidget,这样本身也成为了widget,在Flutter中,很多时候一切都看作是widget,layout和padding等等。上面例子结构很清晰明了,就是StatelessWidget类包含了(应用栏)Appbar,和构成主页面widget树结构的body属性。而body的widget又包含了一个Center widget,Center widget又包含一个Text子widget,Center widget可以将子widget对齐到屏幕中心。
现在,通过在pubspec.yaml文件配置依赖项,去依赖一个提供英文单词的包,在https://pub.dartlang.org/flutter/这个网站可以找到很多开源软件包,可以搜索指定的软件包查看对应版本。
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
english_words: ^3.1.5
flutter packages get
in flutter_demo…//程序继承StatelessWidget,该应用程序成为一个widget,在Flutter中,大多数东西都是widget
class MyApp extends StatelessWidget {
// 这个是应用的根widget
@override
Widget build(BuildContext context) {
//随机生成函数
var wordPair = new WordPair.random();
return MaterialApp(
//标题栏的名字
title: 'Hello Flutter',
//这个是Material library提供的一个widget,它提供了默认的导航栏、标题栏
//包含主屏幕的widget树的body属性
home:new Scaffold(
appBar:new AppBar(
//ios和android标题栏统一在中间
title: new Center(child: const Text("Weclome to Flutter")),
),
body:new Center(
//使用随机生成的英文单词 为什么不能const呢 因为 内容发生变化 const是用来修饰常量的
child:new Text(wordPair.asPascalCase),
),
),
);
}
}
注意的是Center widget 和 Text widget要用new
来创建,因为内容修改了,并不是常量了,这时候按保存就可以看到新的单词出现在屏幕中间文本了。
上面MyApp是继承StatelessWidget,StatelessWidget是无状态的也是不可控的,意思是其属性是不能改变的,所有的值都是最终的。在平时开发中,很多控件都需要根据特定场景改变自身的状态,那么Flutter有没有提供有状态的widget呢?答案肯定是有的,Statefulwidget是有状态的,在其生命周期保持的状态可能会变化,实现一个有状态的widget至少需要两个类:StatefulWidgets类和State类。
main.dart
文件最底下://创建有状态的widget
class RandomWordsWidget extends StatefulWidget{
@override
State<StatefulWidget> createState() {
return new RandomWordsState();
}
}
//用来保存RandomWords widget的状态
class RandomWordsState extends State<RandomWordsWidget>{
@override
Widget build(BuildContext buildContext){
var wordPair = new WordPair.random();
return new Text(wordPair.asPascalCase);
}
}
//程序继承StatelessWidget,该应用程序成为一个widget,在Flutter中,大多数东西都是widget
class MyApp extends StatelessWidget {
// 这个是应用的根widget
@override
Widget build(BuildContext context) {
return MaterialApp(
//标题栏的名字
title: 'Hello Flutter',
//这个是Material library提供的一个widget,它提供了默认的导航栏、标题栏
//包含主屏幕的widget树的body属性
home:new Scaffold(
appBar:new AppBar(
//ios和android标题栏统一在中间
title: new Center(child: const Text("Weclome to Flutter")),
),
body:new Center(
child:new RandomWordsWidget()
),
),
);
}
这时候运行的效果还是跟之前一样,不过实现方式不一样。
下面创建一个滑动组件ListView,开发者接触最多应该是这个滑动组件,下面实现当用户滑动列表的时候,ListView会不断增长,不断显示新的单词。
final _normalWords = <WordPair>[];
_buildNormalWords
方法,用于构建一个ListView,ListView提供了itemBuilder属性,这是一个工厂builder作为匿名函数进行回调,这个函数需要传入两个参数,一个是BuildContext上下文和行迭代器。对于ListView每一行都会执行这个函数调用,这里想想好像是平时Android开发中ListView创建添加item的方法。//创建填充单词的ListView
Widget _buildNormalWorlds() {
//内容上下16dp
return new ListView.builder(
padding: const EdgeInsets.fromLTRB(0, 16, 0, 16),
//每个单词都会条约一次itemBuilder,然后将单词添加到ListTile中
itemBuilder: (context, i) {
//首先创建10条单词
if (i >= _normalWords.length) {
//接着再生成10个单词,添加到列表上
_normalWords.addAll(generateWordPairs().take(10));
}
return _buildItem(_normalWords[i]);
});
}
//设置每个item项的内容和样式
Widget _buildItem(WordPair pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
textAlign: TextAlign.center,
),
);
}
build
方法,增加_buildNormalWords
方法的调用,不是直接用单生成库,代方法如下: Widget build(BuildContext buildContext) {
// var wordPair = new WordPair.random();删掉
// return new Text(wordPair.asPascalCase);删掉
return new Scaffold(
appBar: new AppBar(
title: new Center(child: const Text("Weclome to Flutter")),
),
body:_buildNormalWorlds(),
);
}
build
方法,因为标题的设置放在了RandomWordsState里了,所以不需要再额外添加,并将home变成RandomWords widget,代码一下子简洁很多如下://程序继承StatelessWidget,该应用程序成为一个widget,在Flutter中,大多数东西都是widget
class MyApp extends StatelessWidget {
// 这个是应用的根widget
@override
Widget build(BuildContext context) {
return MaterialApp(
home:new RandomWordsWidget(),
);
}
}
最终效果如图所示
_buildNormalWorlds
方法里判断是奇数项的话就构造出分割线添加到ListView,这里注意分割线也是一项,那么生成单词的时候要稍微处理下,具体代码如下://创建填充单词的ListView
Widget _buildNormalWorlds() {
//内容上下16dp
return new ListView.builder(
padding: const EdgeInsets.fromLTRB(0, 16, 0, 16),
//每个单词都会条约一次itemBuilder,然后将单词添加到ListTile中
itemBuilder: (context, i) {
//添加分割线
//奇数行,会添加一个分割线的widget,分割上下单词
//偶数行,就正常构造添加ListTitle row
//构造一像素的分割线 也可以自己去定义宽高
//i.isOdd判断是否奇数,奇数行添加分割线
if(i.isOdd){
//注意:这里执行后会跳出循环
return new Divider();
}
//这里对2求商,就是计算出ListView中减去分割线后的实际单词数量
//如 i为 1,2,3,4,5,那么商结果是0,1,1,2,2
//1 除以 2商是0 2除以2商是1 以此类推
final int index = i ~/2;
//首先创建10条单词
if (index >= _normalWords.length) {
//接着再生成10个单词,添加到列表上
_normalWords.addAll(generateWordPairs().take(10));
}
return _buildItem(_normalWords[index]);
});
}
实际效果如下:
_collected
的Set集合,这个集合用来存放收藏后的单词,用Set的原因是Set本身的特性不允许元素有重复值:final Set<WordPair> _collected = new Set<WordPair>();
_buildItem
方法中添加isCollected
来检查单词是否添加到收藏里final bool isCollected = _collected.contains(pair);
_buildItem
以后置属性来添加一个❤️图标到ListTiles,matrial包下有默认的❤️图标,这里就设置下颜色就可以,代码如下:return new ListTile(
title: new Text(
pair.asPascalCase,
textAlign: TextAlign.center,
),
//trailing 是后置图标属性
trailing: new Icon(
//material 包下 icons.dart
isCollected ? Icons.favorite : Icons.favorite_border,
//图标颜色设置
color:isCollected ? Colors.red : null,
),
);
运行结果后发现❤️型图标添加到每一行最右边处。
//trailing 是后置图标属性
trailing: new Icon(
//material 包下 icons.dart
isCollected ? Icons.favorite : Icons.favorite_border,
//图标颜色设置
color:isCollected ? Colors.red : null,
),
//item的点击事件属性
onTap:(){
//状态设置
setState(() {
//如果收藏了
if(isCollected){
//那就将set集合里移除
_collected.remove(pair);
} else {
//添加
_collected.add(pair);
}
});
}
效果如下:
Widget build(BuildContext buildContext) {
return new Scaffold(
appBar: new AppBar(
title: new Center(child: const Text("Weclome to Flutter")),
//增加图标和点击事件动作 Icons.list是icon的类型 onPressed添加点击事件 点击会执行_collectWordsPage方法
actions:<Widget>[
new IconButton(icon: const Icon(Icons.list),onPressed: _collectWordsPage),
],
),
body:_buildNormalWorlds(),
);
}
实际运行后,AppBar导航栏最右边的位置添加了一个图标,这时候点击没有任何反应,因为_collectWordsPage没有任何代码,下面添加跳转到新页面的实现。
//跳转新页面开始
void _collectWordsPage(){
Navigator.of(context).push(
);
}
toList
来转换。MaterialPageRoute
是一种模态路由,可以通过平台自适应来切换屏幕,Android而言,页面推送过渡向上滑动页面,淡入淡出,弹出过渡则是向下滑动页面。IOS而言,页面从右侧滑入,反向弹出,当另一个页面进入覆盖时,该页面向左移动。 //跳转新页面开始
void _collectWordsPage() {
Navigator.of(context).push(
new MaterialPageRoute<void>(
builder: (BuildContext context) {
//传递收藏后的单词
final Iterable<ListTile> tiles = _collected.map((WordPair pair) {
return new ListTile(
title: new Text(
pair.asPascalCase, //单词
),
);
},
);
},
),
);
}
Navigator.pop
,点击返回按钮会返回到主界面,代码如下://跳转新页面开始
void _collectWordsPage() {
Navigator.of(context).push(
new MaterialPageRoute<void>(
builder: (BuildContext context) {
//传递收藏后的单词
final Iterable<ListTile> tiles = _collected.map((WordPair pair) {
return new ListTile(
title: new Text(
pair.asPascalCase, //单词
),
);
},
);
//添加分割线开始
final List<Widget> divided = ListTile.divideTiles(
tiles: tiles,
context:context,
).toList();
return new Scaffold(
appBar: new AppBar(
title: new Center(child: const Text("Collect Words")),
),
body: new ListView(children: divided),
);
},
),
);
}
最终运行效果发现标题并不是居中对齐,因为默认的有边距,这时候需要自定义AppBar就可以,代码如下:
return new Scaffold(
appBar: new AppBar(
titleSpacing: 0.0,
//MaterialPageRoute这个自带了返回键 下面的属性设置取消返回键
automaticallyImplyLeading:false,
title: new Container(decoration: new BoxDecoration(color: new Color(0x00000000),
),
child:new Stack(
children: <Widget>[
new Container(
//左边位置
alignment: Alignment.centerLeft,
child:new IconButton(
//图标样式
icon:new Icon(Icons.arrow_back),
//点击事件
onPressed: (){
Navigator.pop(context);
}
),
),
new Center(child:new Text("Collect Words")),
],
),)
),
body: new ListView(children: divided),
);
最终运行效果如下,左边是IOS,右边是Android:
入门Flutter的第一天,简单知道了一下几点:
Widget
,它中间层只有C/C++代码,Flutter
使用Dart语言实现系统的绝大部分功能(布局,动画,手势)。若有错误,欢迎指正~