Basics

网络

简介

Nylo Website 使网络请求变得简单。您在继承 NyApiService 的服务类中定义 API 端点,然后从页面中调用它们。框架负责处理 JSON 解码、错误处理、缓存以及将响应自动转换为模型类(称为"morphing")。

您的 API 服务位于 lib/app/networking/。新项目包含一个默认的 ApiService

class ApiService extends NyApiService {
  ApiService({BuildContext? buildContext})
      : super(
          buildContext,
          decoders: modelDecoders,
        );

  @override
  String get baseUrl => getEnv('API_BASE_URL');

  @override
  Map<Type, Interceptor> get interceptors => {
    ...super.interceptors,
  };

  Future fetchUsers() async {
    return await network(
      request: (request) => request.get("/users"),
    );
  }
}

有三种方式发起 HTTP 请求:

方式 返回值 最适合
便捷方法(getpost 等) T? 简单的 CRUD 操作
network() T? 需要缓存、重试或自定义请求头的请求
networkResponse() NyResponse<T> 需要状态码、请求头或错误详情时

底层 Nylo Website 使用 Dio,一个强大的 HTTP 客户端。

便捷方法

NyApiService 为常见的 HTTP 操作提供了简写方法。这些方法内部调用 network()

GET 请求

Future<User?> fetchUser(int id) async {
  return await get<User>(
    "/users/$id",
    queryParameters: {"include": "profile"},
  );
}

POST 请求

Future<User?> createUser(Map<String, dynamic> data) async {
  return await post<User>("/users", data: data);
}

PUT 请求

Future<User?> updateUser(int id, Map<String, dynamic> data) async {
  return await put<User>("/users/$id", data: data);
}

DELETE 请求

Future<bool?> deleteUser(int id) async {
  return await delete<bool>("/users/$id");
}

PATCH 请求

Future<User?> patchUser(int id, Map<String, dynamic> data) async {
  return await patch<User>("/users/$id", data: data);
}

HEAD 请求

使用 HEAD 检查资源是否存在或在不下载正文的情况下获取请求头:

Future<bool> checkResourceExists(String url) async {
  Response response = await head(url);
  return response.statusCode == 200;
}

Network 辅助方法

network 方法比便捷方法提供更多控制。它直接返回转换后的数据(T?)。

class ApiService extends NyApiService {
  ...

  Future<User?> fetchUser(int id) async {
    return await network<User>(
      request: (request) => request.get("/users/$id"),
    );
  }

  Future<List<User>?> fetchUsers() async {
    return await network<List<User>>(
      request: (request) => request.get("/users"),
    );
  }

  Future<User?> createUser(Map<String, dynamic> data) async {
    return await network<User>(
      request: (request) => request.post("/users", data: data),
    );
  }
}

request 回调接收一个已配置好基础 URL 和拦截器的 Dio 实例。

network 参数

参数 类型 描述
request Function(Dio) 要执行的 HTTP 请求(必需)
bearerToken String? 此请求的 Bearer 令牌
baseUrl String? 覆盖服务基础 URL
headers Map<String, dynamic>? 附加请求头
retry int? 重试次数
retryDelay Duration? 重试之间的延迟
retryIf bool Function(DioException)? 重试条件
connectionTimeout Duration? 连接超时
receiveTimeout Duration? 接收超时
sendTimeout Duration? 发送超时
cacheKey String? 缓存键
cacheDuration Duration? 缓存时长
cachePolicy CachePolicy? 缓存策略
checkConnectivity bool? 请求前检查连接
handleSuccess Function(NyResponse<T>)? 成功回调
handleFailure Function(NyResponse<T>)? 失败回调

networkResponse 辅助方法

当您需要访问完整响应(状态码、请求头、错误消息),而不仅仅是数据时,使用 networkResponse。它返回 NyResponse<T> 而不是 T?

使用 networkResponse 的场景:

  • 需要检查 HTTP 状态码进行特定处理
  • 需要访问响应头
  • 需要获取详细的错误消息用于用户反馈
  • 需要实现自定义错误处理逻辑
Future<NyResponse<User>> fetchUser(int id) async {
  return await networkResponse<User>(
    request: (request) => request.get("/users/$id"),
  );
}

然后在页面中使用响应:

NyResponse<User> response = await _apiService.fetchUser(1);

if (response.isSuccessful) {
  User? user = response.data;
  print('Status: ${response.statusCode}');
} else {
  print('Error: ${response.errorMessage}');
  print('Status: ${response.statusCode}');
}

