Flutter開發系列教程之網路請求

xiangzhihong發表於2019-07-31

簡介

Http網路請求是一門開發語言裡比較常用和重要的功能,主要用於資源訪問、介面資料請求和提交、上傳下載檔案等等操作,Http請求方式主要有:GET、POST、HEAD、PUT、DELETE、TRACE、CONNECT、OPTIONS。本文主要GET和POST這兩種常用請求在Flutter中的用法,其中對POST將進行著重講解。Flutter的Http網路請求的實現主要分為三種:io.dart裡的HttpClient、Dart原生http請求和第三方庫實現。

Http網路請求是網際網路開發的基礎協議,Http支援的請求方式有:GET、POST、HEAD、PUT、DELETE、TRACE、CONNECT、OPTIONS這八種。

GET請求

GET請求主要是執行獲取資源操作的,例如通過URL從伺服器獲取返回的資源,其中GET可以把請求的一些引數資訊拼接在URL上,傳遞給伺服器,由伺服器端進行引數資訊解析,伺服器收到請求後返回相應的資源給請求者。注意:GET請求拼接的URL資料大小和長度是有最大限制的,傳輸的資料量一般限制在2KB。

POST請求

POST請求主要用於執行提交資訊、請求資訊等操作,相比GET請求,POST請求的可以攜帶更多的資料,而且格式不限,如JSON、XML、文字等等都支援。並且POST傳遞的一些資料和引數不是直接拼接在URL後的,而是放在Http請求Body裡,相對GET來說比較安全。並且傳遞的資料大小和格式是無限制的。 POST請求方式是一種比較常用網路請求方式,通常由請求頭(header)和請求體(body)兩部分組成。POST請求常見的請求體(body)有三種傳輸內容型別Content-type:application/x-www-form-urlencoded、application/json和multipart/form-data,當然還有其他的幾種,不過不常用,常用的就是這三種。

HEAD請求

HEAD請求主要用於給請求的客戶端返回頭資訊,而不返回Body主體內容。和GET方式類似,只不過GET方式有Body實體返回,而HEAD只返回頭資訊,無Body實體內容返回。主要是用於確認URL的有效性、資源更新的日期時間、檢視伺服器狀態等等,對於有這方面需求的請求來說,比較不佔用資源。

PUT請求

PUT請求主要用於執行傳輸檔案操作,類似於FTP的檔案上傳一樣,請求裡包含檔案內容,並將此檔案儲存到URI指定的伺服器位置。 和POST方式的主要區別是:PUT請求方式如果前後兩個請求相同,則後一個請求會把前一個請求覆蓋掉,實現了PUT方式的修改資源;而POST請求方式如果前後兩個請求相同,則後一個請求不會把前一個請求覆蓋掉,實現了POST的增加資源。

DELETE請求

DELETE請求主要用於執行刪除操作,告訴伺服器想要刪除的資源,不常用。

OPTIONS請求

OPTIONS請求主要用於執行查詢針對所要請求的URI資源伺服器所支援的請求方式,也就是獲取這個URI所支援客戶端提交給伺服器端的請求方式有哪些。

TRACE請求

TRACE請求主要用於執行追蹤傳輸路徑的操作,例如,我們發起了一個Http請求,在這個過程中這個請求可能會經過很多個路徑和過程,TRACE就是告訴伺服器在收到請求後,返回一條響應資訊,將它收到的原始Http請求資訊返回給客戶端,這樣就可以驗證在Http傳輸過程中請求是否被修改過。

CONNECT請求

CONNECT請求主要用於執行連線代理操作,例如“翻牆”。客戶端通過CONNECT方式與伺服器建立通訊隧道,進行TCP通訊。主要通過SSL和TLS安全傳輸資料。CONNECT的作用就是告訴伺服器讓它代替客戶端去請求訪問某個資源,然後再將資料返回給客戶端,相當於一個媒介中轉。

Dart的Http請求

Dart原生http請求庫是Dart提供的一種請求方式,常見的請求方式都支援,除此之外,還支援如上傳和下載檔案等操作。

