带你读《Flutter技术入门与实战》之二:Flutter基础知识-阿里云开发者社区

开发者社区> 华章出版社> 正文

带你读《Flutter技术入门与实战》之二:Flutter基础知识

简介: 本书的定位适合小白程序员,入门加实战,既有基础知识,又有丰富示例,包括详细的操作步骤,实操性强。由于Flutter大量使用组件,所以对组件的讲解很详细,包括基本概念、属性及代码示例。每个组件都配有小例子,力求精简,还提供了配套网站提供完整代码,复制完整代码就可以立即看到效果。在轻松掌握基础知识的同时快速进入实战。

点击查看第一章
点击查看第三章

第2章

Flutter基础知识
在创建了第一个Flutter程序后,我们还需要继续补充Flutter的基础知识。在后面的章节中讲解组件、布局、动画、装饰等时都需要用到这些基础知识。
本章将围绕以下几个知识点展开:
□入口程序
□Material Design
□Flutter主题
□无状态组件和有状态组件
□使用包资源
□Http请求

2.1 入口程序

每一个Flutter项目的/lib目录下都有一个main.dart文件,打开该文件,里面应该有一个main()函数。Flutter使用Dart语言开发,而在Dart语言中,main()函数是Dart程序的入口,也就是说,Flutter程序在运行的时候,第一个执行的函数就是main()函数。如下面的代码所示:
void main() => runApp(Widget app);
如果你是第一次接触Dart语言,可能会对上面的语法感到陌生,这是Dart语言特有的速写形式,将其展开后,完整代码如下所示:
void main() {
return runApp(Widget app);
}
从上面的代码中可以看到,main()函数中只调用runApp函数,使用runApp函数可以将给定的根组件填满整个屏幕。你可能会有疑问,为什么一定要使用runApp函数?如果不调用runApp函数,项目也可以正常执行,但是屏幕上什么都不会显示。Flutter是Dart语言的移动应用框架,runApp函数就是Flutter框架的入口,如果不调用runApp函数,那你执行的就是一个Dart控制台应用。更多关于Dart语言的细节,会在下面第3章“Dart语言简述”专门讲解。

2.2 Material Design设计风格

每一个.dart文件的第一行几乎都会导入flutter/material.dart包,这个包是Flutter实现Material Design设计风格的基础包,里面有文本输入框(Text)、图标(Icon)、图片(Image)、行排列布局(Row)、列排列布局(Column)、Decoration(装饰器)、动画等组件,大家可以将它们理解为网页中的按钮、标题、选项框等组件库。第一行代码如下所示:
import 'package:flutter/material.dart';
那么Material Design又是什么呢?是谷歌推出的一套视觉设计语言。比如有的App可以换皮肤,而每一套皮肤就是一种设计语言,有古典风、炫酷风、极简风,等等,而Material Design就是谷歌风。Flutter采用的就是Material Design风格。

2.3 Flutter主题

为了在整个应用中使用同一套颜色和字体样式,可以使用“主题”这种方式。定义主题有两种方式:全局主题,或使用Theme来定义应用程序局部的颜色和字体样式。事实上,全局主题只是由应用程序根MaterialApp创建的主题(Theme)。
定义一个主题后,就可以在我们自己的Widget中使用它,Flutter提供的Material Widgets将使用主题为AppBars、Buttons、Checkboxes等设置背景颜色和字体样式。

2.3.1 创建应用主题

创建主题的方法是将ThemeData提供给MaterialApp构造函数,这样就可以在整个应用程序中共享包含颜色和字体样式的主题。ThemeData的主要属性如表2-1所示。

image.png
image.png

如果没有提供主题,Flutter将创建一个默认主题。主题数据的示例代码如下:
new MaterialApp(
title: title,
theme: new ThemeData(

brightness: Brightness.dark,
primaryColor: Colors.lightBlue[800],
accentColor: Colors.cyan[600],

),
);

2.3.2 局部主题

