Flutter - 访问 REST API
Flutter 提供了 http package 来消费 HTTP 资源。http 是一个基于 Future 的库,使用 await 和 async 特性。它提供了许多高级方法,简化了基于 REST 的移动应用开发。
基本概念
http package 提供了一个高级 class http 来执行 web 请求。
http class 提供了执行所有类型 HTTP 请求的功能。
http methods 接受一个 url,以及通过 Dart Map 传递的附加信息(post data、附加 headers 等)。它会请求服务器并以 async/await 模式收集响应。例如,下面的代码从指定的 url 读取数据并在控制台打印。
print(await http.read('https://flutter.dev/'));
以下是一些核心方法:
read − 通过 GET method 请求指定的 url,并将响应作为 Future<String> 返回
get − 通过 GET method 请求指定的 url,并将响应作为 Future<Response> 返回。Response 是一个包含响应信息的 class。
post − 通过 POST method 请求指定的 url,发送提供的 data,并将响应作为 Future<Response> 返回
put − 通过 PUT method 请求指定的 url,并将响应作为 Future<Response> 返回
head − 通过 HEAD method 请求指定的 url,并将响应作为 Future<Response> 返回
delete − 通过 DELETE method 请求指定的 url,并将响应作为 Future<Response> 返回
http 还提供了一个更标准的 HTTP client class,client。client 支持持久连接。当需要向特定服务器发送大量请求时,它会很有用。它需要使用 close method 正确关闭。否则,它与 http class 类似。示例代码如下:
var client = new http.Client();
try {
print(await client.get('https://flutter.dev/'));
}
finally {
client.close();
}
访问 Product service API
让我们创建一个简单的应用,从 web server 获取产品数据,然后使用 ListView 显示产品。
在 Android Studio 中创建一个新的 Flutter 应用,命名为 product_rest_app。
将默认的启动代码 (main.dart) 替换为我们的 product_nav_app 代码。
从 product_nav_app 复制 assets 文件夹到 product_rest_app,并在 pubspec.yaml 文件中添加 assets。
flutter:
assets:
- assets/appimages/floppy.png
- assets/appimages/iphone.png
- assets/appimages/laptop.png
- assets/appimages/pendrive.png
- assets/appimages/pixel.png
- assets/appimages/tablet.png
按照以下方式在 pubspec.yaml 文件中配置 http package −
dependencies: http: ^0.12.0+2
这里,我们将使用最新版本的 http package。Android Studio 会发送一个包提醒,提示 pubspec.yaml 已更新。
点击 Get dependencies 选项。Android Studio 将从 Internet 获取包并为应用正确配置它。
在 main.dart 文件中导入 http package −
import 'dart:async'; import 'dart:convert'; import 'package:http/http.dart' as http;
创建一个新的 JSON 文件 products.json,包含产品信息,如下所示 −
[
{
"name": "iPhone",
"description": "iPhone is the stylist phone ever",
"price": 1000,
"image": "iphone.png"
},
{
"name": "Pixel",
"description": "Pixel is the most feature phone ever",
"price": 800,
"image": "pixel.png"
},
{
"name": "Laptop",
"description": "Laptop is most productive development tool",
"price": 2000,
"image": "laptop.png"
},
{
"name": "Tablet",
"description": "Tablet is the most useful device ever for meeting",
"price": 1500,
"image": "tablet.png"
},
{
"name": "Pendrive",
"description": "Pendrive is useful storage medium",
"price": 100,
"image": "pendrive.png"
},
{
"name": "Floppy Drive",
"description": "Floppy drive is useful rescue storage medium",
"price": 20,
"image": "floppy.png"
}
]
创建一个新的文件夹 JSONWebServer,并将 JSON 文件 products.json 放入其中。
运行一个以 JSONWebServer 为根目录的 web server,并获取其 web 路径。例如 http://192.168.184.1:8000/products.json。我们可以使用任何 web server,如 apache、nginx 等。
最简单的方法是安装基于 Node 的 http-server 应用。按照以下步骤安装并运行 http-server 应用。
安装 Nodejs 应用 (nodejs.org)
进入 JSONWebServer 文件夹。
cd /path/to/JSONWebServer
使用 npm 安装 http-server package。
npm install -g http-server
现在,运行服务器。
http-server . -p 8000 Starting up http-server, serving . Available on: http://192.168.99.1:8000 http://127.0.0.1:8000 Hit CTRL-C to stop the server
在 lib 文件夹中创建一个新文件 Product.dart,并将 Product class 移入其中。
在 Product class 中编写一个 factory 构造函数 Product.fromMap,用于将映射数据 Map 转换为 Product 对象。通常,JSON 文件将被转换为 Dart Map 对象,然后转换为相应的对象 (Product)。
factory Product.fromJson(Map<String, dynamic> data) {
return Product(
data['name'],
data['description'],
data['price'],
data['image'],
);
}
Product.dart 的完整代码如下 −
class Product {
final String name;
final String description;
final int price;
final String image;
Product(this.name, this.description, this.price, this.image);
factory Product.fromMap(Map<String, dynamic> json) {
return Product(
json['name'],
json['description'],
json['price'],
json['image'],
);
}
}
在主类中编写两个方法 − parseProducts 和 fetchProducts - 用于从 web server 获取并加载产品信息到 List<Product> 对象中。
List<Product> parseProducts(String responseBody) {
final parsed = json.decode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Product>((json) =>Product.fromJson(json)).toList();
}
Future<List<Product>> fetchProducts() async {
final response = await http.get('http://192.168.1.2:8000/products.json');
if (response.statusCode == 200) {
return parseProducts(response.body);
} else {
throw Exception('Unable to fetch products from the REST API');
}
}
请注意以下几点 −
Future 用于延迟加载产品信息。延迟加载是一种概念,即推迟代码执行直到必要时。
http.get 用于从 Internet 获取数据。
json.decode 用于将 JSON 数据解码为 Dart Map 对象。一旦 JSON 数据被解码,它将使用 Product class 的 fromMap 方法转换为 List<Product>。
在 MyApp class 中,添加一个新的成员变量 products,类型为 Future<List<Product>>,并在构造函数中包含它。
class MyApp extends StatelessWidget {
final Future<List<Product>> products;
MyApp({Key key, this.products}) : super(key: key);
...
在 MyHomePage class 中,添加一个新的成员变量 products,类型为 Future<List<Product>>,并在构造函数中包含它。同时,移除 items 变量及其相关方法,以及 getProducts 方法调用。将 products 变量放在构造函数中。这将允许在应用首次启动时仅从 Internet 获取一次产品。
class MyHomePage extends StatelessWidget {
final String title;
final Future<ListList<Product>> products;
MyHomePage({Key key, this.title, this.products}) : super(key: key);
...
修改 MyApp widget 的 build 方法中的 home 选项 (MyHomePage) 以适应上述更改 −
home: MyHomePage(title: 'Product Navigation demo home page', products: products),
修改 main 函数以包含 Future<List<Product>> 参数 −
void main() => runApp(MyApp(fetchProduct()));
创建一个新的 widget,ProductBoxList,用于在首页构建产品列表。
class ProductBoxList extends StatelessWidget {
final List<Product> items;
ProductBoxList({Key key, this.items});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return GestureDetector(
child: ProductBox(item: items[index]),
onTap: () {
Navigator.push(
context, MaterialPageRoute(
builder: (context) =gt; ProductPage(item: items[index]),
),
);
},
);
},
);
}
}
请注意,我们使用了 Navigation 应用中相同的产品列表概念,只是将其设计为一个单独的 widget,通过传递类型为 List<Product> 的 products (对象)。
最后,修改 MyHomePage widget 的 build 方法,使用 Future 选项而不是普通方法调用来获取产品信息。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Product Navigation")),
body: Center(
child: FutureBuilder<List<Product>>(
future: products, builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData ? ProductBoxList(items: snapshot.data)
// return the ListView widget :
Center(child: CircularProgressIndicator());
},
),
)
);
}
这里请注意,我们使用了 FutureBuilder widget 来渲染 widget。FutureBuilder 将尝试从其 future 属性(类型为 Future<List<Product>>)获取数据。如果 future 属性返回数据,它将使用 ProductBoxList 渲染 widget,否则抛出错误。
main.dart 的完整代码如下 −
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'Product.dart';
void main() => runApp(MyApp(products: fetchProducts()));
List<Product> parseProducts(String responseBody) {
final parsed = json.decode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Product>((json) => Product.fromMap(json)).toList();
}
Future<List<Product>> fetchProducts() async {
final response = await http.get('http://192.168.1.2:8000/products.json');
if (response.statusCode == 200) {
return parseProducts(response.body);
} else {
throw Exception('Unable to fetch products from the REST API');
}
}
class MyApp extends StatelessWidget {
final Future<List<Product>> products;
MyApp({Key key, this.products}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Product Navigation demo home page', products: products),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
final Future<List<Product>> products;
MyHomePage({Key key, this.title, this.products}) : super(key: key);
// final items = Product.getProducts();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Product Navigation")),
body: Center(
child: FutureBuilder<List<Product>>(
future: products, builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData ? ProductBoxList(items: snapshot.data)
// return the ListView widget :
Center(child: CircularProgressIndicator());
},
),
)
);
}
}
class ProductBoxList extends StatelessWidget {
final List<Product> items;
ProductBoxList({Key key, this.items});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return GestureDetector(
child: ProductBox(item: items[index]),
onTap: () {
Navigator.push(
context, MaterialPageRoute(
builder: (context) => ProductPage(item: items[index]),
),
);
},
);
},
);
}
}
class ProductPage extends StatelessWidget {
ProductPage({Key key, this.item}) : super(key: key);
final Product item;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(this.item.name),),
body: Center(
child: Container(
padding: EdgeInsets.all(0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.asset("assets/appimages/" + this.item.image),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(this.item.name, style:
TextStyle(fontWeight: FontWeight.bold)),
Text(this.item.description),
Text("Price: " + this.item.price.toString()),
RatingBox(),
],
)
)
)
]
),
),
),
);
}
}
class RatingBox extends StatefulWidget {
@override
_RatingBoxState createState() =>_RatingBoxState();
}
class _RatingBoxState extends State<RatingBox> {
int _rating = 0;
void _setRatingAsOne() {
setState(() {
_rating = 1;
});
}
void _setRatingAsTwo() {
setState(() {
_rating = 2;
});
}
void _setRatingAsThree() {
setState(() {
_rating = 3;
});
}
Widget build(BuildContext context) {
double _size = 20;
print(_rating);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 1
? Icon(Icons.star, ize: _size,)
: Icon(Icons.star_border, size: _size,)
),
color: Colors.red[500], onPressed: _setRatingAsOne, iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 2
? Icon(Icons.star, size: _size,)
: Icon(Icons.star_border, size: _size, )
),
color: Colors.red[500],
onPressed: _setRatingAsTwo,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 3 ?
Icon(Icons.star, size: _size,)
: Icon(Icons.star_border, size: _size,)
),
color: Colors.red[500],
onPressed: _setRatingAsThree,
iconSize: _size,
),
),
],
);
}
}
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.item}) : super(key: key);
final Product item;
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(2), height: 140,
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Image.asset("assets/appimages/" + this.item.image),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(this.item.name, style:TextStyle(fontWeight: FontWeight.bold)),
Text(this.item.description),
Text("Price: " + this.item.price.toString()),
RatingBox(),
],
)
)
)
]
),
)
);
}
}
最后运行应用查看结果。它将与我们的 Navigation 示例相同,只是数据来自 Internet 而非编码时输入的本地静态数据。