FLutter 中的网络和错误处理
使用 Dio、flutter_bloc 和 Freezed 包在 Flutter 中进行网络和错误处理
在本文中,我将向您展示如何使用dio
、flutter_bloc
和freezed
包处理网络调用和异常
首先,我们需要在pubspec.yaml
文件中添加依赖项。
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.3
dio: 3.0.8
freezed: 0.10.9
flutter_bloc: 5.0.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner:
我们正在使用
首先,我们需要添加以设置 API 调用。
我们正在使用dio
包,因此我们将创建一个DioClient
类
import 'dart:io'; | |
import 'package:dio/dio.dart'; | |
import 'package:flutter/foundation.dart'; | |
const _defaultConnectTimeout = Duration.millisecondsPerMinute; | |
const _defaultReceiveTimeout = Duration.millisecondsPerMinute; | |
class DioClient { | |
final String baseUrl; | |
Dio _dio; | |
final List<Interceptor> interceptors; | |
DioClient( | |
this.baseUrl, | |
Dio dio, { | |
this.interceptors, | |
}) { | |
_dio = dio ?? Dio(); | |
_dio | |
..options.baseUrl = baseUrl | |
..options.connectTimeout = _defaultConnectTimeout | |
..options.receiveTimeout = _defaultReceiveTimeout | |
..httpClientAdapter | |
..options.headers = {'Content-Type': 'application/json; charset=UTF-8'}; | |
if (interceptors?.isNotEmpty ?? false) { | |
_dio.interceptors.addAll(interceptors); | |
} | |
if (kDebugMode) { | |
_dio.interceptors.add(LogInterceptor( | |
responseBody: true, | |
error: true, | |
requestHeader: false, | |
responseHeader: false, | |
request: false, | |
requestBody: false)); | |
} | |
} | |
Future<dynamic> get( | |
String uri, { | |
Map<String, dynamic> queryParameters, | |
Options options, | |
CancelToken cancelToken, | |
ProgressCallback onReceiveProgress, | |
}) async { | |
try { | |
var response = await _dio.get( | |
uri, | |
queryParameters: queryParameters, | |
options: options, | |
cancelToken: cancelToken, | |
onReceiveProgress: onReceiveProgress, | |
); | |
return response.data; | |
} on SocketException catch (e) { | |
throw SocketException(e.toString()); | |
} on FormatException catch (_) { | |
throw FormatException("Unable to process the data"); | |
} catch (e) { | |
throw e; | |
} | |
} | |
Future<dynamic> post( | |
String uri, { | |
data, | |
Map<String, dynamic> queryParameters, | |
Options options, | |
CancelToken cancelToken, | |
ProgressCallback onSendProgress, | |
ProgressCallback onReceiveProgress, | |
}) async { | |
try { | |
var response = await _dio.post( | |
uri, | |
data: data, | |
queryParameters: queryParameters, | |
options: options, | |
cancelToken: cancelToken, | |
onSendProgress: onSendProgress, | |
onReceiveProgress: onReceiveProgress, | |
); | |
return response.data; | |
} on FormatException catch (_) { | |
throw FormatException("Unable to process the data"); | |
} catch (e) { | |
throw e; | |
} | |
} | |
} |
import 'dart:io'; | |
import 'package:dio/dio.dart'; | |
import 'package:flutter/foundation.dart'; | |
const _defaultConnectTimeout = Duration.millisecondsPerMinute; | |
const _defaultReceiveTimeout = Duration.millisecondsPerMinute; | |
class DioClient { | |
final String baseUrl; | |
Dio _dio; | |
final List<Interceptor> interceptors; | |
DioClient( | |
this.baseUrl, | |
Dio dio, { | |
this.interceptors, | |
}) { | |
_dio = dio ?? Dio(); | |
_dio | |
..options.baseUrl = baseUrl | |
..options.connectTimeout = _defaultConnectTimeout | |
..options.receiveTimeout = _defaultReceiveTimeout | |
..httpClientAdapter | |
..options.headers = {'Content-Type': 'application/json; charset=UTF-8'}; | |
if (interceptors?.isNotEmpty ?? false) { | |
_dio.interceptors.addAll(interceptors); | |
} | |
if (kDebugMode) { | |
_dio.interceptors.add(LogInterceptor( | |
responseBody: true, | |
error: true, | |
requestHeader: false, | |
responseHeader: false, | |
request: false, | |
requestBody: false)); | |
} | |
} | |
Future<dynamic> get( | |
String uri, { | |
Map<String, dynamic> queryParameters, | |
Options options, | |
CancelToken cancelToken, | |
ProgressCallback onReceiveProgress, | |
}) async { | |
try { | |
var response = await _dio.get( | |
uri, | |
queryParameters: queryParameters, | |
options: options, | |
cancelToken: cancelToken, | |
onReceiveProgress: onReceiveProgress, | |
); | |
return response.data; | |
} on SocketException catch (e) { | |
throw SocketException(e.toString()); | |
} on FormatException catch (_) { | |
throw FormatException("Unable to process the data"); | |
} catch (e) { | |
throw e; | |
} | |
} | |
Future<dynamic> post( | |
String uri, { | |
data, | |
Map<String, dynamic> queryParameters, | |
Options options, | |
CancelToken cancelToken, | |
ProgressCallback onSendProgress, | |
ProgressCallback onReceiveProgress, | |
}) async { | |
try { | |
var response = await _dio.post( | |
uri, | |
data: data, | |
queryParameters: queryParameters, | |
options: options, | |
cancelToken: cancelToken, | |
onSendProgress: onSendProgress, | |
onReceiveProgress: onReceiveProgress, | |
); | |
return response.data; | |
} on FormatException catch (_) { | |
throw FormatException("Unable to process the data"); | |
} catch (e) { | |
throw e; | |
} | |
} | |
} |
我们将使用存储库访问此类
import 'package:dio/dio.dart';
import 'package:network_handling/services/dio_client.dart';
class APIRepository {
DioClient dioClient;
String _baseUrl = "";
APIRepository() {
var dio = Dio();
dioClient = DioClient(_baseUrl, dio);
}
}
现在,如果你看到当我们点击 API 时,我们会得到 2 个结果
Api 调用
--->> 成功 🏆
--->> 失败 ❌
创建两个freezed
类来处理数据,
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'network_exceptions.dart';
part 'api_result.freezed.dart';
@freezed
abstract class ApiResult<T> with _$ApiResult<T> {
const factory ApiResult.success({@required T data}) = Success<T>;
const factory ApiResult.failure({@required NetworkExceptions error}) =
Failure<T>;
}
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'network_exceptions.freezed.dart';
@freezed
abstract class NetworkExceptions with _$NetworkExceptions {
const factory NetworkExceptions.requestCancelled() = RequestCancelled;
const factory NetworkExceptions.unauthorisedRequest() = UnauthorisedRequest;
const factory NetworkExceptions.badRequest() = BadRequest;
const factory NetworkExceptions.notFound(String reason) = NotFound;
const factory NetworkExceptions.methodNotAllowed() = MethodNotAllowed;
const factory NetworkExceptions.notAcceptable() = NotAcceptable;
const factory NetworkExceptions.requestTimeout() = RequestTimeout;
const factory NetworkExceptions.sendTimeout() = SendTimeout;
const factory NetworkExceptions.conflict() = Conflict;
const factory NetworkExceptions.internalServerError() = InternalServerError;
const factory NetworkExceptions.notImplemented() = NotImplemented;
const factory NetworkExceptions.serviceUnavailable() = ServiceUnavailable;
const factory NetworkExceptions.noInternetConnection() = NoInternetConnection;
const factory NetworkExceptions.formatException() = FormatException;
const factory NetworkExceptions.unableToProcess() = UnableToProcess;
const factory NetworkExceptions.defaultError(String error) = DefaultError;
const factory NetworkExceptions.unexpectedError() = UnexpectedError;
}
如果您看到该类ApiResult
,如果成功,我将返回T
类型的数据,但如果失败,我必须返回网络异常。
我将如何返回网络异常并确定发生了哪个网络异常?
现在你已经看到了这一行,part of'<file-name>.freezed.dart'
我们需要生成冻结文件
Flutter 软件包 pub run build_runner 构建
- Run this command in terminal to watch auto change
Flutter 软件包 pub run build_runner watch
- Run this command in terminal to watch auto change and delete previously generated files
Flutter 软件包 pub run build_runner watch --delete-conflicting-outputs
####Create a new method in NetworkExceptions class which will return NetworkExceptions
```dart
static NetworkExceptions getDioException(error) {
if (error is Exception) {
try {
NetworkExceptions networkExceptions;
if (error is DioError) {
switch (error.type) {
case DioErrorType.CANCEL:
networkExceptions = NetworkExceptions.requestCancelled();
break;
case DioErrorType.CONNECT_TIMEOUT:
networkExceptions = NetworkExceptions.requestTimeout();
break;
case DioErrorType.DEFAULT:
networkExceptions = NetworkExceptions.noInternetConnection();
break;
case DioErrorType.RECEIVE_TIMEOUT:
networkExceptions = NetworkExceptions.sendTimeout();
break;
case DioErrorType.RESPONSE:
switch (error.response.statusCode) {
case 400:
networkExceptions = NetworkExceptions.unauthorisedRequest();
break;
case 401:
networkExceptions = NetworkExceptions.unauthorisedRequest();
break;
case 403:
networkExceptions = NetworkExceptions.unauthorisedRequest();
break;
case 404:
networkExceptions = NetworkExceptions.notFound("Not found");
break;
case 409:
networkExceptions = NetworkExceptions.conflict();
break;
case 408:
networkExceptions = NetworkExceptions.requestTimeout();
break;
case 500:
networkExceptions = NetworkExceptions.internalServerError();
break;
case 503:
networkExceptions = NetworkExceptions.serviceUnavailable();
break;
default:
var responseCode = error.response.statusCode;
networkExceptions = NetworkExceptions.defaultError(
"Received invalid status code: $responseCode",
);
}
break;
case DioErrorType.SEND_TIMEOUT:
networkExceptions = NetworkExceptions.sendTimeout();
break;
}
} else if (error is SocketException) {
networkExceptions = NetworkExceptions.noInternetConnection();
} else {
networkExceptions = NetworkExceptions.unexpectedError();
}
return networkExceptions;
} on FormatException catch (e) {
// Helper.printError(e.toString());
return NetworkExceptions.formatException();
} catch (_) {
return NetworkExceptions.unexpectedError();
}
} else {
if (error.toString().contains("is not a subtype of")) {
return NetworkExceptions.unableToProcess();
} else {
return NetworkExceptions.unexpectedError();
}
}
}
此方法将返回 NetworkException,我们可以使用它来显示不同类型的错误。
此外,有时我们需要针对不同的错误显示相应的错误消息。
static String getErrorMessage(NetworkExceptions networkExceptions) {
var errorMessage = "";
networkExceptions.when(notImplemented: () {
errorMessage = "Not Implemented";
}, requestCancelled: () {
errorMessage = "Request Cancelled";
}, internalServerError: () {
errorMessage = "Internal Server Error";
}, notFound: (String reason) {
errorMessage = reason;
}, serviceUnavailable: () {
errorMessage = "Service unavailable";
}, methodNotAllowed: () {
errorMessage = "Method Allowed";
}, badRequest: () {
errorMessage = "Bad request";
}, unauthorisedRequest: () {
errorMessage = "Unauthorised request";
}, unexpectedError: () {
errorMessage = "Unexpected error occurred";
}, requestTimeout: () {
errorMessage = "Connection request timeout";
}, noInternetConnection: () {
errorMessage = "No internet connection";
}, conflict: () {
errorMessage = "Error due to a conflict";
}, sendTimeout: () {
errorMessage = "Send timeout in connection with API server";
}, unableToProcess: () {
errorMessage = "Unable to process the data";
}, defaultError: (String error) {
errorMessage = error;
}, formatException: () {
errorMessage = "Unexpected error occurred";
}, notAcceptable: () {
errorMessage = "Not acceptable";
});
return errorMessage;
}
查看完整的网络异常类
import 'dart:io'; | |
import 'package:dio/dio.dart'; | |
import 'package:freezed_annotation/freezed_annotation.dart'; | |
part 'network_exceptions.freezed.dart'; | |
@freezed | |
abstract class NetworkExceptions with _$NetworkExceptions { | |
const factory NetworkExceptions.requestCancelled() = RequestCancelled; | |
const factory NetworkExceptions.unauthorisedRequest() = UnauthorisedRequest; | |
const factory NetworkExceptions.badRequest() = BadRequest; | |
const factory NetworkExceptions.notFound(String reason) = NotFound; | |
const factory NetworkExceptions.methodNotAllowed() = MethodNotAllowed; | |
const factory NetworkExceptions.notAcceptable() = NotAcceptable; | |
const factory NetworkExceptions.requestTimeout() = RequestTimeout; | |
const factory NetworkExceptions.sendTimeout() = SendTimeout; | |
const factory NetworkExceptions.conflict() = Conflict; | |
const factory NetworkExceptions.internalServerError() = InternalServerError; | |
const factory NetworkExceptions.notImplemented() = NotImplemented; | |
const factory NetworkExceptions.serviceUnavailable() = ServiceUnavailable; | |
const factory NetworkExceptions.noInternetConnection() = NoInternetConnection; | |
const factory NetworkExceptions.formatException() = FormatException; | |
const factory NetworkExceptions.unableToProcess() = UnableToProcess; | |
const factory NetworkExceptions.defaultError(String error) = DefaultError; | |
const factory NetworkExceptions.unexpectedError() = UnexpectedError; | |
static NetworkExceptions getDioException(error) { | |
if (error is Exception) { | |
try { | |
NetworkExceptions networkExceptions; | |
if (error is DioError) { | |
switch (error.type) { | |
case DioErrorType.CANCEL: | |
networkExceptions = NetworkExceptions.requestCancelled(); | |
break; | |
case DioErrorType.CONNECT_TIMEOUT: | |
networkExceptions = NetworkExceptions.requestTimeout(); | |
break; | |
case DioErrorType.DEFAULT: | |
networkExceptions = NetworkExceptions.noInternetConnection(); | |
break; | |
case DioErrorType.RECEIVE_TIMEOUT: | |
networkExceptions = NetworkExceptions.sendTimeout(); | |
break; | |
case DioErrorType.RESPONSE: | |
switch (error.response.statusCode) { | |
case 400: | |
networkExceptions = NetworkExceptions.unauthorisedRequest(); | |
break; | |
case 401: | |
networkExceptions = NetworkExceptions.unauthorisedRequest(); | |
break; | |
case 403: | |
networkExceptions = NetworkExceptions.unauthorisedRequest(); | |
break; | |
case 404: | |
networkExceptions = NetworkExceptions.notFound("Not found"); | |
break; | |
case 409: | |
networkExceptions = NetworkExceptions.conflict(); | |
break; | |
case 408: | |
networkExceptions = NetworkExceptions.requestTimeout(); | |
break; | |
case 500: | |
networkExceptions = NetworkExceptions.internalServerError(); | |
break; | |
case 503: | |
networkExceptions = NetworkExceptions.serviceUnavailable(); | |
break; | |
default: | |
var responseCode = error.response.statusCode; | |
networkExceptions = NetworkExceptions.defaultError( | |
"Received invalid status code: $responseCode", | |
); | |
} | |
break; | |
case DioErrorType.SEND_TIMEOUT: | |
networkExceptions = NetworkExceptions.sendTimeout(); | |
break; | |
} | |
} else if (error is SocketException) { | |
networkExceptions = NetworkExceptions.noInternetConnection(); | |
} else { | |
networkExceptions = NetworkExceptions.unexpectedError(); | |
} | |
return networkExceptions; | |
} on FormatException catch (e) { | |
// Helper.printError(e.toString()); | |
return NetworkExceptions.formatException(); | |
} catch (_) { | |
return NetworkExceptions.unexpectedError(); | |
} | |
} else { | |
if (error.toString().contains("is not a subtype of")) { | |
return NetworkExceptions.unableToProcess(); | |
} else { | |
return NetworkExceptions.unexpectedError(); | |
} | |
} | |
} | |
static String getErrorMessage(NetworkExceptions networkExceptions) { | |
var errorMessage = ""; | |
networkExceptions.when(notImplemented: () { | |
errorMessage = "Not Implemented"; | |
}, requestCancelled: () { | |
errorMessage = "Request Cancelled"; | |
}, internalServerError: () { | |
errorMessage = "Internal Server Error"; | |
}, notFound: (String reason) { | |
errorMessage = reason; | |
}, serviceUnavailable: () { | |
errorMessage = "Service unavailable"; | |
}, methodNotAllowed: () { | |
errorMessage = "Method Allowed"; | |
}, badRequest: () { | |
errorMessage = "Bad request"; | |
}, unauthorisedRequest: () { | |
errorMessage = "Unauthorised request"; | |
}, unexpectedError: () { | |
errorMessage = "Unexpected error occurred"; | |
}, requestTimeout: () { | |
errorMessage = "Connection request timeout"; | |
}, noInternetConnection: () { | |
errorMessage = "No internet connection"; | |
}, conflict: () { | |
errorMessage = "Error due to a conflict"; | |
}, sendTimeout: () { | |
errorMessage = "Send timeout in connection with API server"; | |
}, unableToProcess: () { | |
errorMessage = "Unable to process the data"; | |
}, defaultError: (String error) { | |
errorMessage = error; | |
}, formatException: () { | |
errorMessage = "Unexpected error occurred"; | |
}, notAcceptable: () { | |
errorMessage = "Not acceptable"; | |
}); | |
return errorMessage; | |
} | |
} |
import 'dart:io'; | |
import 'package:dio/dio.dart'; | |
import 'package:freezed_annotation/freezed_annotation.dart'; | |
part 'network_exceptions.freezed.dart'; | |
@freezed | |
abstract class NetworkExceptions with _$NetworkExceptions { | |
const factory NetworkExceptions.requestCancelled() = RequestCancelled; | |
const factory NetworkExceptions.unauthorisedRequest() = UnauthorisedRequest; | |
const factory NetworkExceptions.badRequest() = BadRequest; | |
const factory NetworkExceptions.notFound(String reason) = NotFound; | |
const factory NetworkExceptions.methodNotAllowed() = MethodNotAllowed; | |
const factory NetworkExceptions.notAcceptable() = NotAcceptable; | |
const factory NetworkExceptions.requestTimeout() = RequestTimeout; | |
const factory NetworkExceptions.sendTimeout() = SendTimeout; | |
const factory NetworkExceptions.conflict() = Conflict; | |
const factory NetworkExceptions.internalServerError() = InternalServerError; | |
const factory NetworkExceptions.notImplemented() = NotImplemented; | |
const factory NetworkExceptions.serviceUnavailable() = ServiceUnavailable; | |
const factory NetworkExceptions.noInternetConnection() = NoInternetConnection; | |
const factory NetworkExceptions.formatException() = FormatException; | |
const factory NetworkExceptions.unableToProcess() = UnableToProcess; | |
const factory NetworkExceptions.defaultError(String error) = DefaultError; | |
const factory NetworkExceptions.unexpectedError() = UnexpectedError; | |
static NetworkExceptions getDioException(error) { | |
if (error is Exception) { | |
try { | |
NetworkExceptions networkExceptions; | |
if (error is DioError) { | |
switch (error.type) { | |
case DioErrorType.CANCEL: | |
networkExceptions = NetworkExceptions.requestCancelled(); | |
break; | |
case DioErrorType.CONNECT_TIMEOUT: | |
networkExceptions = NetworkExceptions.requestTimeout(); | |
break; | |
case DioErrorType.DEFAULT: | |
networkExceptions = NetworkExceptions.noInternetConnection(); | |
break; | |
case DioErrorType.RECEIVE_TIMEOUT: | |
networkExceptions = NetworkExceptions.sendTimeout(); | |
break; | |
case DioErrorType.RESPONSE: | |
switch (error.response.statusCode) { | |
case 400: | |
networkExceptions = NetworkExceptions.unauthorisedRequest(); | |
break; | |
case 401: | |
networkExceptions = NetworkExceptions.unauthorisedRequest(); | |
break; | |
case 403: | |
networkExceptions = NetworkExceptions.unauthorisedRequest(); | |
break; | |
case 404: | |
networkExceptions = NetworkExceptions.notFound("Not found"); | |
break; | |
case 409: | |
networkExceptions = NetworkExceptions.conflict(); | |
break; | |
case 408: | |
networkExceptions = NetworkExceptions.requestTimeout(); | |
break; | |
case 500: | |
networkExceptions = NetworkExceptions.internalServerError(); | |
break; | |
case 503: | |
networkExceptions = NetworkExceptions.serviceUnavailable(); | |
break; | |
default: | |
var responseCode = error.response.statusCode; | |
networkExceptions = NetworkExceptions.defaultError( | |
"Received invalid status code: $responseCode", | |
); | |
} | |
break; | |
case DioErrorType.SEND_TIMEOUT: | |
networkExceptions = NetworkExceptions.sendTimeout(); | |
break; | |
} | |
} else if (error is SocketException) { | |
networkExceptions = NetworkExceptions.noInternetConnection(); | |
} else { | |
networkExceptions = NetworkExceptions.unexpectedError(); | |
} | |
return networkExceptions; | |
} on FormatException catch (e) { | |
// Helper.printError(e.toString()); | |
return NetworkExceptions.formatException(); | |
} catch (_) { | |
return NetworkExceptions.unexpectedError(); | |
} | |
} else { | |
if (error.toString().contains("is not a subtype of")) { | |
return NetworkExceptions.unableToProcess(); | |
} else { | |
return NetworkExceptions.unexpectedError(); | |
} | |
} | |
} | |
static String getErrorMessage(NetworkExceptions networkExceptions) { | |
var errorMessage = ""; | |
networkExceptions.when(notImplemented: () { | |
errorMessage = "Not Implemented"; | |
}, requestCancelled: () { | |
errorMessage = "Request Cancelled"; | |
}, internalServerError: () { | |
errorMessage = "Internal Server Error"; | |
}, notFound: (String reason) { | |
errorMessage = reason; | |
}, serviceUnavailable: () { | |
errorMessage = "Service unavailable"; | |
}, methodNotAllowed: () { | |
errorMessage = "Method Allowed"; | |
}, badRequest: () { | |
errorMessage = "Bad request"; | |
}, unauthorisedRequest: () { | |
errorMessage = "Unauthorised request"; | |
}, unexpectedError: () { | |
errorMessage = "Unexpected error occurred"; | |
}, requestTimeout: () { | |
errorMessage = "Connection request timeout"; | |
}, noInternetConnection: () { | |
errorMessage = "No internet connection"; | |
}, conflict: () { | |
errorMessage = "Error due to a conflict"; | |
}, sendTimeout: () { | |
errorMessage = "Send timeout in connection with API server"; | |
}, unableToProcess: () { | |
errorMessage = "Unable to process the data"; | |
}, defaultError: (String error) { | |
errorMessage = error; | |
}, formatException: () { | |
errorMessage = "Unexpected error occurred"; | |
}, notAcceptable: () { | |
errorMessage = "Not acceptable"; | |
}); | |
return errorMessage; | |
} | |
} |
调用APIRepository类中的API
我正在使用movieDB
API 来获取热门电影。
class MovieResponse { | |
int page; | |
int totalResults; | |
int totalPages; | |
List<Movie> results; | |
MovieResponse({this.page, this.totalResults, this.totalPages, this.results}); | |
MovieResponse.fromJson(Map<String, dynamic> json) { | |
page = json['page']; | |
totalResults = json['total_results']; | |
totalPages = json['total_pages']; | |
if (json['results'] != null) { | |
results = new List<Movie>(); | |
json['results'].forEach((v) { | |
results.add(new Movie.fromJson(v)); | |
}); | |
} | |
} | |
Map<String, dynamic> toJson() { | |
final Map<String, dynamic> data = new Map<String, dynamic>(); | |
data['page'] = this.page; | |
data['total_results'] = this.totalResults; | |
data['total_pages'] = this.totalPages; | |
if (this.results != null) { | |
data['results'] = this.results.map((v) => v.toJson()).toList(); | |
} | |
return data; | |
} | |
} | |
class Movie { | |
int voteCount; | |
int id; | |
bool video; | |
var voteAverage; | |
String title; | |
double popularity; | |
String posterPath; | |
String originalLanguage; | |
String originalTitle; | |
List<int> genreIds; | |
String backdropPath; | |
bool adult; | |
String overview; | |
String releaseDate; | |
Movie( | |
{this.voteCount, | |
this.id, | |
this.video, | |
this.voteAverage, | |
this.title, | |
this.popularity, | |
this.posterPath, | |
this.originalLanguage, | |
this.originalTitle, | |
this.genreIds, | |
this.backdropPath, | |
this.adult, | |
this.overview, | |
this.releaseDate}); | |
Movie.fromJson(Map<String, dynamic> json) { | |
voteCount = json['vote_count']; | |
id = json['id']; | |
video = json['video']; | |
voteAverage = json['vote_average']; | |
title = json['title']; | |
popularity = json['popularity']; | |
posterPath = json['poster_path']; | |
originalLanguage = json['original_language']; | |
originalTitle = json['original_title']; | |
genreIds = json['genre_ids'].cast<int>(); | |
backdropPath = json['backdrop_path']; | |
adult = json['adult']; | |
overview = json['overview']; | |
releaseDate = json['release_date']; | |
} | |
Map<String, dynamic> toJson() { | |
final Map<String, dynamic> data = new Map<String, dynamic>(); | |
data['vote_count'] = this.voteCount; | |
data['id'] = this.id; | |
data['video'] = this.video; | |
data['vote_average'] = this.voteAverage; | |
data['title'] = this.title; | |
data['popularity'] = this.popularity; | |
data['poster_path'] = this.posterPath; | |
data['original_language'] = this.originalLanguage; | |
data['original_title'] = this.originalTitle; | |
data['genre_ids'] = this.genreIds; | |
data['backdrop_path'] = this.backdropPath; | |
data['adult'] = this.adult; | |
data['overview'] = this.overview; | |
data['release_date'] = this.releaseDate; | |
return data; | |
} | |
} |
class MovieResponse { | |
int page; | |
int totalResults; | |
int totalPages; | |
List<Movie> results; | |
MovieResponse({this.page, this.totalResults, this.totalPages, this.results}); | |
MovieResponse.fromJson(Map<String, dynamic> json) { | |
page = json['page']; | |
totalResults = json['total_results']; | |
totalPages = json['total_pages']; | |
if (json['results'] != null) { | |
results = new List<Movie>(); | |
json['results'].forEach((v) { | |
results.add(new Movie.fromJson(v)); | |
}); | |
} | |
} | |
Map<String, dynamic> toJson() { | |
final Map<String, dynamic> data = new Map<String, dynamic>(); | |
data['page'] = this.page; | |
data['total_results'] = this.totalResults; | |
data['total_pages'] = this.totalPages; | |
if (this.results != null) { | |
data['results'] = this.results.map((v) => v.toJson()).toList(); | |
} | |
return data; | |
} | |
} | |
class Movie { | |
int voteCount; | |
int id; | |
bool video; | |
var voteAverage; | |
String title; | |
double popularity; | |
String posterPath; | |
String originalLanguage; | |
String originalTitle; | |
List<int> genreIds; | |
String backdropPath; | |
bool adult; | |
String overview; | |
String releaseDate; | |
Movie( | |
{this.voteCount, | |
this.id, | |
this.video, | |
this.voteAverage, | |
this.title, | |
this.popularity, | |
this.posterPath, | |
this.originalLanguage, | |
this.originalTitle, | |
this.genreIds, | |
this.backdropPath, | |
this.adult, | |
this.overview, | |
this.releaseDate}); | |
Movie.fromJson(Map<String, dynamic> json) { | |
voteCount = json['vote_count']; | |
id = json['id']; | |
video = json['video']; | |
voteAverage = json['vote_average']; | |
title = json['title']; | |
popularity = json['popularity']; | |
posterPath = json['poster_path']; | |
originalLanguage = json['original_language']; | |
originalTitle = json['original_title']; | |
genreIds = json['genre_ids'].cast<int>(); | |
backdropPath = json['backdrop_path']; | |
adult = json['adult']; | |
overview = json['overview']; | |
releaseDate = json['release_date']; | |
} | |
Map<String, dynamic> toJson() { | |
final Map<String, dynamic> data = new Map<String, dynamic>(); | |
data['vote_count'] = this.voteCount; | |
data['id'] = this.id; | |
data['video'] = this.video; | |
data['vote_average'] = this.voteAverage; | |
data['title'] = this.title; | |
data['popularity'] = this.popularity; | |
data['poster_path'] = this.posterPath; | |
data['original_language'] = this.originalLanguage; | |
data['original_title'] = this.originalTitle; | |
data['genre_ids'] = this.genreIds; | |
data['backdrop_path'] = this.backdropPath; | |
data['adult'] = this.adult; | |
data['overview'] = this.overview; | |
data['release_date'] = this.releaseDate; | |
return data; | |
} | |
} |
import 'package:dio/dio.dart';
import 'package:network_handling/model/movie_response.dart';
import 'package:network_handling/services/api_result.dart';
import 'package:network_handling/services/dio_client.dart';
import 'package:network_handling/services/network_exceptions.dart';
class APIRepository {
DioClient dioClient;
final String _apiKey = <apikey>;
String _baseUrl = "http://api.themoviedb.org/3/";
APIRepository() {
var dio = Dio();
dioClient = DioClient(_baseUrl, dio);
}
Future<ApiResult<List<Movie>>> fetchMovieList() async {
try {
final response = await dioClient
.get("movie/popular", queryParameters: {"api_key": _apiKey});
List<Movie> movieList = MovieResponse.fromJson(response).results;
return ApiResult.success(data: movieList);
} catch (e) {
return ApiResult.failure(error: NetworkExceptions.getDioException(e));
}
}
}
现在,这只是 API 调用,如何在 UI 中显示它?
我用它来flutter_bloc
管理 UI 中的数据
在bloc
课堂上,我们需要两件东西
由于我们只有一个调用 API 的事件,因此类中只有一个事件
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'movie_event.freezed.dart';
@freezed
abstract class MovieEvent with _$MovieEvent {
const factory MovieEvent.loadMovie() = LoadMovies;
}
现在我们需要了解为什么这很重要。
当我们打开任何应用程序时,我们通常会看到一些事件发生,例如加载、显示数据、显示错误。
这些就是我们需要在应用程序中实现的,所以所有这些事件都将是我们的结果状态。
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:network_handling/services/network_exceptions.dart';
part 'result_state.freezed.dart';
@freezed
abstract class ResultState<T> with _$ResultState<T> {
const factory ResultState.idle() = Idle<T>;
const factory ResultState.loading() = Loading<T>;
const factory ResultState.data({@required T data}) = Data<T>;
const factory ResultState.error({@required NetworkExceptions error}) =
Error<T>;
}
现在生成这些文件
flutter packages pub run build_runner build
现在我们已经创建了事件和状态,让我们创建 Bloc 类。
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:network_handling/api_repository.dart';
import 'package:network_handling/bloc/movie/movie_event.dart';
import 'package:network_handling/bloc/movie/result_state.dart';
import 'package:network_handling/model/movie_response.dart';
class MovieBloc extends Bloc<MovieEvent, ResultState<List<Movie>>> {
final APIRepository apiRepository;
MovieBloc({this.apiRepository})
: assert(apiRepository != null),
super(Idle());
@override
Stream<ResultState<List<Movie>>> mapEventToState(MovieEvent event) {
}
}
现在我们需要在这里调用 API,并将状态沉入区块中
class MovieBloc extends Bloc<MovieEvent, ResultState<List<Movie>>> {
final APIRepository apiRepository;
MovieBloc({this.apiRepository})
: assert(apiRepository != null),
super(Idle());
@override
Stream<ResultState<List<Movie>>> mapEventToState(MovieEvent event) async* {
yield ResultState.loading();
if (event is LoadMovies) {
ApiResult<List<Movie>> apiResult = await apiRepository.fetchMovieList();
yield* apiResult.when(success: (List<Movie> data) async* {
yield ResultState.data(data: data);
}, failure: (NetworkExceptions error) async* {
yield ResultState.error(error: error);
});
}
}
}
如果你看到我们已经实现了所有状态
Idle
一开始的状态Loading
状态现在我们已经创建了Bloc
类并实现了其中的逻辑,现在我们需要在 UI 中使用这个 bloc 类。
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<MovieBloc>(
create: (BuildContext context) {
return MovieBloc(apiRepository: APIRepository());
},
child: MyHomePage(),
)
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
),
);
}
}
将 BlocProvider 添加到小部件树的顶部,以便我们可以访问数据。
现在HomePage
我们将调用 API
@override
void initState() {
context.bloc<MovieBloc>().add(LoadMovies());
super.initState();
}
我们将会聆听其中的变化BlocBuilder
。
BlocBuilder<MovieBloc, ResultState<List<Movie>>>(
builder: (BuildContext context, ResultState<List<Movie>> state) {
return state.when(
loading: () {
return Center(child: CircularProgressIndicator());
},
idle: () {
return Container();
},
data: (List<Movie> data) {
return dataWidget(data);
},
error: (NetworkExceptions error) {
return Text(NetworkExceptions.getErrorMessage(error));
},
);
},
)
查看UI代码
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
context.bloc<MovieBloc>().add(LoadMovies());
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Movies"),
),
body: BlocBuilder<MovieBloc, ResultState<List<Movie>>>(
builder: (BuildContext context, ResultState<List<Movie>> state) {
return state.when(
loading: () {
return Center(child: CircularProgressIndicator());
},
idle: () {
return Container();
},
data: (List<Movie> data) {
return dataWidget(data);
},
error: (NetworkExceptions error) {
return Text(NetworkExceptions.getErrorMessage(error));
},
);
},
),
);
}
Widget dataWidget(List<Movie> data) {
return ListView.builder(
itemCount: data.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 300,
width: 300,
child: Card(
elevation: 1,
child: Image.network(
"https://image.tmdb.org/t/p/w342${data[index].posterPath}",
),
),
);
},
);
}
}
现在让我们看看我们构建了什么:
点击此处查看完整代码
感谢您阅读这篇文章❤
如果我有错误,请在评论区告诉我。我很乐意改进。
让我们联系起来:https://www.ashishrawat.dev
鏂囩珷鏉ユ簮锛�https://dev.to/ashishrawat2911/handling-network-calls-and-exceptions-in-flutter-54me