如果我们想在应用程序的某一部分使用特殊的颜色,那么就需要覆盖全局的主题。有两种方法可以解决这个问题:创建特有的主题数据或扩展父主题。
1.创建特有的主题数据
实例化一个ThemeData并将其传递给Theme对象,代码如下:
new Theme(
//创建一个特有的主题数据
data: new ThemeData(

accentColor: Colors.yellow,

),
child: new FloatingActionButton(

onPressed: () {},
child: new Icon(Icons.add),

),
);
2.扩展父主题
扩展父主题时无须覆盖所有的主题属性,我们可以通过使用copyWith方法来实现,代码如下:
new Theme(
//覆盖accentColor为Colors.yellow
data: Theme.of(context).copyWith(accentColor: Colors.yellow),
child: new FloatingActionButton(

onPressed: null,
child: new Icon(Icons.add),

),
);

2.3.3 使用主题

主题定义好后就可以使用它了。首先,函数Theme.of(context)可以通过上下文来获取主题,方法是查找最近的主题,如果找不到就会找整个应用的主题。
下面来看一个简单的示例,应用的主题颜色定义为绿色,界面中间再加一个带有背景色的文本。
完整的例子代码如下所示:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
runApp(new MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {

final appName = '自定义主题';

return new MaterialApp(
  title: appName,
  theme: new ThemeData(
    brightness: Brightness.light,//应用程序整体主题的亮度
    primaryColor: Colors.lightGreen[600],//App主要部分的背景色
    accentColor: Colors.orange[600],//前景色(文本、按钮等)
  ),
  home: new MyHomePage(
    title: appName,
  ),
);

}
}

class MyHomePage extends StatelessWidget {
final String title;

MyHomePage({Key key, @required this.title}) : super(key: key);

@override
Widget build(BuildContext context) {

return new Scaffold(
  appBar: new AppBar(
    title: new Text(title),
  ),
  body: new Center(
    child: new Container(
      //获取主题的accentColor
      color: Theme.of(context).accentColor,
      child: new Text(
        '带有背景颜色的文本组件',
        style: Theme.of(context).textTheme.title,
      ),
    ),
  ),
  floatingActionButton: new Theme(
    //使用copyWith的方式获取accentColor
    data: Theme.of(context).copyWith(accentColor: Colors.grey),
    child: new FloatingActionButton(
      onPressed: null,
      child: new Icon(Icons.computer),
    ),
  ),
);

}
}
自定义主题的效果如图2-1所示。

image.png

2.4 无状态组件和有状态组件

无状态组件(StatelessWidget)是不可变的,这意味着它们的属性不能改变,所有的值都是最终的。
有状态组件(StatefulWidget)持有的状态可能在Widget生命周期中发生变化。实现一个StatefulWidget至少需要两个类:
□一个StatefulWidget类。
□一个State类。StatefulWidget类本身是不变的,但是State类在Widget生命周期中始终存在。
Flutter的官方给出一个有状态组件的示例,点击右下角的+号按钮,应用界面中间的数字会加1,如图2-2所示。
这个示例有几个关键的部分,解析如下。
示例代码中MyHomePage必须继承自StatefulWidget类,如下所示:

image.png

class MyHomePage extends StatefulWidget
重写createState方法,如下所示:
@override
MyHomePageState createState() => new _MyHomePageState();
状态类必须继承自State类,如下所示:
class _MyHomePageState extends State
定义一个普通变量_counter作为计数器变量,调用setState方法来控制这个变量的值的变化,如下所示:
int _counter = 0;

void _incrementCounter() {
setState(() {

//计数器变量
_counter++;

});
}
完整的示例代码如下所示:
import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

//MyApp不需要做状态处理,所以此组件继承StatelessWidget即可
class MyApp extends StatelessWidget {
// 这个组件是整个应用的主组件
@override
Widget build(BuildContext context) {

return new MaterialApp(
  title: 'Flutter Demo',
  theme: new ThemeData(
    //自定义主题
    primarySwatch: Colors.blue,
  ),
  home: new MyHomePage(title: 'Flutter Demo Home Page'),
);

}
}

