Flutter - การเข้าถึง REST API

Flutter จัดเตรียมแพ็กเกจ http เพื่อใช้ทรัพยากร HTTP http เป็นไลบรารีในอนาคตและใช้คุณสมบัติรอและไม่ซิงค์ มีวิธีการระดับสูงมากมายและลดความยุ่งยากในการพัฒนาแอพพลิเคชั่นมือถือที่ใช้ REST

แนวคิดพื้นฐาน

แพคเกจ http มีคลาสระดับสูงและ http เพื่อทำคำขอทางเว็บ

  • คลาส http มีฟังก์ชันเพื่อดำเนินการร้องขอ HTTP ทุกประเภท

  • วิธีการ http ยอมรับ url และข้อมูลเพิ่มเติมผ่าน Dart Map (ข้อมูลโพสต์ส่วนหัวเพิ่มเติม ฯลฯ ) มันร้องขอเซิร์ฟเวอร์และรวบรวมการตอบกลับในรูปแบบ async / await ตัวอย่างเช่นโค้ดด้านล่างอ่านข้อมูลจาก url ที่ระบุและพิมพ์ในคอนโซล

print(await http.read('https://flutter.dev/'));

บางส่วนของวิธีการหลักมีดังนี้ -

  • read - ขอ url ที่ระบุผ่านเมธอด GET และส่งคืนการตอบกลับเป็น Future <String>

  • get- ขอ url ที่ระบุผ่านเมธอด GET และส่งคืนการตอบกลับเป็น Future <Response> การตอบกลับเป็นคลาสที่เก็บข้อมูลการตอบกลับ

  • post - ขอ url ที่ระบุผ่านวิธี POST โดยการโพสต์ข้อมูลที่ให้มาและส่งคืนการตอบกลับเป็นอนาคต

  • put - ขอ url ที่ระบุผ่านวิธี PUT และส่งคืนการตอบกลับเป็น Future <Response>

  • head - ขอ url ที่ระบุผ่าน HEAD method และส่งคืนการตอบกลับเป็น Future <Response>

  • delete - ขอ url ที่ระบุโดยใช้เมธอด DELETE และส่งคืนการตอบกลับเป็น Future <Response>

http ยังมีคลาสไคลเอ็นต์ HTTP มาตรฐานเพิ่มเติมไคลเอนต์ ไคลเอนต์รองรับการเชื่อมต่อแบบต่อเนื่อง จะมีประโยชน์เมื่อมีการร้องขอจำนวนมากไปยังเซิร์ฟเวอร์ใดเซิร์ฟเวอร์หนึ่ง ต้องปิดอย่างถูกต้องโดยใช้วิธีปิด มิฉะนั้นจะคล้ายกับคลาส http โค้ดตัวอย่างมีดังนี้ -

var client = new http.Client(); 
try { 
   print(await client.get('https://flutter.dev/')); 
} 
finally { 
   client.close(); 
}

การเข้าถึง Product service API

ขอให้เราสร้างโปรแกรมที่ง่ายที่จะได้รับข้อมูลผลิตภัณฑ์จากเว็บเซิร์ฟเวอร์แล้วแสดงผลิตภัณฑ์ที่ใช้ListView

  • สร้างใหม่Flutterการประยุกต์ใช้ในสตูดิโอของ Android, product_rest_app

  • แทนที่รหัสเริ่มต้นเริ่มต้น (main.dart) ด้วยรหัสproduct_nav_appของเรา

  • คัดลอกโฟลเดอร์ assets จากproduct_nav_appไปยังproduct_rest_appและเพิ่มเนื้อหาภายในไฟล์ pubspec.yaml

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
  • กำหนดค่าแพ็กเกจ http ในไฟล์ pubspec.yaml ดังที่แสดงด้านล่าง -

dependencies: 
   http: ^0.12.0+2
  • ในที่นี้เราจะใช้แพ็กเกจ http เวอร์ชันล่าสุด Android studio จะส่งการแจ้งเตือนแพ็กเกจว่ามีการอัปเดต pubspec.yaml

  • คลิกตัวเลือกรับการอ้างอิง Android studio จะได้รับแพ็คเกจจากอินเทอร์เน็ตและกำหนดค่าให้เหมาะสมกับแอปพลิเคชัน

  • นำเข้าแพ็กเกจ http ในไฟล์ main.dart -

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 เป็นไดเร็กทอรีรูทและรับเส้นทางเว็บ ตัวอย่างเช่น http://192.168.184.1:8000/products.json เราสามารถใช้เว็บเซิร์ฟเวอร์ใดก็ได้เช่น apache, nginx เป็นต้น

  • วิธีที่ง่ายที่สุดคือการติดตั้งแอ็พพลิเคชันเซิร์ฟเวอร์ http ที่ใช้โหนด ทำตามขั้นตอนด้านล่างเพื่อติดตั้งและเรียกใช้แอ็พพลิเคชันเซิร์ฟเวอร์ http

    • ติดตั้งแอปพลิเคชันNodejs ( nodejs.org )

    • ไปที่โฟลเดอร์ JSONWebServer