network 与 networkResponse 对比

// network() — returns the data directly
User? user = await network<User>(
  request: (request) => request.get("/users/1"),
);

// networkResponse() — returns the full response
NyResponse<User> response = await networkResponse<User>(
  request: (request) => request.get("/users/1"),
);
User? user = response.data;
int? status = response.statusCode;

两种方法接受相同的参数。当您需要检查数据以外的响应内容时,选择 networkResponse

NyResponse

NyResponse<T> 使用转换后的数据和状态辅助方法包装 Dio 响应。

属性

属性 类型 描述
response Response? 原始 Dio Response
data T? 转换/解码后的数据
rawData dynamic 原始响应数据
headers Headers? 响应头
statusCode int? HTTP 状态码
statusMessage String? HTTP 状态消息
contentType String? 请求头中的内容类型
errorMessage String? 提取的错误消息

状态检查

Getter 描述
isSuccessful 状态码 200-299
isClientError 状态码 400-499
isServerError 状态码 500-599
isRedirect 状态码 300-399
hasData 数据不为 null
isUnauthorized 状态码 401
isForbidden 状态码 403
isNotFound 状态码 404
isTimeout 状态码 408
isConflict 状态码 409
isRateLimited 状态码 429

数据辅助方法

NyResponse<User> response = await apiService.fetchUser(1);

// Get data or throw if null
User user = response.dataOrThrow('User not found');

// Get data or use a fallback
User user = response.dataOr(User.guest());

// Run callback only if successful
String? greeting = response.ifSuccessful((user) => 'Hello ${user.name}');

// Pattern match on success/failure
String result = response.when(
  success: (user) => 'Welcome, ${user.name}!',
  failure: (response) => 'Error: ${response.statusMessage}',
);

// Get a specific header
String? authHeader = response.getHeader('Authorization');

基本选项

使用 baseOptions 参数为 API 服务配置默认的 Dio 选项:

class ApiService extends NyApiService {
  ApiService({BuildContext? buildContext}) : super(
    buildContext,
    decoders: modelDecoders,
    baseOptions: (BaseOptions baseOptions) {
      return baseOptions
        ..connectTimeout = Duration(seconds: 5)
        ..sendTimeout = Duration(seconds: 5)
        ..receiveTimeout = Duration(seconds: 5);
    },
  );
  ...
}

您也可以在实例上动态配置选项:

apiService.setConnectTimeout(Duration(seconds: 10));
apiService.setReceiveTimeout(Duration(seconds: 30));
apiService.setSendTimeout(Duration(seconds: 10));
apiService.setContentType('application/json');

点击此处查看所有可设置的基本选项。

添加请求头

每个请求的请求头

Future fetchWithHeaders() async => await network(
  request: (request) => request.get("/test"),
  headers: {
    "Authorization": "Bearer aToken123",
    "Device": "iPhone"
  }
);

Bearer 令牌

Future fetchUser() async => await network(
  request: (request) => request.get("/user"),
  bearerToken: "hello-world-123",
);

服务级别请求头

apiService.setHeaders({"X-Custom-Header": "value"});
apiService.setBearerToken("my-token");

RequestHeaders 扩展

RequestHeaders 类型(一个 Map<String, dynamic> 类型别名)提供了辅助方法:

@override
Future<RequestHeaders> setAuthHeaders(RequestHeaders headers) async {
  String? token = Auth.data(field: 'token');
  if (token != null) {
    headers.addBearerToken(token);
  }
  headers.addHeader('X-App-Version', '1.0.0');
  return headers;
}
方法 描述
addBearerToken(token) 设置 Authorization: Bearer 请求头
getBearerToken() 从请求头中读取 bearer 令牌
addHeader(key, value) 添加自定义请求头
hasHeader(key) 检查请求头是否存在

上传文件

单文件上传

Future<UploadResponse?> uploadAvatar(String filePath) async {
  return await upload<UploadResponse>(
    '/upload',
    filePath: filePath,
    fieldName: 'avatar',
    additionalFields: {'userId': '123'},
    onProgress: (sent, total) {
      double progress = sent / total * 100;
      print('Progress: ${progress.toStringAsFixed(0)}%');
    },
  );
}

多文件上传

Future<UploadResponse?> uploadDocuments() async {
  return await uploadMultiple<UploadResponse>(
    '/upload',
    files: {
      'avatar': '/path/to/avatar.jpg',
      'document': '/path/to/doc.pdf',
    },
    additionalFields: {'userId': '123'},
    onProgress: (sent, total) {
      print('Progress: ${(sent / total * 100).toStringAsFixed(0)}%');
    },
  );
}