//主页需要继承自StatefulWidget
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

//标题
final String title;

//必须重写createState方法
@override
_MyHomePageState createState() => new _MyHomePageState();
}
//状态类必须继承State类,注意后面需要指定为
class _MyHomePageState extends State {
int _counter = 0;//计数器

void _incrementCounter() {

//调用State类里的setState方法来更改状态值,使得计数器加1
setState(() {
  //计数器变量,每次点击让其加1
  _counter++;
});

}

@override
Widget build(BuildContext context) {

return new Scaffold(
  appBar: new AppBar(
    title: new Text(widget.title),
  ),
  //居中布局
  body: new Center(
    
    //垂直布局
    child: new Column(
      //主轴居中对齐
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        new Text(
          'You have pushed the button this many times:',
        ),
        new Text(
          '$_counter',//绑定计数器的值
          style: Theme.of(context).textTheme.display1,
        ),
      ],
    ),
  ),
  floatingActionButton: new FloatingActionButton(
    onPressed: _incrementCounter,//按下+号按钮调用自增函数
    tooltip: 'Increment',
    child: new Icon(Icons.add),
  ), 
);

}
}

2.5 使用包资源

Flutter包类似于Java语言里的jar包,由全球众多开发者共同提供第三方库。例如,网络请求(http)、自定义导航/路由处理(fluro)、集成设备API(如url_launcher&battery) 以及第三方平台SDK(如Firebase)等。这使得开发者可以快速构建应用程序,而无须从头造轮子。
1.包仓库
所有包(package)都会发布到Dart的包仓库里,如图2-3所示,输入你想使用的包后点击搜索即可。
提示:包仓库地址为:https://pub.dartlang.org

2.包使用示例
接下来使用url_launcher这个包来详解讲解第三方包的使用,步骤如下。
步骤1:打开pubspec.yaml文件,在dependencies下添加包的名称及版本,如图2-4箭头指向的内容所示。

image.png

image.png

步骤2:点击Packages get命令来获取工程配置文件中所添加的引用包,或者打开命令行窗口执行flutter packages get命令,如图2-5所示。
注意:在更新包资源的过程中注意观察控制台消息,可能有版本错误、网络问题,这些都会导致更新失败。

image.png


步骤3:打开main.dart文件,导入url_launcher.dart包:
import 'package:url_launcher/url_launcher.dart';
步骤4:这时就可以使用launch方法来打开url地址了:
const url = 'https://www.baidu.com';
launch(url);
完整的main.dart代码如下所示:
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {

return new MaterialApp(
  title: '使用第三方包示例',
  home: new Scaffold(
    appBar: new AppBar(
      title: new Text('使用第三方包示例'),
    ),
    body: new Center(
      child: new RaisedButton(
        onPressed: () {
          //指定url并发起请求
          const url = 'https://www.baidu.com';
          launch(url);
        },
        child: new Text('打开百度'),
      ),
    ),
  ),
);

}
}
步骤5:启动示例,打开界面如图2-6所示。
点击“打开百度”按钮,页面会跳转至百度页面,如图2-7 所示。

image.png

2.6 Http请求

HTTP协议通常用于做前后端的数据交互。Flutter请求网络有两种方法,一种是用Http请求,另一种是用HttpClient请求。
1. Http请求方式
在使用Http方式请求网络时,需要导入http包。如下所示:
import 'package:http/http.dart' as http;
请看下面的完整示例代码,示例中发起了一个http的get请求,并将返回的结果信息打印到控制台里:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {

return new MaterialApp(
  title: 'http请求示例',
  home: new Scaffold(
    appBar: new AppBar(
      title: new Text('http请求示例'),
    ),
    body: new Center(
      child: new RaisedButton(
        onPressed: () {

          var url = 'http://httpbin.org/';
          //向http://httpbin.org/发送get请求
          http.get(url).then((response) {
            print("状态: ${response.statusCode}");
            print("正文: ${response.body}");
          });

        },
        child: new Text('发起http请求'),
      ),
    ),
  ),
);

}
}
请求界面如图2-8所示。

