使用Flutter和REST API建立一个天气应用程序

132 阅读9分钟

在靠近用户的地方部署容器

本工程教育(EngEd)计划由科支持。

在全球范围内即时部署容器。Section是经济实惠、简单而强大的。

免费开始

使用Flutter和REST API构建一个天气应用程序

12月1日, 2021

本文将引导读者了解如何在flutter应用程序中使用Dio 包来消费REST API。我们将建立一个天气应用程序,通过对天气API进行网络调用来提供实时天气信息。

该应用程序请求用户的位置并返回用户当前位置的天气信息。Flutter GetX包将被用于状态管理;然而,重点是Dio和网络调用。

主要收获

  • 如何使用Dio包进行网络调用。
  • 如何建立一个实时天气应用程序。
  • 如何访问用户的地理定位。
  • 了解如何在网络调用过程中抛出一个异常。

前提条件

要跟上进度,您应该具备以下条件

  • Dart和Flutter的基本知识。
  • 在您的计算机上安装Flutter。
  • 安装了Android StudioVS Code

在Android Studio中创建一个Flutter应用程序

本项目使用Android Studio作为其集成开发环境(IDE)。您需要启动Android Studio并创建一个新的Flutter项目。确保您将类型设置为Flutter应用程序,并选择您的Flutter SDK所在的路径,然后点击下一步。接下来,填写下图中的项目细节并点击完成。

creating a new Flutter project

集成Dio包

要把Dio包作为一个依赖项添加到应用程序中,请到Dio文档中,复制dio: ^4.0.0 ,并把它添加到项目的pubspec.yaml 文件中。然后,运行命令pub get ,在项目中同步该依赖关系。

adding dio to the project

同时添加GetxGet Storage,分别用于状态管理和本地存储。如果你不知道如何在flutter中使用GetX包进行状态管理,请阅读我关于Getx的文章。此外,添加geolocator依赖项geolocator: ^7.7.0flutter spinkit依赖项flutter_spinkit: ^5.1.0

geolocator允许我们轻松访问特定平台的位置,而flutter Spinkit为我们提供了一个加载指标的集合。你的pubspec.yaml 文件的依赖项部分应该是这样的。

pubspec

项目结构

项目的结构是按照这个顺序进行的。

  • 模型(数据的对象表示)
  • 控制器(逻辑)
  • 服务(允许我们进行网络调用的类)
  • 视图(用户界面)
  • 实用工具(我们想要重复使用的组件)

模型

在模型文件夹中,我们将创建一个代表我们将从服务器接收的对象的类。首先,创建一个名为weather_model.dart 的dart文件,如下所示。

天气模型


// To parse this JSON data, do
//
//     final weather = weatherFromJson(jsonString);

import 'dart:convert';

Weather weatherFromJson(String str) => Weather.fromJson(json.decode(str));

String weatherToJson(Weather data) => json.encode(data.toJson());

class Weather {
  Weather({
    required this.coord,
    required this.weather,
    required this.base,
    required this.main,
    required this.visibility,
    required this.wind,
    required this.clouds,
    required this.dt,
    required this.sys,
    required this.timezone,
    required this.id,
    required this.name,
    required this.cod,
  });

  Coord coord;
  List<WeatherElement> weather;
  String base;
  Main main;
  int visibility;
  Wind wind;
  Clouds clouds;
  int dt;
  Sys sys;
  int timezone;
  int id;
  String name;
  int cod;

  factory Weather.fromJson(Map<String, dynamic> json) => Weather(
        coord: Coord.fromJson(json["coord"]),
        weather: List<WeatherElement>.from(
            json["weather"].map((x) => WeatherElement.fromJson(x))),
        base: json["base"],
        main: Main.fromJson(json["main"]),
        visibility: json["visibility"],
        wind: Wind.fromJson(json["wind"]),
        clouds: Clouds.fromJson(json["clouds"]),
        dt: json["dt"],
        sys: Sys.fromJson(json["sys"]),
        timezone: json["timezone"],
        id: json["id"],
        name: json["name"],
        cod: json["cod"],
      );

  Map<String, dynamic> toJson() => {
        "coord": coord.toJson(),
        "weather": List<dynamic>.from(weather.map((x) => x.toJson())),
        "base": base,
        "main": main.toJson(),
        "visibility": visibility,
        "wind": wind.toJson(),
        "clouds": clouds.toJson(),
        "dt": dt,
        "sys": sys.toJson(),
        "timezone": timezone,
        "id": id,
        "name": name,
        "cod": cod,
      };
}