下载文件

Future<void> downloadFile(String url, String savePath) async {
  await download(
    url,
    savePath: savePath,
    onProgress: (received, total) {
      if (total != -1) {
        print('Progress: ${(received / total * 100).toStringAsFixed(0)}%');
      }
    },
    deleteOnError: true,
  );
}

拦截器

拦截器允许您在发送请求之前修改请求、处理响应和管理错误。它们在通过 API 服务发出的每个请求上运行。

使用拦截器的场景:

  • 需要为所有请求添加认证请求头
  • 需要记录请求和响应用于调试
  • 需要全局转换请求/响应数据
  • 需要处理特定错误码(例如,在 401 时刷新令牌)
class ApiService extends NyApiService {
  ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);

  @override
  Map<Type, Interceptor> get interceptors => {
    ...super.interceptors,
    BearerAuthInterceptor: BearerAuthInterceptor(),
    LoggingInterceptor: LoggingInterceptor(),
  };
  ...
}

创建自定义拦截器

metro make:interceptor logging

文件: app/networking/dio/interceptors/logging_interceptor.dart

import 'package:nylo_framework/nylo_framework.dart';

class LoggingInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    print('REQUEST[${options.method}] => PATH: ${options.path}');
    return super.onRequest(options, handler);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    print('RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}');
    handler.next(response);
  }

  @override
  void onError(DioException dioException, ErrorInterceptorHandler handler) {
    print('ERROR[${dioException.response?.statusCode}] => PATH: ${dioException.requestOptions.path}');
    handler.next(dioException);
  }
}

网络日志记录器

Nylo Website 包含内置的 NetworkLogger 拦截器。当环境中 APP_DEBUGtrue 时默认启用。

配置

class ApiService extends NyApiService {
  ApiService({BuildContext? buildContext}) : super(
    buildContext,
    decoders: modelDecoders,
    useNetworkLogger: true,
    networkLogger: NetworkLogger(
      logLevel: LogLevelType.verbose,
      request: true,
      requestHeader: true,
      requestBody: true,
      responseBody: true,
      responseHeader: false,
      error: true,
    ),
  );
}

您可以通过设置 useNetworkLogger: false 来禁用它。

class ApiService extends NyApiService {
  ApiService({BuildContext? buildContext})
      : super(
          buildContext,
          decoders: modelDecoders,
          useNetworkLogger: false, // <-- 禁用日志记录器
        );

日志级别

级别 描述
LogLevelType.verbose 打印所有请求/响应详情
LogLevelType.minimal 仅打印方法、URL、状态和时间
LogLevelType.none 无日志输出

过滤日志

NetworkLogger(
  filter: (options, args) {
    // Only log requests to specific endpoints
    return options.path.contains('/api/v1');
  },
)

使用 API 服务

有两种方式从页面调用 API 服务。

直接实例化

class _MyHomePageState extends NyPage<MyHomePage> {

  ApiService _apiService = ApiService();

  @override
  get init => () async {
    List<User>? users = await _apiService.fetchUsers();
    print(users);
  };
}

使用 api() 辅助方法

api 辅助方法使用 config/decoders.dart 中的 apiDecoders 创建实例:

class _MyHomePageState extends NyPage<MyHomePage> {

  @override
  get init => () async {
    User? user = await api<ApiService>((request) => request.fetchUser());
    print(user);
  };
}

带回调:

await api<ApiService>(
  (request) => request.fetchUser(),
  onSuccess: (response, data) {
    // data is the morphed User? instance
  },
  onError: (DioException dioException) {
    // Handle the error
  },
);

api() 辅助方法参数

参数 类型 描述
request Function(T) API 请求函数
context BuildContext? 构建上下文
headers Map<String, dynamic> 附加请求头
bearerToken String? Bearer 令牌
baseUrl String? 覆盖基础 URL
page int? 分页页码
perPage int? 每页项目数
retry int 重试次数
retryDelay Duration? 重试之间的延迟
onSuccess Function(Response, dynamic)? 成功回调
onError Function(DioException)? 错误回调
cacheKey String? 缓存键
cacheDuration Duration? 缓存时长

创建 API 服务

要创建新的 API 服务:

metro make:api_service user

带模型:

metro make:api_service user --model="User"

这将创建一个带有 CRUD 方法的 API 服务:

class UserApiService extends NyApiService {
  ...