Dart官方倉庫提供了大量的三方庫和官方庫,引用也非常的方便,Dart PUB官方地址為:pub.dartlang.org,如下圖所示:

在這裡插入圖片描述

1.1 安裝依賴

使用Dart的原生http庫進行網路請求時,需要先在Dart PUB或官方Github裡把相關的http庫引用下來,然後才能使用。新增包依賴前,我們可以使用https://pub.dev/packages/http來檢視依賴包的版本和使用方法。

在這裡插入圖片描述
然後,在pubspec.yaml檔案的dependencies節點新增http庫依賴,如下所示:

http: ^0.12.0+2
複製程式碼

然後,使用flutter packages get命令拉取庫依賴。使用http進行網路請求前,需要先匯入http包,如下:

import 'package:http/http.dart' as http;
複製程式碼

1.2 常用方法

http庫支援常見的get、post、del等請求。其中,get請求的格式如下:

get(dynamic url, { Map<String, String> headers }) → Future<Response>
複製程式碼
  • (必須)url:請求地址
  • (可選)headers:請求頭

post請求的格式如下:

post(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding }) → Future<Response>
複製程式碼
  • (必須)url:請求地址
  • (可選)headers:請求頭
  • (可選)body:引數
  • (編碼)Encoding:編碼

例如,下面是post的示例:

http.post('https://flutter-cn.firebaseio.com/products.json',
            body: json.encode(param),encoding: Utf8Codec())
    .then((http.Response response) {
      final Map<String, dynamic> responseData = json.decode(response.body);
       // 處理響應資料
    
    }).catchError((error) {
      print('$error錯誤');
    });
複製程式碼

1.3 示例

例如,下面使用Dart的http庫實現get請求的示例,示例程式碼如下:

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

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

var hotMovies =
    'https://api.douban.com/v2/movie/in_theaters?apikey=0df993c66c0c636e29ecbb5344252a4a';

class MyApp extends StatelessWidget {
  var movies = '';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'http請求示例',
        theme: new ThemeData(
          primaryColor: Colors.white,
        ),
        home: new Scaffold(
          appBar: new AppBar(
            title: new Text('http請求示例'),
          ),
          body: new Column(children: <Widget>[
            new RaisedButton(
                child: new Text('獲取電影列表'), onPressed: getFilmList()),
            new Expanded(
              child: new Text('$movies'),
            )
          ]),
        ));
  }

  getFilmList() {
    http.get(hotMovies).then((response) {
      movies = response.body;
    });
  }
}

複製程式碼

執行上面的程式碼,結果如下圖:

在這裡插入圖片描述

除了get請求,http的post請求示例如下:

import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';

class DartHttpUtils {
  //建立client例項
  var _client = http.Client();

  //傳送GET請求
  getClient() async {
    var url = "https://abc.com:8090/path1?name=abc&pwd=123";
    _client.get(url).then((http.Response response) {
      //處理響應資訊
      if (response.statusCode == 200) {
        print(response.body);
      } else {
        print('error');
      }
    });
  }

//傳送POST請求,application/x-www-form-urlencoded
  postUrlencodedClient() async {
    var url = "https://abc.com:8090/path2";
    //設定header
    Map<String, String> headersMap = new Map();
    headersMap["content-type"] = "application/x-www-form-urlencoded";
    //設定body引數
    Map<String, String> bodyParams = new Map();
    bodyParams["name"] = "value1";
    bodyParams["pwd"] = "value2";
    _client
        .post(url, headers: headersMap, body: bodyParams, encoding: Utf8Codec())
        .then((http.Response response) {
      if (response.statusCode == 200) {
        print(response.body);
      } else {
        print('error');
      }
    }).catchError((error) {
      print('error');
    });
  }

  //傳送POST請求,application/json
  postJsonClient() async {
    var url = "https://abc.com:8090/path3";
    Map<String, String> headersMap = new Map();
    headersMap["content-type"] = ContentType.json.toString();
    Map<String, String> bodyParams = new Map();
    bodyParams["name"] = "value1";
    bodyParams["pwd"] = "value2";
    _client
        .post(url,
            headers: headersMap,
            body: jsonEncode(bodyParams),
            encoding: Utf8Codec())
        .then((http.Response response) {
      if (response.statusCode == 200) {
        print(response.body);
      } else {
        print('error');
      }
    }).catchError((error) {
      print('error');
    });
  }

