Dart中调用泛型方法:处理可空参数与非空返回值
问题描述
我有如下Dart的API请求工具实现:
enum RequestMethod { get, put, post, delete, } typedef FromJson<T> = T Function(Map<String, dynamic>); class ApiHelper { Future<T?> apiRequest<T>( String endPoint, RequestMethod method, { FromJson<T>? create, Object body = '', bool secure = false, }) async { Response resp; final Map<String, String> headers = new Map<String, String>(); headers.putIfAbsent( HttpHeaders.contentTypeHeader, () => 'application/json', ); headers.putIfAbsent( HttpHeaders.acceptHeader, () => 'application/json', ); if (secure) { final String? token = await SecureStorage().getToken(); headers.putIfAbsent( HttpHeaders.authorizationHeader, () => 'Bearer $token', ); } Uri url = Uri.https( apiUrl, endPoint, ); try { if (method == RequestMethod.get) { resp = await http.get( url, headers: headers, ); } else if (method == RequestMethod.put) { resp = await http.put( url, headers: headers, body: jsonEncode(body), ); } else if (method == RequestMethod.post) { resp = await http.post( url, headers: headers, body: jsonEncode(body), ); } else { resp = await http.delete( url, headers: headers, ); } switch (resp.statusCode) { case 200: { var data = json.decode(resp.body); var api = ApiResponse<T>.fromJson(data, create); if (api.status.isSuccess) { return api.data; } throw ApiException(api.status.message); } case 400: throw BadRequestException(resp.body); case 401: case 403: throw UnauthorisedException(resp.body); case 500: case 404: default: throw FetchDataException(resp.statusCode); } } on TimeoutException { throw FetchDataException('Tiempo de espera agotado'); } on SocketException { throw FetchDataException('Sin conexión'); } on Error catch (e) { throw FetchDataException('Error general: $e'); } } } class ApiResponse<T>{ final T? data; final APIStatus status; ApiResponse({ required this.status, required this.data, }); factory ApiResponse.fromJson( Map<String, dynamic> json, FromJson<T>? create, ) => new ApiResponse( status: APIStatus.fromJson(json["status"]), data: (json['data'] != null) ? create!(json['data']) : null, ); }
目前遇到两种API调用场景:
- 无需返回数据的接口:比如注册接口
signUp,调用方式如下:
@override Future signUp( String username, String password, String firstname, String lastname, String phone, ) async { return await api.apiRequest( "api/user/signUp", RequestMethod.get, body: { "email": username, "password": password, "firstname": firstname, "lastname": lastname, "phone": phone }, secure: false, ); }
- 需要返回非空对象的接口:比如登录接口
signIn,当前调用出现类型错误:
@override Future<SignIn> signIn( String username, String password, ) async { return await api.apiRequest<SignIn>( "api/user/signIn", RequestMethod.post, create: (json) => SignIn.fromJson(json), body: { "email": username, "password": password, }, secure: false, )!; }
错误提示:
A value of type 'SignIn?' can't be returned from the method 'signIn' because it has a return type of 'Future<SignIn>'.
请问如何在两种场景下正确调用apiRequest方法,确保signIn方法能返回非空的SignIn对象?
解决方案
方案1:新增非空返回的重载方法(推荐)
给ApiHelper新增一个专门用于返回非空数据的方法,明确区分两种场景,保证类型安全:
class ApiHelper { // 原有方法:用于无需返回值或数据可能为空的场景 Future<T?> apiRequest<T>( String endPoint, RequestMethod method, { FromJson<T>? create, Object body = '', bool secure = false, }) async { // 保持原实现不变 } // 新增方法:用于确定接口一定会返回有效数据的场景 Future<T> apiRequestNonNull<T>( String endPoint, RequestMethod method, { required FromJson<T> create, Object body = '', bool secure = false, }) async { final result = await apiRequest<T>( endPoint, method, create: create, body: body, secure: secure, ); // 若接口约定返回非空但实际为null,直接抛出异常 if (result == null) { throw FetchDataException('接口返回数据为空'); } return result; } }
调用方式:
- 无需返回值的场景(signUp):保持原调用逻辑即可,泛型会自动推断适配:
@override Future signUp(...) async { return await api.apiRequest( "api/user/signUp", RequestMethod.get, body: {...}, secure: false, ); }
- 需要非空返回的场景(signIn):调用新增的
apiRequestNonNull,无需手动加!,类型完全匹配:
@override Future<SignIn> signIn(...) async { return await api.apiRequestNonNull<SignIn>( "api/user/signIn", RequestMethod.post, create: (json) => SignIn.fromJson(json), body: {...}, secure: false, ); }
方案2:在调用处显式处理空值
如果不想修改ApiHelper,可以在signIn方法中手动校验空值,确保返回非空对象:
@override Future<SignIn> signIn( String username, String password, ) async { final result = await api.apiRequest<SignIn>( "api/user/signIn", RequestMethod.post, create: (json) => SignIn.fromJson(json), body: { "email": username, "password": password, }, secure: false, ); // 空值时抛出异常,符合登录接口"成功必返回用户数据"的约定 if (result == null) { throw FetchDataException('登录接口未返回有效用户数据'); } return result; }
这种方式无需改动工具类,但需要在每个非空返回的调用处重复处理空值,适合场景较少的情况。
方案3:底层重构区分场景
如果接口明确约定:status.isSuccess为true时,需要返回数据的接口必然有非空data,可以从ApiResponse和apiRequest底层拆分逻辑:
- 修改
ApiResponse:
class ApiResponse<T>{ final T data; // 改为非空,仅用于需要返回数据的场景 final APIStatus status; ApiResponse({ required this.status, required this.data, }); // 新增无数据构造,用于无需返回值的接口 factory ApiResponse.empty(APIStatus status) => ApiResponse( status: status, data: null as T, // 仅用于无数据场景,需保证类型安全 ); factory ApiResponse.fromJson( Map<String, dynamic> json, required FromJson<T> create, // 强制要求传入解析方法 ) { final status = APIStatus.fromJson(json["status"]); if (status.isSuccess && json['data'] != null) { return ApiResponse( status: status, data: create(json['data']), ); } else { return ApiResponse.empty(status); } } }
- 拆分
apiRequest为两个方法:
class ApiHelper { // 用于无需返回数据的接口 Future<void> apiRequestVoid( String endPoint, RequestMethod method, { Object body = '', bool secure = false, }) async { await apiRequest<dynamic>( endPoint, method, create: (json) => json, // 占位用 body: body, secure: secure, ); } // 用于返回非空数据的接口 Future<T> apiRequest<T>( String endPoint, RequestMethod method, { required FromJson<T> create, Object body = '', bool secure = false, }) async { // 原请求逻辑保持不变 switch(resp.statusCode) { case 200: var data = json.decode(resp.body); var api = ApiResponse<T>.fromJson(data, create); if (api.status.isSuccess) { return api.data; // 此时data已确保非空 } throw ApiException(api.status.message); // 其他状态码处理逻辑不变 } } }
这种方式从底层区分两种场景,类型安全性最高,但改动范围较大,适合项目重构时采用。
内容的提问来源于stack exchange,提问作者Marc
相关产品推荐
相关产品推荐