  Future<List<User>?> fetchAll({dynamic query}) async {
    return await network<List<User>>(
      request: (request) => request.get("/endpoint-path", queryParameters: query),
    );
  }

  Future<User?> find({required int id}) async {
    return await network<User>(
      request: (request) => request.get("/endpoint-path/$id"),
    );
  }

  Future<User?> create({required dynamic data}) async {
    return await network<User>(
      request: (request) => request.post("/endpoint-path", data: data),
    );
  }

  Future<User?> update({dynamic query}) async {
    return await network<User>(
      request: (request) => request.put("/endpoint-path", queryParameters: query),
    );
  }

  Future<bool?> delete({required int id}) async {
    return await network<bool>(
      request: (request) => request.delete("/endpoint-path/$id"),
    );
  }
}

将 JSON 转换为模型

"Morphing" 是 Nylo Website 用于自动将 JSON 响应转换为 Dart 模型类的术语。当您使用 network<User>(...) 时,响应 JSON 会通过解码器创建 User 实例——无需手动解析。

class ApiService extends NyApiService {
  ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);

  // Returns a single User
  Future<User?> fetchUser() async {
    return await network<User>(
      request: (request) => request.get("/user/1"),
    );
  }

  // Returns a List of Users
  Future<List<User>?> fetchUsers() async {
    return await network<List<User>>(
      request: (request) => request.get("/users"),
    );
  }
}

解码器在 lib/bootstrap/decoders.dart 中定义:

final Map<Type, dynamic> modelDecoders = {
  User: (data) => User.fromJson(data),

  List<User>: (data) =>
      List.from(data).map((json) => User.fromJson(json)).toList(),
};

您传递给 network<T>() 的类型参数会与 modelDecoders 映射匹配以找到正确的解码器。

另请参阅: 解码器 了解注册模型解码器的详细信息。

缓存响应

缓存响应以减少 API 调用并提高性能。缓存适用于不经常变化的数据,如国家列表、分类或配置。

提供 cacheKey 和可选的 cacheDuration

Future<List<Country>> fetchCountries() async {
  return await network<List<Country>>(
    request: (request) => request.get("/countries"),
    cacheKey: "app_countries",
    cacheDuration: const Duration(hours: 1),
  ) ?? [];
}

清除缓存

// Clear a specific cache key
await apiService.clearCache("app_countries");

// Clear all API cache
await apiService.clearAllCache();

使用 api() 辅助方法进行缓存

api<ApiService>(
  (request) => request.fetchCountries(),
  cacheKey: "app_countries",
  cacheDuration: const Duration(hours: 1),
);

缓存策略

使用 CachePolicy 对缓存行为进行细粒度控制:

策略 描述
CachePolicy.networkOnly 始终从网络获取(默认)
CachePolicy.cacheFirst 先尝试缓存,回退到网络
CachePolicy.networkFirst 先尝试网络,回退到缓存
CachePolicy.cacheOnly 仅使用缓存,为空则报错
CachePolicy.staleWhileRevalidate 立即返回缓存,后台更新

用法

Future<List<Country>> fetchCountries() async {
  return await network<List<Country>>(
    request: (request) => request.get("/countries"),
    cacheKey: "app_countries",
    cacheDuration: const Duration(hours: 1),
    cachePolicy: CachePolicy.staleWhileRevalidate,
  ) ?? [];
}

何时使用每种策略

  • cacheFirst -- 很少变化的数据。立即返回缓存数据,仅在缓存为空时从网络获取。
  • networkFirst -- 应尽可能保持最新的数据。先尝试网络,失败时回退到缓存。
  • staleWhileRevalidate -- 需要即时响应但应保持更新的 UI。返回缓存数据,然后在后台刷新。
  • cacheOnly -- 离线模式。如果没有缓存数据则抛出错误。

注意: 如果您提供了 cacheKeycacheDuration 但未指定 cachePolicy,默认策略为 cacheFirst

重试失败的请求

自动重试失败的请求。

基本重试

Future fetchUsers() async {
  return await network(
    request: (request) => request.get("/users"),
    retry: 3,
  );
}

带延迟的重试

Future fetchUsers() async {
  return await network(
    request: (request) => request.get("/users"),
    retry: 3,
    retryDelay: Duration(seconds: 2),
  );
}

条件重试

Future fetchUsers() async {
  return await network(
    request: (request) => request.get("/users"),
    retry: 3,
    retryIf: (DioException dioException) {
      // Only retry on server errors
      return dioException.response?.statusCode == 500;
    },
  );
}

