- Flutter 教程
- Flutter - 首頁
- Flutter - 簡介
- Flutter - 安裝
- 在 Android Studio 中建立簡單的應用程式
- Flutter - 架構應用程式
- Dart 程式語言簡介
- Flutter - Widget 簡介
- Flutter - 佈局簡介
- Flutter - 手勢簡介
- Flutter - 狀態管理
- Flutter - 動畫
- Flutter - 編寫 Android 特定程式碼
- Flutter - 編寫 iOS 特定程式碼
- Flutter - 包簡介
- Flutter - 訪問 REST API
- Flutter - 資料庫概念
- Flutter - 國際化
- Flutter - 測試
- Flutter - 部署
- Flutter - 開發工具
- Flutter - 編寫高階應用程式
- Flutter - 結論
- Flutter 有用資源
- Flutter - 快速指南
- Flutter - 有用資源
- Flutter - 討論
Flutter - 訪問 REST API
Flutter 提供了 http 包來使用 HTTP 資源。http 是一個基於 Future 的庫,並使用了 await 和 async 特性。它提供了許多高階方法,簡化了基於 REST 的移動應用程式的開發。
基本概念
http 包提供了一個高階類 http 來執行 Web 請求。
http 類提供執行所有型別 HTTP 請求的功能。
http 方法接受一個 url,並透過 Dart Map 傳遞其他資訊(釋出資料、附加頭等)。它向伺服器傳送請求,並在非同步/等待模式下收集響應。例如,以下程式碼從指定的 url 讀取資料並在控制檯中列印它。
print(await http.read('https://flutter.club.tw/'));
一些核心方法如下:
read - 透過 GET 方法請求指定的 url,並將響應作為 Future<String> 返回
get - 透過 GET 方法請求指定的 url,並將響應作為 Future<Response> 返回。Response 是一個包含響應資訊的類。
post - 透過 POST 方法請求指定的 url,釋出提供的資料,並將響應作為 Future<Response> 返回
put - 透過 PUT 方法請求指定的 url,並將響應作為 Future<Response> 返回
head - 透過 HEAD 方法請求指定的 url,並將響應作為 Future<Response> 返回
delete - 透過 DELETE 方法請求指定的 url,並將響應作為 Future<Response> 返回
http 還提供了一個更標準的 HTTP 客戶端類,client。client 支援持久連線。當需要向特定伺服器發出大量請求時,它將非常有用。它需要使用 close 方法正確關閉。否則,它類似於 http 類。示例程式碼如下:
var client = new http.Client();
try {
print(await client.get('https://flutter.club.tw/'));
}
finally {
client.close();
}
訪問產品服務 API
讓我們建立一個簡單的應用程式,從 Web 伺服器獲取產品資料,然後使用 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 包,如下所示:
dependencies: http: ^0.12.0+2
這裡,我們將使用 http 包的最新版本。Android Studio 將傳送一個包警報,指示 pubspec.yaml 已更新。
點選獲取依賴項選項。Android Studio 將從 Internet 獲取包併為應用程式正確配置它。
在 main.dart 檔案中匯入 http 包:
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 放置其中。
執行任何 Web 伺服器,並將 JSONWebServer 作為其根目錄,並獲取其 Web 路徑。例如,http://192.168.184.1:8000/products.json。我們可以使用任何 Web 伺服器,如 apache、nginx 等。
最簡單的方法是安裝基於 Node 的 http-server 應用程式。按照以下步驟安裝和執行 http-server 應用程式
安裝 Nodejs 應用程式 (nodejs.org)
轉到 JSONWebServer 資料夾。
cd /path/to/JSONWebServer
使用 npm 安裝 http-server 包。
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 類移動到其中。
在 Product 類中編寫一個工廠建構函式 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 伺服器獲取並載入產品資訊到 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 類的 fromMap 轉換為 List<Product>。
在 MyApp 類中,新增一個新的成員變數 products,型別為 Future<Product>,並在建構函式中包含它。
class MyApp extends StatelessWidget {
final Future<List<Product>> products;
MyApp({Key key, this.products}) : super(key: key);
...
在 MyHomePage 類中,新增一個新的成員變數 products,型別為 Future<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 小部件的 build 方法中的 home 選項(MyHomePage)以適應上述更改:
home: MyHomePage(title: 'Product Navigation demo home page', products: products),
更改 main 函式以包含 Future<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]),
),
);
},
);
},
);
}
}
請注意,我們使用了與導航應用程式中用於列出產品的相同概念,只是它被設計為一個單獨的小部件,透過傳遞型別為 List<Product> 的 products(物件)。
最後,修改 MyHomePage 小部件的 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 小部件來渲染小部件。FutureBuilder 將嘗試從其 future 屬性(型別為 Future<List<Product>>)中獲取資料。如果 future 屬性返回資料,它將使用 ProductBoxList 渲染小部件,否則丟擲錯誤。
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,而不是在編寫應用程式時輸入的本地靜態資料。