class Clouds {
  Clouds({
    required this.all,
  });

  int all;

  factory Clouds.fromJson(Map<String, dynamic> json) => Clouds(
        all: json["all"],
      );

  Map<String, dynamic> toJson() => {
        "all": all,
      };
}

class Coord {
  Coord({
    required this.lon,
    required this.lat,
  });

  double lon;
  double lat;

  factory Coord.fromJson(Map<String, dynamic> json) => Coord(
        lon: json["lon"].toDouble(),
        lat: json["lat"].toDouble(),
      );

  Map<String, dynamic> toJson() => {
        "lon": lon,
        "lat": lat,
      };
}

class Main {
  Main({
    required this.temp,
    required this.feelsLike,
    required this.tempMin,
    required this.tempMax,
    required this.pressure,
    required this.humidity,
  });

  double temp;
  double feelsLike;
  double tempMin;
  double tempMax;
  int pressure;
  int humidity;

  factory Main.fromJson(Map<String, dynamic> json) => Main(
        temp: json["temp"].toDouble(),
        feelsLike: json["feels_like"].toDouble(),
        tempMin: json["temp_min"].toDouble(),
        tempMax: json["temp_max"].toDouble(),
        pressure: json["pressure"],
        humidity: json["humidity"],
      );

  Map<String, dynamic> toJson() => {
        "temp": temp,
        "feels_like": feelsLike,
        "temp_min": tempMin,
        "temp_max": tempMax,
        "pressure": pressure,
        "humidity": humidity,
      };
}

class Sys {
  Sys({
    required this.type,
    required this.id,
    required this.country,
    required this.sunrise,
    required this.sunset,
  });

  int type;
  int id;
  String country;
  int sunrise;
  int sunset;

  factory Sys.fromJson(Map<String, dynamic> json) => Sys(
        type: json["type"],
        id: json["id"],
        country: json["country"],
        sunrise: json["sunrise"],
        sunset: json["sunset"],
      );

  Map<String, dynamic> toJson() => {
        "type": type,
        "id": id,
        "country": country,
        "sunrise": sunrise,
        "sunset": sunset,
      };
}

class WeatherElement {
  WeatherElement({
    required this.id,
    required this.main,
    required this.description,
    required this.icon,
  });

  int id;
  String main;
  String description;
  String icon;

  factory WeatherElement.fromJson(Map<String, dynamic> json) => WeatherElement(
        id: json["id"],
        main: json["main"],
        description: json["description"],
        icon: json["icon"],
      );

  Map<String, dynamic> toJson() => {
        "id": id,
        "main": main,
        "description": description,
        "icon": icon,
      };
}

class Wind {
  Wind({
    required this.speed,
    required this.deg,
  });

  double speed;
  int deg;

  factory Wind.fromJson(Map<String, dynamic> json) => Wind(
        speed: json["speed"].toDouble(),
        deg: json["deg"],
      );

  Map<String, dynamic> toJson() => {
        "speed": speed,
        "deg": deg,
      };
}

该类包含实例变量、创建对象时初始化字段的构造函数,以及将我们从API收到的JSON转换为Dart类的方法。

服务

这个文件夹将包含一些类,使应用程序能够通过HTTP进行网络调用,从后端服务器访问资源。

BaseService类

创建一个名为logger.dart 的dart文件,并创建一个名为LogginInterceptor 的类,它将扩展Dio 包中的Interceptor 类。


import 'package:dio/dio.dart';

class LoggingInterceptor extends Interceptor {
  int _maxCharactersPerLine = 200;

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    String responseAsString = response.data.toString();
    if (responseAsString.length > _maxCharactersPerLine) {
      int iterations =
          (responseAsString.length / _maxCharactersPerLine).floor();
      for (int i = 0; i <= iterations; i++) {
        int endingIndex = i * _maxCharactersPerLine + _maxCharactersPerLine;
        if (endingIndex > responseAsString.length) {
          endingIndex = responseAsString.length;
        }
        print(
            responseAsString.substring(i * _maxCharactersPerLine, endingIndex));
      }
    } else {
      print(response.data);
    }
    print("<-- END HTTP");

    super.onResponse(response, handler);
  }

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    print("HEADER--> ${options.method} ${options.headers}");
    print("PATH --> ${options.method} ${options.path}");
    print("DATA --> ${options.data} ${options.data}");
    print(
        "qweryPara --> ${options.queryParameters} ${options.queryParameters}");
    print("Content type: ${options.contentType}");
    print("<-- END HTTP");
    super.onRequest(options, handler);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    print("<-- Error -->");
    print(err.error);
    print(err.message);
    super.onError(err, handler);
  }
}

