在靠近用户的地方部署容器
本工程教育(EngEd)计划由科支持。
在全球范围内即时部署容器。Section是经济实惠、简单而强大的。
免费开始。
使用Flutter和REST API构建一个天气应用程序
12月1日, 2021
本文将引导读者了解如何在flutter应用程序中使用Dio 包来消费REST API。我们将建立一个天气应用程序,通过对天气API进行网络调用来提供实时天气信息。
该应用程序请求用户的位置并返回用户当前位置的天气信息。Flutter GetX包将被用于状态管理;然而,重点是Dio和网络调用。
主要收获
- 如何使用Dio包进行网络调用。
- 如何建立一个实时天气应用程序。
- 如何访问用户的地理定位。
- 了解如何在网络调用过程中抛出一个异常。
前提条件
要跟上进度,您应该具备以下条件
- Dart和Flutter的基本知识。
- 在您的计算机上安装Flutter。
- 安装了Android Studio或VS Code。
在Android Studio中创建一个Flutter应用程序
本项目使用Android Studio作为其集成开发环境(IDE)。您需要启动Android Studio并创建一个新的Flutter项目。确保您将类型设置为Flutter应用程序,并选择您的Flutter SDK所在的路径,然后点击下一步。接下来,填写下图中的项目细节并点击完成。

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

同时添加Getx和Get Storage,分别用于状态管理和本地存储。如果你不知道如何在flutter中使用GetX包进行状态管理,请阅读我关于Getx的文章。此外,添加geolocator依赖项geolocator: ^7.7.0 和flutter spinkit依赖项flutter_spinkit: ^5.1.0 。
geolocator允许我们轻松访问特定平台的位置,而flutter Spinkit为我们提供了一个加载指标的集合。你的pubspec.yaml 文件的依赖项部分应该是这样的。

项目结构
项目的结构是按照这个顺序进行的。
- 模型(数据的对象表示)
- 控制器(逻辑)
- 服务(允许我们进行网络调用的类)
- 视图(用户界面)
- 实用工具(我们想要重复使用的组件)
模型
在模型文件夹中,我们将创建一个代表我们将从服务器接收的对象的类。首先,创建一个名为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 ,并在有SocketException 、connection 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 方法,它返回一个Weather 的Future 。Weather 是我们在模型中创建的类,代表我们的数据。
我们使用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
类似文章
[

语言, API
在Jetpack Compose中创建一个嵌套的滚动音乐播放器应用程序
阅读更多

语言,API
如何使用本地模块在Golang中建立一个REST API
阅读更多

语言,API
在Python中使用自然语言处理创建聊天机器人
阅读更多