  // 傳送POST請求,multipart/form-data
  postFormDataClient() async {
    var url = "https://abc.com:8090/path4";
    var client = new http.MultipartRequest("post", Uri.parse(url));
    client.fields["name"] = "value1";
    client.fields["pwd"] = "value2";
    client.send().then((http.StreamedResponse response) {
      if (response.statusCode == 200) {
        response.stream.transform(utf8.decoder).join().then((String string) {
          print(string);
        });
      } else {
        print('error');
      }
    }).catchError((error) {
      print('error');
    });
  }

// 傳送POST請求,multipart/form-data,上傳檔案
  postFileClient() async {
    var url = "https://abc.com:8090/path5";
    var client = new http.MultipartRequest("post", Uri.parse(url));
    http.MultipartFile.fromPath('file', 'sdcard/img.png',
            filename: 'img.png', contentType: MediaType('image', 'png'))
        .then((http.MultipartFile file) {
      client.files.add(file);
      client.fields["description"] = "descriptiondescription";
      client.send().then((http.StreamedResponse response) {
        if (response.statusCode == 200) {
          response.stream.transform(utf8.decoder).join().then((String string) {
            print(string);
          });
        } else {
          response.stream.transform(utf8.decoder).join().then((String string) {
            print(string);
          });
        }
      }).catchError((error) {
        print(error);
      });
    });
  }
  ///其餘的HEAD、PUT、DELETE請求用法類似,大同小異,大家可以自己試一下
  ///在Widget裡請求成功資料後,使用setState來更新內容和狀態即可
  ///setState(() {
  ///    ...
  ///  });
}

複製程式碼

HttpClient請求

Dart IO庫中提供的HttpClient可以實現一些基本的Http請求。不過,HttpClient只能實現一些基本的網路請求,對應一些複雜的網路請求還無法完成,如POST裡的Body請求體傳輸內容型別部分還無法支援,multipart/form-data這個型別傳輸還不支援。

2.1 使用方法

使用HttpClient發起請求主要分為五步: 1,建立一個HttpClient。

HttpClient httpClient = new HttpClient();
複製程式碼

2,開啟Http連線,設定請求頭。

HttpClientRequest request = await httpClient.getUrl(uri);
複製程式碼

在這一步,我們可以使用任意Http method,如httpClient.post(...)、httpClient.delete(...)等。如果包含Query引數,可以在構建uri時新增,如:

Uri uri=Uri(scheme: "https", host: "flutterchina.club", queryParameters: {
    "xx":"xx",
    "yy":"dd"
  });
複製程式碼

如果需要設定請求頭,可以通過HttpClientRequest設定請求header,如:

request.headers.add("user-agent", "test");
複製程式碼

如果是post或put等可以攜帶請求體的請求,還可以通過HttpClientRequest物件傳送request body,如:

String payload="...";
request.add(utf8.encode(payload)); 
//request.addStream(_inputStream); //可以直接新增輸入流
複製程式碼

3,等待連線伺服器。

HttpClientResponse response = await request.close();
複製程式碼

到這一步之後,請求資訊就已經傳送給伺服器了,返回一個HttpClientResponse物件,它包含響應頭(header)和響應流(響應體的Stream),接下來就可以通過讀取響應流來獲取響應內容。

4,讀取響應內容

String responseBody = await response.transform(utf8.decoder).join();
複製程式碼

5,請求結束後,還需要關閉HttpClient。

httpClient.close();
複製程式碼

2.2 請求示例

import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:io';

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

var hotMovies =
    'https://api.douban.com/v2/movie/in_theaters?apikey=0df993c66c0c636e29ecbb5344252a4a';

class MyApp extends StatelessWidget {