通过扩展Interceptor 类,我们可以访问它的方法并重写它们,正如已经看到的那样。Interceptor 类中的onRequest 方法,我们已经覆盖了它,它将在任何请求开始之前被执行。另一方面,onResponse 方法将在我们的网络调用成功后被执行。通过重写onError 方法,我们可以访问网络调用过程中可能出现的错误信息。当出现错误时,它会被执行。

让我们配置一下Dio 包,这样我们就可以连接到服务器。创建base_service.dart 类。

import 'package:dio/dio.dart';

import 'local_storage.dart';
import 'logger.dart';

class BaseService {
  
  final Dio _dio = Dio(BaseOptions(
      baseUrl: "https://samples.openweathermap.org",
      validateStatus: (status) {
        return status! < 500;
      },
      headers: {
        "Accept": "*/*",
        "Content-Type": "application/json",
        "Connection": "keep-alive",
      },
      connectTimeout: 60 * 1000,
      receiveTimeout: 60 * 1000))
    ..interceptors.add(LoggingInterceptor());

  Future<Response> request(String url, {dynamic body, String? method}) async {
    var token = LocalStorage.getToken();

    var res = _dio.request(url,
        data: body,
        options: Options(
            method: method,
            headers:
                token != null ? {'authorization': 'Bearer $token'} : null));
    return res;
  }
}

handleError(DioError error) {
  print(error.response.toString());
  if (error.message.contains('SocketException')) {
    return 'Cannot connect. Check that you have internet connection';
  }

  if (error.type == DioErrorType.connectTimeout) {
    return 'Connection timed out. Please retry.';
  }

  if (error.response == null || error.response!.data is String) {
    return 'Something went wrong. Please try again later';
  }
  return 'Something went wrong. Please try again later;
}

首先,我们创建了一个Dio 的实例,名为_dio 。下划线表示它是私有的。我们已经传入了头信息,在这里我们定义了我们想要接收的内容类型。在这个例子中,是一个JSON。设置连接超时,接收超时和一个Interceptor (这就是我们上面创建的LoggingInterceptor 类)。

接下来,我们创建一个名为request 的方法,它使用Dio(_dio)的实例来调用Dio 请求方法,该方法允许我们用选项进行HTTP调用。它接收一个URL,即URL路径(端点),数据,即请求数据,以及包含HTTP方法和头文件的选项。

最后,我们创建了一个方法来处理错误。它接收一个DioError ,并在有SocketExceptionconnection Timeout 、和没有返回的情况下,返回一个适当的消息。

LocalStorage

这个类将使我们能够将一个令牌保存到本地存储区,获得一个令牌,等等。我们为此使用GetStorage

import 'package:get_storage/get_storage.dart';

class LocalStorage {
  /// use this to [saveToken] to local storage
  static saveToken(String tokenValue) {
    return GetStorage().write("token", tokenValue);
  }

  /// use this to [getToken] from local storage
  static getToken() {
    return GetStorage().read("token");
  }

  /// use this to [deleteToken] from local storage
  deleteToken() {
    return GetStorage().remove("token");
  }

  /// use this to [saveUsername] to local storage
  static saveUsername(String userName) {
    return GetStorage().write('name', userName);
  }

  /// use this to [getUsername] from local storage
  static getUsername() {
    return GetStorage().read('name');
  }
}

天气服务

创建一个dart文件,将其称为weather_service.dart ,并编写以下代码。


import 'package:dio/dio.dart';
import 'package:geolocator/geolocator.dart';

import 'base_service.dart';

class WeatherService {
  BaseService service = BaseService();
  static const String apiKey = "b30de56fcbd933743d24fc9004670526";

  Future<Response> getWeather() async {
    try {
      Position position = await Geolocator.getCurrentPosition(
          desiredAccuracy: LocationAccuracy.low);
      double longitude = position.longitude;
      double latitude = position.latitude;
      Response response = await service.request(
          "https://api.openweathermap.org/data/2.5/weather?lat=$latitude&lon=$longitude&appid=$apiKey&units=metric",
          method: "Get");
      print("_++++++++++++++++++${response.statusCode}");
      return response;
    } on DioError catch (e) {
      throw handleError(e);
    }
  }
}

首先,我们创建了一个上面的BaseService 类的实例,以访问request 方法。接下来,我们创建了一个变量apiKey ,用来保存我们项目在下一步的API密钥。要获得API密钥,请到Open Weather创建一个账户,然后为自己生成一个API密钥。