服务级别重试

apiService.setRetry(3);
apiService.setRetryDelay(Duration(seconds: 2));
apiService.setRetryIf((dioException) => dioException.response?.statusCode == 500);

连接检查

在设备离线时快速失败,而不是等待超时。

服务级别

class ApiService extends NyApiService {
  ApiService({BuildContext? buildContext}) : super(buildContext, decoders: modelDecoders);

  @override
  bool get checkConnectivityBeforeRequest => true;
  ...
}

每个请求

await network(
  request: (request) => request.get("/users"),
  checkConnectivity: true,
);

动态设置

apiService.setCheckConnectivityBeforeRequest(true);

启用后,当设备离线时:

  • networkFirst 策略回退到缓存
  • 其他策略立即抛出 DioExceptionType.connectionError

取消令牌

管理和取消待处理的请求。

// Create a managed cancel token
final token = apiService.createCancelToken();
await apiService.get('/endpoint', cancelToken: token);

// Cancel all pending requests (e.g., on logout)
apiService.cancelAllRequests('User logged out');

// Check active request count
int count = apiService.activeRequestCount;

// Clean up a specific token when done
apiService.removeCancelToken(token);

设置认证请求头

重写 setAuthHeaders 以在每个请求上附加认证请求头。当 shouldSetAuthHeaderstrue(默认值)时,此方法在每个请求之前调用。

class ApiService extends NyApiService {
  ...

  @override
  Future<RequestHeaders> setAuthHeaders(RequestHeaders headers) async {
    String? myAuthToken = Auth.data(field: 'token');
    if (myAuthToken != null) {
      headers.addBearerToken(myAuthToken);
    }
    return headers;
  }
}

禁用认证请求头

对于不需要认证的公共端点:

// Per-request
await network(
  request: (request) => request.get("/public-endpoint"),
  shouldSetAuthHeaders: false,
);

// Service-level
apiService.setShouldSetAuthHeaders(false);

另请参阅: 认证 了解用户认证和令牌存储的详细信息。

刷新令牌

重写 shouldRefreshTokenrefreshToken 以处理令牌过期。这些方法在每个请求之前调用。

class ApiService extends NyApiService {
  ...

  @override
  Future<bool> shouldRefreshToken() async {
    // Check if the token needs refreshing
    return false;
  }

  @override
  Future<void> refreshToken(Dio dio) async {
    // Use the fresh Dio instance (no interceptors) to refresh the token
    dynamic response = (await dio.post("https://example.com/refresh-token")).data;

    // Save the new token to storage
    await Auth.set((data) {
      data['token'] = response['token'];
      return data;
    });
  }
}

refreshToken 中的 dio 参数是一个新的 Dio 实例,与服务的主实例分离,以避免拦截器循环。

单例 API 服务

默认情况下,api 辅助方法每次都会创建新实例。要使用单例,在 config/decoders.dart 中传递实例而不是工厂:

final Map<Type, dynamic> apiDecoders = {
  ApiService: () => ApiService(), // New instance each time

  ApiService: ApiService(), // Singleton — same instance always
};

高级配置

自定义 Dio 初始化

class ApiService extends NyApiService {
  ApiService({BuildContext? buildContext}) : super(
    buildContext,
    decoders: modelDecoders,
    initDio: (Dio dio) {
      dio.options.validateStatus = (status) => status! < 500;
      return dio;
    },
  );
}

访问 Dio 实例

Dio dioInstance = apiService.dio;

Response response = await dioInstance.request(
  '/custom-endpoint',
  options: Options(method: 'OPTIONS'),
);

分页辅助方法

apiService.setPagination(
  1,
  paramPage: 'page',
  paramPerPage: 'per_page',
  perPage: '20',
);

事件回调

apiService.onSuccess((response, data) {
  print('Success: ${response.statusCode}');
});

apiService.onError((dioException) {
  print('Error: ${dioException.message}');
});

可重写的属性

属性 类型 默认值 描述
baseUrl String "" 所有请求的基础 URL
interceptors Map<Type, Interceptor> {} Dio 拦截器
decoders Map<Type, dynamic>? {} JSON 转换的模型解码器
shouldSetAuthHeaders bool true 是否在请求前调用 setAuthHeaders
retry int 0 默认重试次数
retryDelay Duration 1 second 默认重试延迟
checkConnectivityBeforeRequest bool false 请求前检查连接