  var movies = '';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'HttpClient請求示例',
        theme: new ThemeData(
          primaryColor: Colors.white,
        ),
        home: new Scaffold(
          appBar: new AppBar(
            title: new Text('HttpClient請求示例'),
          ),
          body: new Column(children: <Widget>[
            new RaisedButton(
                child: new Text('獲取電影列表'), onPressed: getFilmList),
            new Expanded(
              child: new Text('$movies'),
            )
          ]),
        ));
  }

 void getFilmList() async {
    try {
      HttpClient httpClient = new HttpClient();
      HttpClientRequest request = await httpClient.getUrl(Uri.parse(hotMovies));
      HttpClientResponse response = await request.close();
      var result = await response.transform(utf8.decoder).join();
      movies = result;
      print('movies'+result);
      httpClient.close();
    }catch(e){
      print('請求失敗:$e');
    }
  }
}

複製程式碼

執行上面的程式碼,結果如下圖:

在這裡插入圖片描述

利用dio庫請求

除了上面兩種常見的請求方式外,Flutter開發中還可以使用dio等第三方庫來實現Http網路請求,如Dart社群提供的dio庫。

前面說過,HttpClient發起網路請求是比較麻煩的,很多事情都需要我們手動處理,如果再涉及到檔案上傳/下載、Cookie管理等就會非常繁瑣。而Dart社群有一些第三方http請求庫,就可以簡化這些操作。dio庫不僅支援常見的網路請求,還支援Restful API、FormData、攔截器、請求取消、Cookie管理、檔案上傳/下載、超時等操作。

3.1 安裝依賴

和使用其他的第三方庫一樣,使用dio庫之前需要先安裝依賴,安裝前可以在Dart PUB上搜尋dio,確定其版本號,如下所示:

dependencies:
  dio: 2.1.x  #latest version
複製程式碼

然後,執行flutter packages get命令或者點選【Packages get】選項拉取庫依賴。 使用dio之前需要先匯入dio庫,並建立dio例項,如下所示:

import 'package:dio/dio.dart';
Dio dio = new Dio();
複製程式碼

接下來,就可以通過 dio例項來發起網路請求了,注意,一個dio例項可以發起多個http請求,一般來說,APP只有一個http資料來源時,dio應該使用單例模式。

3.2 使用方法

3.2.1 GET請求

import 'package:dio/dio.dart';
void getHttp() async {
  try {
    Response response;
   response=await dio.get("/test?id=12&name=wendu")
   print(response.data.toString());
  } catch (e) {
    print(e);
  }
}
複製程式碼

在上面的示例中,我們可以將query引數通過物件來傳遞,上面的程式碼等同於:

response=await dio.get("/test",queryParameters:{"id":12,"name":"wendu"})
print(response);
複製程式碼

3.2.2 POST請求

response=await dio.post("/test",data:{"id":12,"name":"wendu"})
複製程式碼

3.2.3 多個併發請求

如果要發起多個併發請求,可以使用下面的方式:

response= await Future.wait([dio.post("/info"),dio.get("/token")]);
複製程式碼

3.2.4 下載檔案

如果要下載檔案,可以使用dio的download函式,如下所示:

response=await dio.download("https://www.google.com/",_savePath);
複製程式碼

3.2.5 FormData請求

如果要發起表單請求,可以使用下面的方式:

FormData formData = new FormData.from({
   "name": "wendux",
   "age": 25,
});
response = await dio.post("/info", data: formData)
複製程式碼

如果傳送的資料是FormData,則dio會將請求header的contentType設為“multipart/form-data”。 當然,FormData也支援上傳多個檔案操作,例如:

FormData formData = new FormData.from({
   "name": "wendux",
   "age": 25,
   "file1": new UploadFileInfo(new File("./upload.txt"), "upload1.txt"),
   "file2": new UploadFileInfo(new File("./upload.txt"), "upload2.txt"),
     // 支援檔案陣列上傳
   "files": [
      new UploadFileInfo(new File("./example/upload.txt"), "upload.txt"),
      new UploadFileInfo(new File("./example/upload.txt"), "upload.txt")
    ]
});
response = await dio.post("/info", data: formData)
複製程式碼

3.2.6 回撥設定

值得一提的是,dio內部仍然使用HttpClient發起的請求,所以代理、請求認證、證書校驗等和HttpClient是相同的,我們可以在onHttpClientCreate回撥中進行設定,例如:

(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
    //設定代理 
    client.findProxy = (uri) {
      return "PROXY 192.168.1.2:8888";
    };
    //校驗證書
    httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){
      if(cert.pem==PEM){
      return true; //證書一致,則允許傳送資料
     }
     return false;
    };   
  };
複製程式碼

3.3 示例

import 'package:flutter/material.dart';
import 'package:dio/dio.dart';

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

var hotMovies = 'http://api.douban.com/v2/movie/in_theaters?apikey=0df993c66c0c636e29ecbb5344252a4a';

class MyApp extends StatelessWidget {
  var movies = '';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Dio請求示例',
        theme: new ThemeData(
          primaryColor: Colors.white,
        ),
        home: new Scaffold(
          appBar: new AppBar(
            title: new Text('Dio請求示例'),
          ),
          body: new Column(children: <Widget>[
            new RaisedButton(
                child: new Text('獲取電影列表'), onPressed: getFilmList),
            new Expanded(
              child: new Text('$movies'),
            )
          ]),
        ));
  }

  void getFilmList() async {
    Dio dio = new Dio();
    Response response=await dio.get(hotMovies);
    movies=response.toString();
    print('電影資料:'+movies);
  }
}

複製程式碼

綜合示例

為了對前面的知識做一個簡單的總結,下面通過一個見得的示例來講解Flutter的基本使用,最終效果如圖:

在這裡插入圖片描述
需要說的是,最新版本豆瓣api需要傳遞apikey才能獲取值,下面是電影列表的原始碼:

import 'package:flutter/material.dart';
import 'dart:convert' as Convert;
import 'dart:io';
import 'package:flutter/cupertino.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '豆瓣電影',
      home: Scaffold(
        appBar: new AppBar(
          title: new Text('豆瓣電影列表'),
        ),
        body: DouBanListView(),),
    );
  }
}


class DouBanListView extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return DouBanState();
  }
}

class DouBanState extends State<DouBanListView> with AutomaticKeepAliveClientMixin{

  var url='http://api.douban.com/v2/movie/top250?start=25&count=10&apikey=0df993c66c0c636e29ecbb5344252a4a';
  var subjects = [];
  var itemHeight = 150.0;

  requestMovieTop() async {
    var httpClient = new HttpClient();
    var request = await httpClient.getUrl(Uri.parse(url));
    var response = await request.close();
    var responseBody = await response.transform(Convert.utf8.decoder).join();
    Map data = Convert.jsonDecode(responseBody);
    setState(() {
      subjects = data['subjects'];
    });
  }