cd /path/to/JSONWebServer
  • ติดตั้งแพ็กเกจ http-server โดยใช้ npm

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
  • สร้างไฟล์ใหม่ Product.dart ในโฟลเดอร์ lib และย้ายคลาส Product เข้าไป

  • เขียนตัวสร้างโรงงานในคลาส Product, Product.fromMap เพื่อแปลงแผนที่ข้อมูลที่แมปลงในวัตถุผลิตภัณฑ์ โดยปกติไฟล์ JSON จะถูกแปลงเป็นวัตถุ Dart Map จากนั้นแปลงเป็นวัตถุที่เกี่ยวข้อง (ผลิตภัณฑ์)

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 - ในคลาสหลักเพื่อดึงข้อมูลและโหลดข้อมูลผลิตภัณฑ์จากเว็บเซิร์ฟเวอร์ลงในรายการ <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 ใช้เพื่อดึงข้อมูลจากอินเทอร์เน็ต

    • json.decode ใช้เพื่อถอดรหัสข้อมูล JSON ลงในวัตถุ Dart Map เมื่อถอดรหัสข้อมูล JSON แล้วข้อมูลจะถูกแปลงเป็นรายการ <Product> โดยใช้ fromMap ของคลาสผลิตภัณฑ์

    • ในคลาส MyApp ให้เพิ่มตัวแปรสมาชิกใหม่ผลิตภัณฑ์ประเภท Future <Product> และรวมไว้ในตัวสร้าง

class MyApp extends StatelessWidget { 
   final Future<List<Product>> products; 
   MyApp({Key key, this.products}) : super(key: key); 
   ...
  • ในคลาส MyHomePage ให้เพิ่มผลิตภัณฑ์ตัวแปรสมาชิกใหม่ประเภท Future <Product> และรวมไว้ในตัวสร้าง ลบตัวแปรรายการและวิธีการที่เกี่ยวข้องเรียกเมธอด getProducts การวางตัวแปรผลิตภัณฑ์ในตัวสร้าง จะอนุญาตให้ดึงผลิตภัณฑ์จากอินเทอร์เน็ตเพียงครั้งเดียวเมื่อแอปพลิเคชันเริ่มต้นครั้งแรก

class MyHomePage extends StatelessWidget { 
   final String title; 
   final Future<ListList<Product>> products; 
   MyHomePage({Key key, this.title, this.products}) : super(key: key); 
   ...
  • เปลี่ยนตัวเลือกโฮม (MyHomePage) ในวิธีการสร้างของวิดเจ็ต MyApp เพื่อรองรับการเปลี่ยนแปลงข้างต้น -

home: MyHomePage(title: 'Product Navigation demo home page', products: products),
  • เปลี่ยนฟังก์ชันหลักเพื่อรวมอาร์กิวเมนต์ <Product> ในอนาคต -

void main() => runApp(MyApp(fetchProduct()));
  • สร้างวิดเจ็ตใหม่ 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]), 
                     ), 
                  ); 
               }, 
            ); 
         }, 
      ); 
   } 
}

โปรดทราบว่าเราใช้แนวคิดเดียวกับที่ใช้ในแอปพลิเคชันการนำทางเพื่อแสดงรายการผลิตภัณฑ์ยกเว้นว่าจะออกแบบเป็นวิดเจ็ตแยกต่างหากโดยส่งผลิตภัณฑ์ (วัตถุ) ประเภทรายการ <ผลิตภัณฑ์>

  • สุดท้ายแก้ไขวิธีการสร้างวิดเจ็ตMyHomePageเพื่อรับข้อมูลผลิตภัณฑ์โดยใช้ตัวเลือก 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 <List <Product>>) หากคุณสมบัติในอนาคตส่งคืนข้อมูลจะแสดงวิดเจ็ตโดยใช้ 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(), 
                           ], 
                        )
                     )
                  )
               ]
            ), 
         )
      ); 
   } 
}

สุดท้ายเรียกใช้แอปพลิเคชันเพื่อดูผลลัพธ์ จะเหมือนกับตัวอย่างการนำทางของเรายกเว้นข้อมูลที่มาจากอินเทอร์เน็ตแทนที่จะเป็นข้อมูลแบบคงที่ที่ป้อนขณะเข้ารหัสแอปพลิเคชัน