You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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调用场景:

  1. 无需返回数据的接口:比如注册接口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,
  );
}
  1. 需要返回非空对象的接口:比如登录接口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底层拆分逻辑:

  1. 修改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);
    }
  }
}
  1. 拆分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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 13:17:58