  @override
  void initState() {
    super.initState();
    requestMovieTop();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: getListViewContainer(),
    );
  }

  getListViewContainer() {
    if (subjects.length == 0) {
      //loading
      return CupertinoActivityIndicator();
    }
    return ListView.builder(
      //item 的數量
        itemCount: subjects.length,
        itemBuilder: (BuildContext context, int index) {
          return GestureDetector(
            //Flutter 手勢處理
            child: Container(
              color: Colors.transparent,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  numberWidget(index + 1),
                  getItemContainerView(subjects[index]),
                  //下面的灰色分割線
                  Container(
                    height: 10,
                    color: Color.fromARGB(255, 234, 233, 234),
                  )
                ],
              ),
            ),
            onTap: () {
              //監聽點選事件
              print("click item index=$index");
            },
          );
        });
  }

  //肖申克的救贖(1993) View
  getTitleView(subject) {
    var title = subject['title'];
    var year = subject['year'];
    return Container(
      child: Row(
        children: <Widget>[
          Icon(
            Icons.play_circle_outline,
            color: Colors.redAccent,
          ),
          Text(
            title,
            style: TextStyle(
                fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
          ),
          Text('($year)',
              style: TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                  color: Colors.grey))
        ],
      ),
    );
  }

  getItemContainerView(var subject) {
    var imgUrl = subject['images']['medium'];
    return Container(
      width: double.infinity,
      padding: EdgeInsets.all(5.0),
      child: Row(
        children: <Widget>[
          getImage(imgUrl),
          Expanded(
            child: getMovieInfoView(subject),
            flex: 1,
          )
        ],
      ),
    );
  }

  //圓角圖片
  getImage(var imgUrl) {
    return Container(
      decoration: BoxDecoration(
          image:
          DecorationImage(image: NetworkImage(imgUrl), fit: BoxFit.cover),
          borderRadius: BorderRadius.all(Radius.circular(5.0))),
      margin: EdgeInsets.only(left: 8, top: 3, right: 8, bottom: 3),
      height: itemHeight,
      width: 100.0,
    );
  }

  getStaring(var stars) {
    return Row(
      children: <Widget>[RatingBar(stars), Text('$stars')],
    );
  }

  //電影標題,星標評分,演員簡介Container
  getMovieInfoView(var subject) {
    var start = subject['rating']['average'];
    return Container(
      height: itemHeight,
      alignment: Alignment.topLeft,
      child: Column(
        children: <Widget>[
          getTitleView(subject),
          RatingBar(start),
          DescWidget(subject)
        ],
      ),
    );
  }

  //NO.1 圖示
  numberWidget(var no) {
    return Container(
      child: Text(
        'No.$no',
        style: TextStyle(color: Color.fromARGB(255, 133, 66, 0)),
      ),
      decoration: BoxDecoration(
          color: Color.fromARGB(255, 255, 201, 129),
          borderRadius: BorderRadius.all(Radius.circular(5.0))),
      padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
      margin: EdgeInsets.only(left: 12, top: 10),
    );
  }
  @override
  bool get wantKeepAlive => true;
}

//類別、演員介紹
class DescWidget extends StatelessWidget {
  var subject;

  DescWidget(this.subject);

  @override
  Widget build(BuildContext context) {
    var casts = subject['casts'];
    var sb = StringBuffer();
    var genres = subject['genres'];
    for (var i = 0; i < genres.length; i++) {
      sb.write('${genres[i]}  ');
    }
    sb.write("/ ");
    List<String> list = List.generate(
        casts.length, (int index) => casts[index]['name'].toString());

    for (var i = 0; i < list.length; i++) {
      sb.write('${list[i]} ');
    }
    return Container(
      alignment: Alignment.topLeft,
      child: Text(
        sb.toString(),
        softWrap: true,
        textDirection: TextDirection.ltr,
        style:
        TextStyle(fontSize: 16, color: Color.fromARGB(255, 118, 117, 118)),
      ),
    );
  }
}

class RatingBar extends StatelessWidget {
  double stars;

  RatingBar(this.stars);

  @override
  Widget build(BuildContext context) {
    List<Widget> startList = [];
    //實心星星
    var startNumber = stars ~/ 2;
    //半實心星星
    var startHalf = 0;
    if (stars.toString().contains('.')) {
      int tmp = int.parse((stars.toString().split('.')[1]));
      if (tmp >= 5) {
        startHalf = 1;
      }
    }
    //空心星星
    var startEmpty = 5 - startNumber - startHalf;

    for (var i = 0; i < startNumber; i++) {
      startList.add(Icon(
        Icons.star,
        color: Colors.amberAccent,
        size: 18,
      ));
    }
    if (startHalf > 0) {
      startList.add(Icon(
        Icons.star_half,
        color: Colors.amberAccent,
        size: 18,
      ));
    }
    for (var i = 0; i < startEmpty; i++) {
      startList.add(Icon(
        Icons.star_border,
        color: Colors.grey,
        size: 18,
      ));
    }
    startList.add(Text(
      '$stars',
      style: TextStyle(
        color: Colors.grey,
      ),
    ));
    return Container(
      alignment: Alignment.topLeft,
      padding: const EdgeInsets.only(left: 0, top: 8, right: 0, bottom: 5),
      child: Row(
        children: startList,
      ),
    );
  }
}
複製程式碼

附: 1,Flutter系列教程之環境搭建 2,Flutter系列教程之學習線路 3,Flutter系列教程之Dart語法 4,Flutter系列教程之快速入門 5,Flutter系列教程之Flutter 1.7新特性 6,通過HttpClient發起HTTP請求

相關文章