Сетевой запрос для серии учебных пособий по разработке Flutter

Flutter

Введение

Сетевой запрос Http — это распространенная и важная функция языка разработки, которая в основном используется для доступа к ресурсам, запроса и отправки данных интерфейса, загрузки и скачивания файлов и т. д. Основные методы запроса Http: GET, POST, HEAD, PUT, УДАЛИТЬ, ПРОСЛЕДИТЬ, ПОДКЛЮЧИТЬ, ВАРИАНТЫ. Эта статья в основном посвящена использованию двух распространенных запросов, GET и POST, во Flutter, в которых особое внимание будет уделено POST. Реализация сетевого Http-запроса Flutter в основном делится на три типа: HttpClient в io.dart, собственный http-запрос Dart и реализация сторонней библиотеки.

Сетевой запрос Http является основным протоколом разработки Интернета.Http поддерживает восемь методов запроса: GET, POST, HEAD, PUT, DELETE, TRACE, CONNECT и OPTIONS.

ПОЛУЧИТЬ запрос

Запрос GET в основном предназначен для выполнения операции получения ресурсов, такой как получение возвращенных ресурсов с сервера через URL-адрес, в котором GET может объединять некоторые параметры запроса с URL-адресом, передавая его на сервер, и сервер анализирует информацию о параметрах, и сервер получает запрос, а затем возвращает запрашивающей стороне соответствующий ресурс. Примечание. Существует максимальное ограничение на размер и длину данных URL-адреса, объединенных запросом GET, а объем передаваемых данных обычно ограничен 2 КБ.

POST-запрос

Запрос POST в основном используется для выполнения таких операций, как отправка информации и запрос информации.По сравнению с запросом GET запрос POST может содержать больше данных, а формат не ограничен, например, поддерживаются JSON, XML, текст и т. д. . А некоторые данные и параметры, передаваемые POST, не встраиваются напрямую после URL, а помещаются в Тело Http-запроса, что безопаснее, чем GET. И размер передаваемых данных и формат неограничен. Метод запроса POST — относительно распространенный метод сетевого запроса, который обычно состоит из заголовка запроса и тела запроса. Общее тело запроса (тело) POST-запроса имеет три типа содержимого передачи Content-type: application/x-www-form-urlencoded, application/json и multipart/form-data, конечно, есть и другие типы, но они обычно не используются Эти три обычно используются.

ГОЛОВНОЙ запрос

Запрос на голову в основном используется для возврата информации заголовка в запрашивающий клиент без возврата контента организма. Подобно методу Get, за исключением того, что метод получения возвращает объект организма, а голова возвращает только информацию заголовка, и контент объекта тела не возвращается. Он в основном используется для подтверждения действия URL, даты и времени обновления ресурсов, проверьте состояние сервера и т. Д. Для запросов с этим требованием он не занимает ресурсов.

PUT-запрос

Запрос PUT в основном используется для выполнения операций передачи файлов.Как и при загрузке файла по FTP, запрос содержит содержимое файла и сохраняет файл на сервере, указанном в URI. Основное отличие от метода POST заключается в следующем: если метод запроса PUT такой же, как до и после двух запросов, последний запрос перезапишет предыдущий запрос, реализуя измененный ресурс в методе PUT; а метод запроса POST, если два запроса до и после одного и того же запроса, то последний запрос не перезапишет предыдущий запрос, реализующий добавление ресурсов POST.

УДАЛИТЬ запрос

Запрос DELETE в основном используется для выполнения операций удаления, сообщая серверу ресурсы, которые вы хотите удалить, и обычно он не используется.

ОПЦИИ запрос

Запрос Options в основном используется для выполнения запроса на запрос запрашиваемого сервера ресурсов URI, то есть, как получить метод запроса клиента, поддерживаемый этим URI, на стороне сервера.

TRACE-запрос

Запрос TRACE в основном используется для выполнения операции отслеживания пути передачи.Например, мы инициируем Http-запрос.Во время этого процесса запрос может пройти множество путей и процессов.TRACE сообщает серверу вернуть ответное сообщение после получения запрос. и возвращает исходную информацию о запросе Http, полученную клиентом, чтобы он мог проверить, был ли запрос изменен во время передачи Http.

Подключить запрос

Подключите запрос соединительного брокера для выполнения основных операций, например «через стену». Клиент устанавливает туннель связи путем подключения сервера Connect, TCP Communication. В основном через SSL безопасную передачу данных и TLS. Подсоедините роль, чтобы сказать на сервере вместо клиента запрашивает доступ к ресурсу, а затем вернуть данные клиенту, эквивалент средней транзита.

Http-запрос Dart

Собственная библиотека HTTP-запросов Dart — это метод запроса, предоставляемый Dart. Он поддерживает все распространенные методы запросов. Кроме того, он также поддерживает такие операции, как загрузка и скачивание файлов.

Официальный репозиторий Dart предоставляет большое количество сторонних библиотек и официальных библиотек, а также очень удобен справочник.Официальный адрес Dart PUB:pub.dartlang.org,Как показано ниже:

在这里插入图片描述

1.1 Установить зависимости

При использовании собственной http-библиотеки Dart для сетевых запросов вам необходимо сослаться на соответствующую http-библиотеку в Dart PUB или официальном Github, прежде чем вы сможете ее использовать. Прежде чем добавлять зависимости пакетов, мы можем использовать https://pub.dev/packages/http для просмотра версии и использования зависимых пакетов.

在这里插入图片描述
Затем добавьте зависимость библиотеки http в узел зависимостей файла pubspec.yaml следующим образом:

http: ^0.12.0+2

Затем используйте команду получения пакетов флаттера, чтобы получить зависимости библиотеки. Прежде чем использовать http для выполнения сетевых запросов, вам необходимо импортировать пакет http следующим образом:

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

1.2 Общие методы

Библиотека http поддерживает общие запросы get, post, del и другие. Среди них формат запроса на получение выглядит следующим образом:

get(dynamic url, { Map<String, String> headers }) → Future<Response>
  • (обязательно) url: адрес запроса
  • (необязательно) заголовки: заголовки запроса

Формат почтового запроса заключается в следующем:

post(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding }) → Future<Response>
  • (обязательно) url: адрес запроса
  • (необязательно) заголовки: заголовки запроса
  • (необязательно) тело: параметр
  • (Кодирование) Кодирование: кодирование

Например, вот пример поста:

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 Пример

Например, в следующем примере для реализации запроса на получение используется http-библиотека Dart. Пример кода выглядит следующим образом:

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;
    });
  }
}

Запустив приведенный выше код, результат будет следующим:

在这里插入图片描述

В дополнение к запросам на получение, примеры HTTP-запросов на публикацию следующие:

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

2.1 Как использовать

Использование HttpClient для инициирования запроса в основном делится на пять шагов: 1. Создайте HttpClient.

HttpClient httpClient = new HttpClient();

2. Откройте соединение Http и установите заголовок запроса.

HttpClientRequest request = await httpClient.getUrl(uri);

На этом шаге мы можем использовать любой метод Http, например httpClient.post(...), httpClient.delete(...) и т. д. Если вы включаете параметр запроса, вы можете добавить его при создании uri, например:

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

Если вам нужно установить заголовок запроса, вы можете установить заголовок запроса через HttpClientRequest, например:

request.headers.add("user-agent", "test");

Если это запрос, который может нести тело запроса, такого как пост или поставить, вы также можете отправить тело запроса через объект httpClientrequest, например:

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 для запроса

В дополнение к двум вышеупомянутым распространенным методам запроса, сторонние библиотеки, такие как dio, также могут использоваться при разработке Flutter для реализации сетевых запросов Http, таких как библиотека dio, предоставленная сообществом Dart.

Как упоминалось ранее, для HttpClient сложнее инициировать сетевые запросы, многие вещи нужно обрабатывать вручную, а если это связано с загрузкой/скачиванием файлов, управлением файлами cookie и т. д., это будет очень громоздко. В сообществе Dart есть несколько сторонних библиотек HTTP-запросов, которые могут упростить эти операции. Библиотека dio не только поддерживает общие сетевые запросы, но также поддерживает такие операции, как Restful API, FormData, перехватчики, отмену запросов, управление файлами cookie, загрузку/выгрузку файлов и тайм-аут.

3.1 Установить зависимости

Как и в случае с другими сторонними библиотеками, перед использованием библиотеки dio необходимо установить зависимости.Перед установкой вы можете выполнить поиск dio в Dart PUB, чтобы определить номер его версии, как показано ниже:

dependencies:
  dio: 2.1.x  #latest version

Затем выполните команду получения пакетов флаттера или щелкните параметр [Получить пакеты], чтобы получить зависимости библиотеки. Прежде чем использовать dio, вам необходимо импортировать библиотеку dio и создать экземпляр dio, как показано ниже:

import 'package:dio/dio.dart';
Dio dio = new Dio();

Затем вы можете инициировать сетевые запросы через экземпляр dio.Обратите внимание, что экземпляр dio может инициировать несколько HTTP-запросов.Вообще говоря, когда приложение имеет только один источник данных http, dio должен использовать одноэлементный режим.

3.2 Как использовать

3.2.1 Получить запрос

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);
  }
}

В приведенном выше примере мы можем передать параметр запроса как объект, приведенный выше код эквивалентен:

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 следующим образом:

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 установит для 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 объясняется на наглядном примере ниже.Окончательный эффект показан на рисунке:

在这里插入图片描述
Нужно сказать, что последняя версия Douban 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,Учебные пособия по созданию среды из серии Flutter2,Учебная линия из серии руководств по Flutter3,Учебное пособиение серии Trader4,Краткое руководство по серии Flutter5,Flutter 1.7 Новые возможности учебных пособий серии Flutter6,Делайте HTTP-запросы через HttpClient