上篇文章从原生开发到Flutter教程(一)认识Flutter我们已经大概了解了Flutter
的魅力并搭建好了开发环境,终于到了大展身手的时候了。
接下来我们来做一个App,是央视新闻客户端。
带着这个例子,功能挺齐全,相信大家学完这套教程,应对日常的开发应该就不会有大的问题了。但是除了学会写项目,笔者觉得,更重要的是,我们通过这个例子,一起来领略一下Google出品的沥血之作其中的奥妙,体会Google工程师对于一些问题的解决方案的理念,如UI构建、数据流传输、用户交互、数据异步处理等等。话不多说,Let's Get Started.
项目初始化
注意:我的开发工具是VSCode。
项目的初始化很简单,Shift + Cmd + p
,选择Flutter: New Project
,然后写上项目名称(比如cctv_news),再选择一个放置文件夹即可。
接下来,VSCode会自动初始化项目,等待大概10s即可完成。
了解文件夹构成
项目初始化好后,会看到一堆文件夹和文件,如果之前很少接触Flutter,对这些文件可能会比较陌生。其实很简单,下面我来简述一下文件夹构成。
- ios、android
这两个文件夹望文知义,就是iOS、Android的工程文件夹。可以在里面写一些原生代码,如OC/Swift/Java/Kotlin等,做一些原生交互。 - lib
这里存放的是Dart语言编写的代码,这里是核心代码。我们做Flutter开发的大概98%的时间都是在这个文件夹下书写代码。我们可以在这个lib目录下面创建不同的文件夹,里面存放不同的文件。 - pubspec.yaml
看着些许陌生,但是很简单。它就是配置依赖项的地方,比如配置远程pub仓库的依赖库,或者指定本地资源(图片、字体、音频、视频等),有点类似iOS中的Podfile
,当然比后者功能更强大。 - test
测试文件 - build
存储iOS和Android构建文件夹。
编码开始之前
万丈高楼平地起,如果想快速上手写Flutter项目,下面几个概念一定要先熟悉一下,磨刀不误砍柴工。
1、Widget
在Flutter
中,万物皆Widget
。
如果你了解React、VUE
等,这个概念不难理解。如果你从iOS过来的,Widget
很像UIView
,但是绝对不能等同。Flutter
中的Widget
非常轻量,他们本身不是什么控件,也不会被直接绘制出什么,他们只是UI的描述,即"声明和构建UI的方法"。一定要理解这个概念,否则后面你会产生类似"App为什么继承自Widget"这样的困惑。
StatelessWidget和StatefulWidget
Widget
分为两种,StatelessWidget
和StatefulWidget
。
我们自定义控件大多继承自两者之一。他们的区别是,前者没有state
状态的概念,而后者有。
- StatelessWidget
继承自StatelessWidget控件都是无状态的,不需状态管理,非常高效。有个必须重写的方法build
,在这个方法中返回创建的Widget
控件即可。 - StatefulWidget
继承自StatelessWidget控件都是有状态的。既然是有状态的,肯定得有个state
对象。没错,在这个类中,必须重写一个方法返回自己的state
对象,即createState
方法。而在这个state
类中,实现build
方法返回需要的控件,然后在用户操作的时候,调用setState
即可完成数据源改变页面刷新。
另外值得注意的是state的生命周期:- 1、initState :初始化,理论上只有初始化一次。
- 2、didChangeDependencies:在 initState 之后调用,此时可以获取其他 State 。
- 3、dispose :销毁,只会调用一次。
下面以StatefulWidget
为例,它有两个类组成,即widget
本身和他的状态state
。看下面代码实例:
小提示:stl和stf是创建
StatelessWidget
和StatefulWidget
的快捷键。
class Counter extends StatefulWidget {
Counter({Key key, this.title}) : super(key: key);
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int counter = 0;
void increaseCount() {
setState(() {
this.counter++;
}
}
Widget build(context) {
return RaisedButton(
onPressed: increaseCount,
child: new Text('Tap to Increase'),
);
}
}
2、Material 和 Cupertino Widgets
上篇文章也谈到了这两个概念。Flutter之所以可以快速构建精美的页面,离不开这两个内建widget库。前者是安卓原生风格,后者是仿苹果风格。
3、常见控件
下面列一下常见的控件,为后面铺垫。
- Text - 文字控件。类似UILabel
- Image - 图片控件。类似UIImageView
- ListView - 列表视图。类似UITableView
- Icon - 图标控件。用来展示Material 和 Cupertino Widgets内建库的图标。
- Container - 容器控件。可以为子控件添加padding, alignment, backgrounds等。
- TextInput - 文字输入控件。类似UITextfield
- Row, Column - 水平、垂直布局控件容器。类似于CSS3的Flex布局。
- Stack - 层叠布局控件。这个控件对于原生开发人员来说比较陌生,flutter中如果想布局一个控件压在另一个控件上面,就用这个控件。
- Scaffold - 内建页面控件。提供了navigations, appBars, back buttons等。
简单分析页面
先来简单分析一下央视新闻首页。
上面的内容比较多,我们先做最简单的iOS中的TableView视图,里面有多个Cell构成。
下面我们开始正式进入代码阶段。
(PS:由于笔者是主栈iOS开发的,所以一些名词术语暂以最熟悉的iOS平台的术语为准)
上文也提到了,我们以后大部分开发时间,都是在
lib
文件夹下。我们打开这个文件夹,发现里面有个main.dart
文件,这是程序的入口文件,稍微懂点编程的都知道其作用。打开这个文件,发现已经存在示例代码,这是Flutter官方写的范例,你可以运行一下看看效果。当然,我们后面开始写代码之前,最好把他们清空,完全从0开始。
开始写代码
实现main.dart
- 1、我们清空
main.dart
代码,开始写属于自己的第一行dart代码。 - 2、先将
material.dart
引入进来,这是Flutter提供的安卓原生控件库,我们可以直接基于他们快速开发出精美的页面。
import 'package:flutter/material.dart';
- 3、实现
main
函数
main函数即程序入口,我们来实现main函数,实现内建函数runApp
,该函数需要一个参数,是一个Widget,runApp
会将这个Widget渲染到用户的屏幕。所以,我们的传入自己创建的App实例对象即可。
注意,当函数体只有一行代码时,我们可以用胖箭头
=>
代替花括号,语法更简洁。
void main() => runApp(MainApp());
- 4、创建App类。我们可以随便命名自己的App类名称,这里叫
MainApp
,注意,他是继承自StatelessWidget
,上文已经解释过,在Flutter世界中,万物皆Widget。
我们知道,StatelessWidget
必须重写实现一个方法,即build
,返回一个Widget用于展示,我们在这个方法中,返回一个MaterialApp
对象,他即是使用Material
风格的App类。里面有一些很有用的属性,如title
、home
、theme
、routes
等,基本上都可以望文知意。
注意,我们在home
属性传入的是一个Scaffold
实例,他即是一个脚手架,可以简单把它当做我们的Material
风格控件的容器,提供一些很有用的属性,如appBar
、body
等。body
即是我们放置我们写的控件的地方。我们可以简单写个控件运行看看效果,如body: Text('CCTV News')
。
main.dart完整代码如下:
import 'package:flutter/material.dart';
void main() => runApp(MainApp());
class MainApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'CCTV NRES',
home: Scaffold(
appBar: AppBar(
title: Text('CCTV News'),
),
body: Text('欢迎来到Flutter~'),
),
);
}
}
运行后会发现,'欢迎来到Flutter~'文字居于左上角,我们现在想将这段文字屏幕居中,跟我们之前原生开始的布局逻辑不同,Flutter是通过Widget
来完成布局,关于布局的知识我们会在后面详细讲,这里只需要知道,要想居中控件,我们可以用Center
控件包裹一下即可。
修改body
属性如下:body: Center(child: Text('Test')),
,输入r
热加载一下,即可看到文字展示在中间了。
创建卡片视图(TableViewCell)
有了上面的铺垫,我们就可以正式开始写App了。先从最简单的入手,先实现一下新闻详情的列表的Cell的样式。
如上图,Cell里面的内容,是由两大部分构成,Row和Column。再次强调一下,Flutter的布局理念跟原生的Layout的概念完全不一样。形象点比喻就是,Flutter的布局就像装集装箱,先将一堆东西按照想要的规则放在一个盒子里,在将这个盒子按照想要的规则放在更大的盒子里面。好了,我们开始写代码,先创建
home
文件夹和HomeNewsCell.dart
文件,如下图:
-lib/home
-lib/home/HomeNewsCell.dart
进入HomeNewsCell.dart
文件中开始写Cell
视图。这里我会分析得细一些,后面文章我们就快一些了。下面这样一层一层分析:
- 由于内容承载视图和分割线属于上下布局,所以需要先用
Column
布局。 - 内容承载视图,首先是左右布局,左边是标题和'听新闻'按钮视图,右边是新闻图片视图,所以用
Row
布局。 - 内容承载视图的左半边,由于是上下布局,上面标题下面按钮,所以又得使用
Column
布局。
即 Column > Row > Column
。
注意,这里有几个需要注意的地方:
- 文字撑开布局使用是
Expended
控件包裹 - 加载本地资源图片,需要先将图片拖入
images
文件夹中,然后在pubspec.yaml
中配置上才能使用。
assets:
- images/news_image.jpg
其实整个布局比较基础,但是大家要通过这个简单的布局理解Flutter
的布局思想,理解透了思想,再复杂的布局,也可以拆解成简单的单元。代码如下:
import 'package:flutter/material.dart';
class HomeNewsCell extends StatelessWidget {
Widget get _cellContentView {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'继山东编导艺考联考被曝疑似出现泄题和作弊的情况。江西编导艺考联考也被曝疑似出现泄题和作弊的情况。',
style: TextStyle(
fontSize: 15.0,
color: Color(0xff111111),
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
Container(
width: 50.0,
height: 20.0,
margin: EdgeInsets.only(top: 6.0),
child: ButtonTheme(
buttonColor: Color(0xff1C64CF),
shape: StadiumBorder(),
child: RaisedButton(
onPressed: () => print('test'),
padding: EdgeInsets.all(2.0),
child: Text(
'听新闻',
style: TextStyle(
color: Colors.white,
fontSize: 11.0,
fontWeight: FontWeight.w300),
),
),
),
),
],
),
),
SizedBox(
width: 10.0,
),
Container(
height: 85.0,
width: 115.0,
margin: EdgeInsets.only(top: 3.0),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(5.0),
image: DecorationImage(
image: AssetImage('images/news_image.jpg'),
fit: BoxFit.cover,
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Container(
height: 115.0,
child: Column(
children: <Widget>[
// 内容视图
Container(
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 6.0),
child: _cellContentView,
),
// 分割线
Container(
margin: EdgeInsets.only(top: 4.0),
color: Color(0xffeaeaea),
constraints: BoxConstraints.expand(height: 4.0),
)
],
),
);
}
}
展示HomeNewsCell样式
上面已经写好了Cell
的布局,我们现在改写一下main.dart
,加载看一下Cell
的样式。很简单,先将HomeNewsCell.dart
引入进来后,改写body
属性成Column
,即可完成渲染。
...
body: Column(
children: <Widget>[
HomeNewsCell(),
HomeNewsCell(),
],
),
...
flutter run
看一下效果,是不是很惊喜,这么快时间,就写好了横跨iOS/Android
平台的代码,效果还不错,如下图:
使用ListView渲染列表
上面我们是把Cell放在了Column
里面,但是在实际开发场景中,我们需要放在ListView
里面,这样就可以多了很多如下拉刷新、滑动加载、阻尼效果等等的功能。下面我们继续改造main.dart
,加入ListView
。
同上,改写body
属性如下:
body: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return HomeNewsCell();
},
)),
使用ListView.builder
很简单,itemCount
是cell的个数,相当于iOS中TableView
的numberOfRowsInSection
,itemBuilder
是一个回调函数,需要外界告知需要渲染的Cell的样式,即相当于iOS中的cellForRowAtIndexPath
。还有一些其他的属性,我们后面再介绍。
好了,改造完成后,flutter run
一下,可以滑动了,效果还不错,见下图:
总结
本节教程,抛砖引玉,完成了基础的新闻列表页的布局及展示,我们也了解到了Flutter
的布局跟原生布局的思想的差异,其实,跟原生布局比起来,Flutter
的这种布局方式刚一开始可能会觉得有些笨拙,但是写顺手之后会发现,这种堆积木、集装箱式的布局构建方式,另外配合上Flutter
的数据绑定、响应式编程,这种方式写起来更得心应手,水到渠成。
当然,从上面列表页的小例子我们也不难很快就能发现其中的缺憾,这种UI构建方式,稍不注意,就很容易造成代码冗长,各种括号,各种回车等。颇有在写标记语言的的韵味,当然,注意好组件抽象封装隔离,就能在一定程度上很好避免上述问题。
下篇教程,我们会搭建Tab
主UI框架,自定义组件,另外,本篇教程使用的是假数据,下篇教程,我们会请求网络真实数据,来体验一下,Dart
作为优秀的现代化语言,异步任务处理是怎么样的一番景象。