Giới thiệu
Trong bối cảnh phát triển ứng dụng di động hiện nay, mô hình phát triển hybrid (kết hợp giữa Native và Flutter) đang dần trở thành xu hướng phổ biến. Đặc biệt, trong trường hợp của chúng tôi, việc xử lý các yêu cầu mạng trên Android thực hiện thông qua OkHttp cho lớp native và Dio cho Flutter đã đưa ra một số thách thức đáng kể. Bài viết này sẽ hướng dẫn bạn cách chuyển tiếp các yêu cầu mạng từ Flutter sang lớp native một cách hiệu quả.
Những vấn đề cần giải quyết
1. Mã lặp lại
Khi áp dụng mô hình hybrid, các yêu cầu mạng cần phải được quản lý thông qua hai bộ mã khác nhau: một cho lớp native và một cho Flutter. Điều này dẫn đến việc phải viết mã lặp lại cho các cơ chế như retry và caching. Ví dụ, nếu người dùng cần được chuyển hướng đến trang đăng nhập khi token hết hạn, bạn sẽ phải viết mã hai lần.
2. Khó khăn khi sử dụng công cụ phân tích lưu lượng mạng
Việc sử dụng công cụ phân tích như Charles để bắt gói tin trở nên rắc rối. Flutter's Dio thường bỏ qua cài đặt proxy của điện thoại, điều này có nghĩa là bạn phải cấu hình proxy trong mã. Ngược lại, việc sử dụng các thành phần mạng native rất dễ dàng với Charles.
Giải pháp tổng quát
Dựa trên các vấn đề nêu trên, chúng tôi quyết định ủy thác tất cả các yêu cầu mạng hiện có và yêu cầu mạng mới trong ứng dụng cho lớp native. Flutter chỉ cần phân tích kết quả của các yêu cầu mạng và hiển thị UI.
Hiểu về Dio
Dio là một thư viện mạnh mẽ cho việc xử lý yêu cầu mạng trong Flutter. Nó cung cấp một cách để tùy chỉnh các yêu cầu mạng, cho phép bạn thực hiện việc gửi và nhận yêu cầu mạng theo cách của riêng mình. Để hiểu rõ hơn, chúng ta sẽ xem xét quá trình yêu cầu mạng của Dio.
Quá trình yêu cầu mạng của Dio
Đầu tiên, chúng ta sẽ tạo một yêu cầu GET đơn giản. Dưới đây là mã ví dụ:
dart
Future<Response<dynamic>> _dispatchRequest<T>(RequestOptions reqOpt) async {
final responseBody = await httpClientAdapter.fetch(
reqOpt,
stream,
cancelToken?.whenCancel,
);
final headers = Headers.fromMap(responseBody.headers);
responseBody.headers = headers.map;
final ret = Response<dynamic>(
headers: headers,
requestOptions: reqOpt,
redirects: responseBody.redirects ?? [],
isRedirect: responseBody.isRedirect,
statusCode: responseBody.statusCode,
statusMessage: responseBody.statusMessage,
extra: responseBody.extra,
);
if (statusOk || reqOpt.receiveDataWhenStatusError == true) {
Object? data = await transformer.transformResponse(
reqOpt,
responseBody,
);
if (data is String && data.isEmpty && T != dynamic && T != String && reqOpt.responseType == ResponseType.json) {
data = null;
}
ret.data = data;
}
}
Trong đoạn mã trên, chúng ta có ba điểm quan trọng cần lưu ý:
- Điểm 1: Lấy đối tượng responseBody thông qua httpClientAdapter. Đây là nơi thực hiện logic của yêu cầu mạng HTTP và trả về responseBody sau khi thực hiện.
- Điểm 2: Tạo một đối tượng response. responseBody chưa phải là phản hồi cuối cùng, nó chỉ là phản hồi khi kết nối mạng đã được thiết lập.
- Điểm 3: Đọc nội dung của stream và sử dụng nó như nội dung dữ liệu cho phản hồi mạng thực tế.
Để chuyển tiếp yêu cầu mạng sang lớp native, chúng ta cần thay thế quy trình yêu cầu mạng để các yêu cầu thực tế được thực hiện ở phía native và trả về phản hồi. Dio hỗ trợ cung cấp tùy chỉnh httpClientAdapter và transformer để kết nối với lớp native.
HttpClientAdapter và HttpClient
HttpClientAdapter là cầu nối giữa Dio và HttpClient. Dio cung cấp API thân thiện cho các nhà phát triển, trong khi HttpClient thực sự thực hiện việc gửi và nhận yêu cầu mạng. Chúng ta có thể cung cấp HttpClient riêng của mình thông qua HttpClientAdapter thay vì sử dụng mặc định.
Giải pháp tổng quát
Sau khi nắm rõ quy trình của Dio, tôi đã tùy chỉnh một NativeClientAdapter, ghi đè việc thực hiện phương thức fetch. Trong fetch, thay vì sử dụng http client mặc định, nó sẽ gửi các tham số yêu cầu đến lớp native thông qua method channel. Sau khi lớp native nhận được, nó sẽ gửi yêu cầu mạng và trả về phản hồi cho Flutter.
Triển khai mã
dart
class NativeNetDelegate {
final dio = Dio();
NativeNetDelegate(String gateway) {
dio.httpClientAdapter = NativeClientAdapter();
dio.transformer = NativeTransformer();
dio.options.baseUrl = Base.baseUrl + gateway;
}
}
Trong mã trên, tôi đã tùy chỉnh một Dio, sử dụng NativeClientAdapter và NativeTransformer của riêng mình. NativeTransformer dùng để chuyển đổi dữ liệu trả về từ lớp native.
dart
class NativeClientAdapter implements HttpClientAdapter {
@override
Future<ResponseBody> fetch(RequestOptions options, Stream<Uint8List>? requestStream, Future<void>? cancelFuture) async {
NativeRequestOption nativeRequestOption = NativeRequestOption().compose(options);
dynamic result = await FlutterMethodHelper.instance.sendHttpRequest(nativeRequestOption.toJson());
int httpCode = result['httpCode'];
String data = result['data'];
return NativeResponseBody(fakeStream(), httpCode)..data = data;
}
Stream<Uint8List> fakeStream() async* {}
@override
void close({bool force = false}) {}
}
Tương tự, chúng ta có NativeTransformer:
dart
class NativeTransformer implements Transformer {
@override
Future<String> transformRequest(RequestOptions options) async {
return "";
}
@override
Future<dynamic> transformResponse(RequestOptions options, ResponseBody responseBody) async {
if (responseBody is NativeResponseBody) {
if (responseBody.data != null) {
Map<String, dynamic> data = jsonDecode(responseBody.data!);
return data;
} else {
return "";
}
} else {
throw DioException(requestOptions: options, message: "no support responseBody type");
}
}
}
Cuối cùng, chúng ta cần một lớp để đại diện cho tham số yêu cầu:
dart
class NativeRequestOption {
String? baseUrl;
String? path;
String? method;
String? data;
Map<String, dynamic>? queryParameters;
NativeRequestOption compose(RequestOptions options) {
baseUrl = options.baseUrl;
path = options.path;
method = options.method;
data = options.data?.toString();
queryParameters = options.queryParameters;
return this;
}
}
Kết luận
Bài viết này đã cung cấp một cái nhìn tổng quan về cách chuyển tiếp yêu cầu mạng từ Flutter sang lớp native. Thực hiện theo các bước hướng dẫn và mã mẫu, bạn có thể tùy chỉnh HttpClientAdapter để đáp ứng nhu cầu của riêng bạn. Nếu bạn thấy bổ ích, hãy chia sẻ và để lại ý kiến của bạn!