接下来,我们创建了一个getWeather 方法,它返回一个响应的Future。Flutter中的Future代表一个潜在的值或错误,将在未来的某个时候出现。因为我们不知道我们的方法何时会从API返回天气信息,所以返回类型是一个Future。因为我们要返回一个Future,所以我们需要在方法中添加async 关键字,并且await 我们认为需要时间完成的任务。因此,async -await使我们的方法成为异步的。

我们使用GeoLocator 包到用户的当前位置,然后访问经度和纬度,我们将其作为请求参数添加到端点。为了获得位置,我们授予应用程序访问设备位置的权限。对于Andriod来说,进入AndroidManifest.xml 文件,在manifest tag 里面添加了这两行。AndroidManifest.xml 文件可以在这个目录上找到android -> app-> src -> main.

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

请求方法被调用并传入端点,返回Response。注意,我们把可能出错的地方用try和catch包装起来。我们调用了我们在BaseService 类中创建的handleError 方法并抛出了错误。如果有错误发生,我们就抓住它。

在控制器上工作

在控制器文件夹下,创建一个weather_controller.dart 文件,如下图所示。


import 'package:get/get.dart';
import 'package:weather_app/model/weather_model.dart';
import 'package:weather_app/service/weather_service.dart';
import 'package:weather_app/utilities/snack_bar.dart';

class WeatherController extends GetxController {
  final weatherService = Get.put(WeatherService());

  Future<Weather> getWeatherData() async {
    var res;
    try {
      res = await weatherService.getWeather();
      if (res.statusCode != 200 || res.statusCode != 201) {
      
        return WeatherSnackBars.errorSnackBar(message: res.data['message']);
      } else {}
    } catch (e) {
      WeatherSnackBars.errorSnackBar(message: e.toString());
    }
    return Weather.fromJson(res.data);
  }
}

我们创建了一个WeatherController 类,在上面的片段中扩展了GetxController 类。在这里了解如何使用GetX进行状态管理。然后我们定义了一个getWeatherData 方法,它返回一个WeatherFutureWeather 是我们在模型中创建的类,代表我们的数据。

我们使用GetX注入我们在服务文件夹中创建的WeatherService 类,并使用创建的实例来调用getWeather 方法,并将Response保存在变量res ,并返回Response。

建立项目的实用程序

这个文件夹存放我们的辅助类。创建一个constants.dart 文件,如下图所示。

import 'package:flutter/material.dart';

const kTempTextStyle =
    TextStyle(fontFamily: 'Spartan MB', fontSize: 100.0, color: Colors.white);

const kMessageTextStyle =
    TextStyle(fontFamily: 'Spartan MB', fontSize: 50.0, color: Colors.white);

const kButtonTextStyle = TextStyle(
  fontSize: 30.0,
  fontFamily: 'Spartan MB',
);

const kConditionTextStyle = TextStyle(
  fontSize: 100.0,
);

这个文件包含我们想在应用程序中使用的文本样式和尺寸。

编码WeatherSnackbar类


import 'package:flutter/material.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_navigation/src/extension_navigation.dart';
import 'package:get/get_navigation/src/snackbar/snack.dart';

class WeatherSnackBars {
  static errorSnackBar({required String message}) {
    Get.rawSnackbar(
      snackStyle: SnackStyle.FLOATING,
      message: message,
      messageText: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        mainAxisSize: MainAxisSize.max,
        children: [
          SizedBox(
            width: Get.width / 1.6,
            child: Text(
              message,
              style: const TextStyle(color: Colors.white),
            ),
          ),
          const Icon(
            Icons.clear,
            color: Colors.white,
          )
        ],
      ),
      margin: const EdgeInsets.all(24),
      snackPosition: SnackPosition.TOP,
      borderRadius: 8,
      icon: const Icon(
        Icons.cancel,
        color: Colors.white,
      ),
      isDismissible: false,
      backgroundColor: Colors.red,
    );
  }

  static successSnackBar({required String message}) {
    return Get.rawSnackbar(
      snackStyle: SnackStyle.FLOATING,
      message: message,
      messageText: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            message,
            style: const TextStyle(color: Colors.white),
          ),
          const Icon(
            Icons.clear,
            color: Colors.white,
          )
        ],
      ),
      margin: const EdgeInsets.all(24),
      snackPosition: SnackPosition.TOP,
      borderRadius: 8,
      icon: const Icon(
        Icons.check_circle_rounded,
        color: Colors.white,
      ),
      isDismissible: false,
      backgroundColor: Colors.green,
    );
  }
}

我们用GetX ,创建了两个小吃店,分别是成功响应和错误响应。