image.png


点击“发起http请求”按钮,程序开始请求指定的url,如果服务器正常返回数据,则状态码为200。控制台输出内容如下:
Performing hot reload...
Syncing files to device iPhone X...
Reloaded 1 of 509 libraries in 452ms.
flutter: 状态: 200
flutter: 正文:

image.png

注意:服务器返回状态200,同时返回正文。完整的正文远不只这些内容,你可以自己测试此示例,并查看控制台的输出消息。

2. HttpClient请求方式
在使用HttpClient方式请求网络时,需要导入io及convert包,如下所示:
import 'dart:convert';
import 'dart:io';
请看下面的完整示例代码,示例中使用HttpClient请求了一条天气数据,并将返回的结果信息打印到控制台里。具体请求步骤看代码注释即可。
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:io';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {

//获取天气数据
void getWeatherData() async {

try {
  //实例化一个HttpClient对象
  HttpClient httpClient = new HttpClient();

  //发起请求
  HttpClientRequest request = await httpClient.getUrl(
      Uri.parse("http://t.weather.sojson.com/api/weather/city/101030100"));

  //等待服务器返回数据
  HttpClientResponse response = await request.close();

  //使用utf8.decoder从response里解析数据
  var result = await response.transform(utf8.decoder).join();
  //输出响应头
  print(result);

  //httpClient关闭
  httpClient.close();

} catch (e) {
  print("请求失败:$e");
} finally {

}

}

@override
Widget build(BuildContext context) {

return MaterialApp(
  title: 'httpclient请求',
  home: Scaffold(
    appBar: AppBar(
      title: Text('httpclient请求'),
    ),
    body: Center(
      child: RaisedButton(
        child: Text("获取天气数据"),
        onPressed: getWeatherData,
      ),
    ),
  ),
);

}
}
请求界面如图2-9所示。

image.png

点击“获取天气数据”按钮,程序开始请求指定的url,如果服务器正常返回数据,则状态码为200。控制台输出内容如下:
Performing hot reload...
Syncing files to device iPhone X...
Reloaded 1 of 419 libraries in 412ms.
flutter: {"time":"2018-11-30 08:09:00","cityInfo":{"city":"天津市","cityId":
"101030100","parent":"天津","updateTime":"07:56"},"date":"20181130","message":
"Success!", "status":200,"data":{"shidu":"84%","pm25":30.0,"pm10":79.0,"quality":"良","wendu":"1","ganmao":"极少数敏感人群应减少户外活动","yesterday":{"date":"29日星期四","sunrise":"07:08","high":"高温 9.0℃","low":"低温 0.0℃","sunset":"16:50","aqi":86.0,"fx":"东风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"}, "forecast": [{"date":"30日星期五","sunrise":"07:09","high":"高温 10.0℃","low":"低温 1.0℃","sunset":"16:50","aqi":53.0,"fx":"西风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"01日星期六","sunrise":"07:10","high":"高温 10.0℃","low":"低温4.0℃", "sunset":"16:49","aqi":94.0,"fx":"东风","fl":"<3级","type":"阴","notice":"不要被阴云遮挡住好心情"},{"date":"02日星期日","sunrise":"07:11","high":"高温 1<…>
注意:返回的数据是JSON格式,所以后续还需要做JSON处理。另外还需要使用utf8.decoder从response里解析数据。

如果请求里需要带参数,可以在URI里增加查询参数,具体的请求地址和参数要根据实际需要编写,代码如下所示:
Uri uri=Uri(scheme: "https", host: "t.weather.sojson.com", queryParameters: {

"_id": 26,
"city_code": "101030100",//接口需要的city_code
"city_name": "天津"

});

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接