MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter平台差异的网络请求:处理iOS与Android的网络配置

2023-10-226.8k 阅读

1. Flutter 网络请求基础

在 Flutter 应用开发中,网络请求是非常常见的操作,无论是获取数据展示给用户,还是将用户数据上传到服务器。Flutter 提供了多种方式来进行网络请求,其中最常用的是 http 包。

首先,在 pubspec.yaml 文件中添加 http 依赖:

dependencies:
  http: ^0.13.5

然后导入包:

import 'package:http/http.dart' as http;

简单的 GET 请求示例:

Future<String> fetchData() async {
  final response = await http.get(Uri.parse('https://example.com/api/data'));
  if (response.statusCode == 200) {
    return response.body;
  } else {
    throw Exception('Failed to load data');
  }
}

POST 请求示例:

Future<String> postData(Map<String, dynamic> data) async {
  final response = await http.post(
    Uri.parse('https://example.com/api/data'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(data),
  );
  if (response.statusCode == 200) {
    return response.body;
  } else {
    throw Exception('Failed to post data');
  }
}

2. iOS 网络配置

2.1 ATS 配置

在 iOS 9 及更高版本中,苹果引入了 App Transport Security (ATS)。ATS 要求应用与服务器之间的网络连接使用 HTTPS,并且对证书验证等方面有严格要求。如果服务器的配置不符合 ATS 标准,Flutter 应用在 iOS 上进行网络请求时可能会失败。

要配置 ATS,可以在 ios/Runner/Info.plist 文件中进行设置。

允许所有 HTTP 请求(不推荐,因为存在安全风险)

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

允许特定的 HTTP 请求

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>example.com</key>
        <dict>
            <key>NSIncludesSubdomains</key>
            <true/>
            <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSTemporaryExceptionMinimumTLSVersion</key>
            <string>TLSv1.2</string>
        </dict>
    </dict>
</dict>

在上述配置中,我们针对 example.com 及其子域名允许了不安全的 HTTP 加载,并且指定了最小的 TLS 版本为 1.2。

2.2 证书验证

如果服务器使用的是自签名证书,默认情况下,iOS 会拒绝连接。可以通过配置 Info.plist 来信任自签名证书。

添加以下内容到 Info.plist

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>example.com</key>
        <dict>
            <key>NSIncludesSubdomains</key>
            <true/>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSExceptionRequiresForwardSecrecy</key>
            <false/>
            <key>NSExceptionMinimumTLSVersion</key>
            <string>TLSv1.2</string>
            <key>NSExceptionRequiresValidBSSLCertificate</key>
            <false/>
        </dict>
    </dict>
</dict>

这里通过设置 NSExceptionRequiresValidBSSLCertificatefalse 来绕过证书有效性检查。不过,这同样存在安全风险,仅适用于开发和测试环境。

3. Android 网络配置

3.1 网络权限

在 Android 中,应用需要声明网络权限才能进行网络请求。在 android/app/src/main/AndroidManifest.xml 文件中添加以下权限声明:

<uses-permission android:name="android.permission.INTERNET" />

3.2 信任自签名证书

与 iOS 类似,Android 也对证书验证有严格要求。如果服务器使用自签名证书,默认情况下网络请求会失败。要信任自签名证书,可以通过自定义 OkHttpClient 并添加证书到信任列表。

首先,将自签名证书添加到 android/app/src/main/res/raw 目录下,假设证书名为 my_cert.crt

然后,在 Flutter 中配置 OkHttpClient

import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'dart:io';

Future<String> fetchDataWithCustomCert() async {
  final securityContext = SecurityContext();
  securityContext.setTrustedCertificatesBytes(
      rootBundle.load('assets/my_cert.crt').then((value) => value.buffer.asUint8List()));
  final client = IOClient(HttpClient()..badCertificateCallback =
      (X509Certificate cert, String host, int port) => true, securityContext: securityContext);
  final response = await client.get(Uri.parse('https://example.com/api/data'));
  if (response.statusCode == 200) {
    return response.body;
  } else {
    throw Exception('Failed to load data');
  }
}

上述代码中,我们使用 SecurityContext 加载证书,并创建了一个 IOClient 来进行网络请求,同时设置了 badCertificateCallback 来绕过证书验证。

3.3 HTTP/2 支持

从 Android 5.0(API 级别 21)开始,Android 原生支持 HTTP/2。Flutter 应用默认也会使用 HTTP/2 进行网络请求。但是,如果服务器端配置不正确,可能会导致问题。

要确保服务器正确配置 HTTP/2,并且在 Android 端可以通过设置 OkHttpClient 来优化 HTTP/2 的使用。例如,可以设置连接池大小:

import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'dart:io';

final client = IOClient(HttpClient()
  ..connectionTimeout = const Duration(seconds: 10)
  ..idleTimeout = const Duration(seconds: 10)
  ..maxConnectionsPerHost = 10);

Future<String> fetchDataWithHttp2() async {
  final response = await client.get(Uri.parse('https://example.com/api/data'));
  if (response.statusCode == 200) {
    return response.body;
  } else {
    throw Exception('Failed to load data');
  }
}

通过设置 maxConnectionsPerHost 等参数,可以优化 HTTP/2 连接的使用,提高网络请求的效率。

4. 平台差异处理策略

4.1 使用条件编译

Flutter 支持条件编译,可以根据不同的平台执行不同的代码。例如,对于 iOS 的 ATS 配置和 Android 的网络权限等特定平台的配置,可以使用条件编译来进行区分。

import 'package:flutter/foundation.dart';

void configureNetwork() {
  if (kIsWeb) {
    // Web 平台的网络配置
  } else if (defaultTargetPlatform == TargetPlatform.iOS) {
    // iOS 平台的网络配置,例如检查 ATS 配置
  } else if (defaultTargetPlatform == TargetPlatform.android) {
    // Android 平台的网络配置,例如检查网络权限
  }
}

4.2 抽象网络层

为了更好地管理平台差异,可以抽象出网络层。创建一个统一的网络请求接口,然后针对不同平台实现具体的网络请求逻辑。

定义网络请求接口:

abstract class NetworkService {
  Future<String> get(String url);
  Future<String> post(String url, Map<String, dynamic> data);
}

iOS 平台实现:

class iOSNetworkService implements NetworkService {
  @override
  Future<String> get(String url) async {
    // 处理 iOS 特定的网络配置,如 ATS 相关
    final response = await http.get(Uri.parse(url));
    if (response.statusCode == 200) {
      return response.body;
    } else {
      throw Exception('Failed to load data');
    }
  }

  @override
  Future<String> post(String url, Map<String, dynamic> data) async {
    // 处理 iOS 特定的网络配置,如 ATS 相关
    final response = await http.post(
      Uri.parse(url),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(data),
    );
    if (response.statusCode == 200) {
      return response.body;
    } else {
      throw Exception('Failed to post data');
    }
  }
}

Android 平台实现:

class AndroidNetworkService implements NetworkService {
  @override
  Future<String> get(String url) async {
    // 处理 Android 特定的网络配置,如权限等
    final response = await http.get(Uri.parse(url));
    if (response.statusCode == 200) {
      return response.body;
    } else {
      throw Exception('Failed to load data');
    }
  }

  @override
  Future<String> post(String url, Map<String, dynamic> data) async {
    // 处理 Android 特定的网络配置,如权限等
    final response = await http.post(
      Uri.parse(url),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(data),
    );
    if (response.statusCode == 200) {
      return response.body;
    } else {
      throw Exception('Failed to post data');
    }
  }
}

在应用中根据平台选择具体的实现:

NetworkService getNetworkService() {
  if (defaultTargetPlatform == TargetPlatform.iOS) {
    return iOSNetworkService();
  } else if (defaultTargetPlatform == TargetPlatform.android) {
    return AndroidNetworkService();
  }
  throw UnsupportedError('Unsupported platform');
}

5. 常见问题及解决方法

5.1 网络请求超时

无论是 iOS 还是 Android,网络请求都可能会因为各种原因超时。在 Flutter 中,可以通过设置 http.ClientconnectionTimeoutreadTimeout 来控制超时时间。

import 'package:http/http.dart' as http;

Future<String> fetchDataWithTimeout() async {
  final client = http.Client();
  try {
    final response = await client.get(
      Uri.parse('https://example.com/api/data'),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      timeout: const Duration(seconds: 10),
    );
    if (response.statusCode == 200) {
      return response.body;
    } else {
      throw Exception('Failed to load data');
    }
  } catch (e) {
    if (e is TimeoutException) {
      throw Exception('Network request timed out');
    }
    rethrow;
  } finally {
    client.close();
  }
}

5.2 证书错误

如前文所述,iOS 和 Android 在证书验证方面有不同的机制。如果遇到证书错误,首先要检查服务器证书的有效性。对于自签名证书,按照前文介绍的方法在相应平台进行信任配置。

在 iOS 上,如果配置了 ATS 但仍然遇到证书问题,仔细检查 Info.plist 中的配置是否正确。在 Android 上,确保自定义的 OkHttpClient 正确加载了证书。

5.3 网络权限问题

在 Android 上,如果网络请求失败并且没有权限声明,会出现权限相关的错误。确保在 AndroidManifest.xml 中正确声明了网络权限。

在 iOS 上,虽然没有像 Android 那样明确的权限声明,但 ATS 的配置不当也可能导致网络请求失败,需要仔细检查 ATS 相关配置。

6. 性能优化

6.1 缓存策略

为了提高网络请求的性能,可以实现缓存策略。在 Flutter 中,可以使用 http_cache 等包来实现缓存。

首先,在 pubspec.yaml 文件中添加依赖:

dependencies:
  http_cache: ^0.1.0

使用示例:

import 'package:http/http.dart' as http;
import 'package:http_cache/http_cache.dart';

Future<String> fetchDataWithCache() async {
  final client = http.ClientWithCache();
  final response = await client.get(Uri.parse('https://example.com/api/data'));
  if (response.statusCode == 200) {
    return response.body;
  } else {
    throw Exception('Failed to load data');
  }
}

http_cache 包会自动处理缓存逻辑,根据请求的 URL 和响应头来决定是否使用缓存数据。

6.2 并发请求优化

在处理多个网络请求时,可以通过合理管理并发请求来提高性能。例如,使用 Future.wait 来同时发起多个请求,但要注意控制并发数量,避免过度占用资源。

Future<List<String>> fetchMultipleData() async {
  final requests = [
    fetchData('https://example.com/api/data1'),
    fetchData('https://example.com/api/data2'),
    fetchData('https://example.com/api/data3'),
  ];
  return Future.wait(requests);
}

如果有大量请求,可以使用 Stream 来处理,以避免一次性加载过多数据导致内存问题。

Stream<String> fetchDataStream() async* {
  final urls = [
    'https://example.com/api/data1',
    'https://example.com/api/data2',
    'https://example.com/api/data3',
  ];
  for (final url in urls) {
    yield await fetchData(url);
  }
}

7. 安全考虑

7.1 数据加密

在进行网络请求时,尤其是涉及用户敏感数据时,数据加密非常重要。可以使用 encrypt 包来对数据进行加密。

首先,添加依赖:

dependencies:
  encrypt: ^5.0.0

加密示例:

import 'package:encrypt/encrypt.dart';

void encryptData() {
  final key = Key.fromUtf8('my 32 length key................');
  final iv = IV.fromLength(16);
  final encrypter = Encrypter(AES(key));
  final plainText = 'sensitive data';
  final encrypted = encrypter.encrypt(plainText, iv: iv);
  print(encrypted.base64);
}

在网络请求中,将加密后的数据发送到服务器,服务器端再进行解密。

7.2 防止中间人攻击

为了防止中间人攻击,除了使用 HTTPS 外,还可以验证服务器证书的公钥。在 Android 上,可以通过 OkHttpClientCertificatePinner 来实现。

import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'dart:io';

Future<String> fetchDataWithCertificatePinning() async {
  final securityContext = SecurityContext();
  final client = IOClient(HttpClient()
    ..badCertificateCallback =
        (X509Certificate cert, String host, int port) => true,
    certificatePinner: CertificatePinner(
      pins: {
        'example.com': [
          'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
        ],
      },
    ),
    securityContext: securityContext);
  final response = await client.get(Uri.parse('https://example.com/api/data'));
  if (response.statusCode == 200) {
    return response.body;
  } else {
    throw Exception('Failed to load data');
  }
}

在上述代码中,通过 CertificatePinner 验证服务器证书的公钥,只有公钥匹配的服务器才能建立连接,从而防止中间人攻击。

8. 测试网络请求

8.1 单元测试

在 Flutter 中,可以使用 flutter_test 包来进行网络请求的单元测试。对于网络请求相关的函数,可以使用 Mockito 来模拟 http.Client 的行为。

首先,添加依赖:

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.0.0

单元测试示例:

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:mockito/mockito.dart';

class MockClient extends Mock implements http.Client {}

void main() {
  test('fetchData should return data on success', () async {
    final client = MockClient();
    when(client.get(Uri.parse('https://example.com/api/data')))
      .thenAnswer((_) async => http.Response('{"data": "test"}', 200));
    final result = await fetchData(client);
    expect(result, '{"data": "test"}');
  });

  test('fetchData should throw on failure', () async {
    final client = MockClient();
    when(client.get(Uri.parse('https://example.com/api/data')))
      .thenAnswer((_) async => http.Response('Error', 400));
    expect(() => fetchData(client), throwsException);
  });
}

Future<String> fetchData(http.Client client) async {
  final response = await client.get(Uri.parse('https://example.com/api/data'));
  if (response.statusCode == 200) {
    return response.body;
  } else {
    throw Exception('Failed to load data');
  }
}

在上述测试中,我们使用 MockClient 模拟了 http.Client 的行为,分别测试了网络请求成功和失败的情况。

8.2 集成测试

对于集成测试,可以使用 flutter_driver 包。集成测试可以测试整个应用在真实环境中的网络请求行为。

首先,添加依赖:

dev_dependencies:
  flutter_driver:
    sdk: flutter

编写集成测试代码,例如在 test_driver 目录下创建 network_test.dart

import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Network requests', () {
    FlutterDriver driver;

    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    tearDownAll(() async {
      if (driver != null) {
        driver.close();
      }
    });

    test('App can fetch data', () async {
      // 假设应用中有一个按钮触发网络请求并显示结果
      await driver.tap(find.byValueKey('fetchButton'));
      final result = await driver.getText(find.byValueKey('resultText'));
      expect(result, isNotEmpty);
    });
  });
}

通过集成测试,可以确保应用在实际运行环境中网络请求的功能正常。

9. 总结

处理 Flutter 应用在 iOS 和 Android 平台上的网络配置差异是开发过程中的重要环节。通过正确配置 ATS、网络权限、证书验证等,以及采用合适的平台差异处理策略,可以确保网络请求在不同平台上稳定、高效、安全地运行。同时,通过性能优化、安全考虑和测试等方面的工作,可以进一步提升应用的质量和用户体验。在实际开发中,需要根据项目的具体需求和服务器配置,灵活运用这些知识和技巧,打造出优秀的 Flutter 应用。

以上就是关于 Flutter 平台差异的网络请求处理的详细内容,希望对开发者在处理相关问题时有所帮助。