在WeatherStatus类上下功夫

接下来,创建一个WeatherStatus 类,它将根据返回的天气数据返回适当的消息和Icon。


class WeatherStatus {
  String getWeatherIcon(int condition) {
    if (condition < 300) {
      return '🌩';
    } else if (condition < 400) {
      return '🌧';
    } else if (condition < 600) {
      return '☔️';
    } else if (condition < 700) {
      return '☃️';
    } else if (condition < 800) {
      return '🌫';
    } else if (condition == 800) {
      return '☀️';
    } else if (condition <= 804) {
      return '☁️';
    } else {
      return '🤷‍';
    }
  }

  String getMessage(int temp) {
    if (temp > 25) {
      return 'It\'s 🍦 time';
    } else if (temp > 20) {
      return 'Time for shorts and 👕';
    } else if (temp < 10) {
      return 'You\'ll need 🧣 and 🧤';
    } else {
      return 'Bring a 🧥 just in case';
    }
  }
}

对视图进行编码

import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:get/get.dart';
import 'package:weather_app/controller/weather_controller.dart';
import 'package:weather_app/model/weather_model.dart';
import 'package:weather_app/utilities/constants.dart';
import 'package:weather_app/utilities/weather_status.dart';

class LocationScreen extends StatelessWidget {
  final controller = Get.put(WeatherController());
  final weatherStatus = Get.put(WeatherStatus());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder<Weather>(
          future: controller.getWeatherData(),
          builder: (context, snapshot) {
            if (snapshot.hasError) {
              return Center(
                child: Text("${snapshot.error.toString()}"),
              );
            } else if (snapshot.hasData) {
              var data = snapshot.data;
              var weatherIcon = weatherStatus.getWeatherIcon(data!.cod);

              return Container(
                decoration: BoxDecoration(
                  image: DecorationImage(
                    image: const AssetImage('images/location_background.jpg'),
                    fit: BoxFit.cover,
                    colorFilter: ColorFilter.mode(
                        Colors.white.withOpacity(0.8), BlendMode.dstATop),
                  ),
                ),
                constraints: BoxConstraints.expand(),
                child: SafeArea(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: <Widget>[
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: <Widget>[
                          TextButton(
                            onPressed: () {
                              controller.getWeatherData();
                            },
                            child: const Icon(
                              Icons.near_me,
                              size: 50.0,
                            ),
                          ),
                          TextButton(
                            onPressed: () {},
                            child: const Icon(
                              Icons.location_city,
                              size: 50.0,
                            ),
                          ),
                        ],
                      ),
                      Padding(
                        padding: EdgeInsets.only(left: 15.0),
                        child: Row(
                          children: <Widget>[
                            Text(
                              "${data.main.temp.toInt().toString()}°",
                              style: kTempTextStyle,
                            ),
                            Text(
                              weatherStatus.getWeatherIcon(data.cod),
                              style: kConditionTextStyle,
                            ),
                          ],
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(right: 15.0),
                        child: Text(
                          "${weatherStatus.getMessage(data.main.temp.toInt())} in ${data.name}!",
                          textAlign: TextAlign.right,
                          style: kMessageTextStyle,
                        ),
                      ),
                    ],
                  ),
                ),
              );
            }
            return const Center(
              child: SpinKitDoubleBounce(
                color: Colors.blue,
                size: 50.0,
              ),
            );
          }),
    );
  }
}

我们已经注入了一个控制器的实例,并在一个FutureBuilder 类型的Weather 中调用了getWeatherData 方法。如果未来有数据,它将返回数据;否则,如果有错误,它将显示错误。然而,如果不是上述情况,我们显示SpinKitDoubleBounce ,显示我们的数据正在加载。

总结

在本教程中,你已经学会了如何通过HTTP进行网络调用,并使用Dio包消费一个rest API。我们通过建立一个从Weather API获取天气数据的天气应用程序来证明这一点。

源代码可以在这个资源库中找到。


同行评议的贡献者。Jerim Kaura

类似文章

[

Nested Scroll Music Player App in Jetpack Compose

语言, API

在Jetpack Compose中创建一个嵌套的滚动音乐播放器应用程序

阅读更多

](www.section.io/engineering…

How to Build a REST API with Golang using Native Modules Hero Image

语言,API

如何使用本地模块在Golang中建立一个REST API

阅读更多

](www.section.io/engineering…

Creating ChatBot Using Natural Language Processing in Python Hero Image

语言,API

在Python中使用自然语言处理创建聊天机器人

阅读更多

](www.section.io/engineering…)