fix(flutter): 修复 iOS 启动网络加载

This commit is contained in:
Zhou Weidong committed 2026-10-20 18:01:12 +08:00
1 parent 23059578d2
commit a63334f5a5
3 files changed
+129 -26

No files matched your search

+33
View File
@@ -28,8 +28,41 @@
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>127.0.0.1</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>192.168.31.187</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>39.162.50.99</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>开发阶段需要访问同一局域网内的短剧内容服务。</string>
+42 -22
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import 'dart:io';
import 'package:flutter/foundation.dart';
@@ -22,12 +24,17 @@ class AppConfig {
/// 公网地址(远程/移动网络访问时使用)。
static const String publicBaseUrl = 'http://39.162.50.99:8081';
static const String _loopbackBaseUrl = 'http://127.0.0.1:8081';
static const String _overrideBaseUrl = String.fromEnvironment('API_BASE_URL');
static String _runtimeBaseUrl = '';
static const Duration _probeTimeout = Duration(seconds: 2);
static const List<String> _probeCandidates = [
lanBaseUrl,
devHostBaseUrl,
publicBaseUrl,
];
/// Build-time server URL supplied through `--dart-define=API_BASE_URL`.
static String get compiledBaseUrl => _overrideBaseUrl;
@@ -40,7 +47,7 @@ class AppConfig {
: (compiledBaseUrl.isNotEmpty ? compiledBaseUrl : defaultBaseUrl);
static final String _defaultBaseUrl = _runsOnDevMachine
? _loopbackBaseUrl
? lanBaseUrl
: devHostBaseUrl;
/// 模拟器、桌面端与开发机同机,环回地址可用;iOS 真机不满足。
@@ -66,29 +73,42 @@ class AppConfig {
static const Duration requestTimeout = Duration(seconds: 8);
/// 探测内网地址是否可达,可达则切换为内网直连,否则回落到公网地址。
/// 按环回、开发机、公网顺序探测,写入首个返回 200 的地址。
///
/// Wi-Fi 下内网直连更快;移动数据下内网不可达,自动回落到公网端口映射。
static Future<void> probeAndApplyBaseUrl() async {
try {
final client = HttpClient()
..connectionTimeout = const Duration(seconds: 2);
final request = await client.getUrl(
Uri.parse('$lanBaseUrl/app/categories'),
);
final response = await request.close().timeout(const Duration(seconds: 2));
await response.drain<void>();
client.close(force: true);
if (response.statusCode == 200) {
applyRuntimeBaseUrl(lanBaseUrl);
/// 全部不可达时使用平台默认地址,真机保留开发机地址,避免退回公网。
static Future<void> probeAndApplyBaseUrl({
Future<bool> Function(String baseUrl)? probe,
String? fallbackBaseUrl,
}) async {
final candidateProbe = probe ?? _probeBaseUrl;
for (final candidate in _probeCandidates) {
if (await candidateProbe(candidate)) {
applyRuntimeBaseUrl(candidate);
return;
}
} catch (_) {
// 内网不可达,回落到公网。
}
applyRuntimeBaseUrl(
compiledBaseUrl.isNotEmpty ? compiledBaseUrl : publicBaseUrl,
);
applyRuntimeBaseUrl(fallbackBaseUrl ?? defaultBaseUrl);
}
static Future<bool> _probeBaseUrl(String baseUrl) async {
final client = HttpClient()..connectionTimeout = _probeTimeout;
try {
return await _requestCategories(
client,
Uri.parse('$baseUrl/app/categories'),
).timeout(_probeTimeout);
} catch (_) {
return false;
} finally {
client.close(force: true);
}
}
static Future<bool> _requestCategories(HttpClient client, Uri uri) async {
final request = await client.getUrl(uri);
final response = await request.close();
await response.drain<void>();
return response.statusCode == 200;
}
/// 后端返回的是相对路径(/files/...),这里补上服务地址。
+54 -4
View File
@@ -60,6 +60,59 @@ void main() {
expect(AppConfig.devHostBaseUrl, 'http://192.168.31.187:8081');
});
test('should_keep_loopback_priority_when_probe_succeeds', () async {
final attempts = <String>[];
await AppConfig.probeAndApplyBaseUrl(
fallbackBaseUrl: AppConfig.devHostBaseUrl,
probe: (baseUrl) async {
attempts.add(baseUrl);
return baseUrl == AppConfig.lanBaseUrl;
},
);
expect(attempts, [AppConfig.lanBaseUrl]);
expect(AppConfig.apiBaseUrl, AppConfig.lanBaseUrl);
});
test('should_probe_loopback_dev_host_then_public_in_order', () async {
final attempts = <String>[];
await AppConfig.probeAndApplyBaseUrl(
fallbackBaseUrl: AppConfig.devHostBaseUrl,
probe: (baseUrl) async {
attempts.add(baseUrl);
return baseUrl == AppConfig.publicBaseUrl;
},
);
expect(attempts, [
AppConfig.lanBaseUrl,
AppConfig.devHostBaseUrl,
AppConfig.publicBaseUrl,
]);
expect(AppConfig.apiBaseUrl, AppConfig.publicBaseUrl);
});
test('should_use_platform_default_when_all_probes_fail', () async {
final attempts = <String>[];
await AppConfig.probeAndApplyBaseUrl(
fallbackBaseUrl: AppConfig.devHostBaseUrl,
probe: (baseUrl) async {
attempts.add(baseUrl);
return false;
},
);
expect(attempts, [
AppConfig.lanBaseUrl,
AppConfig.devHostBaseUrl,
AppConfig.publicBaseUrl,
]);
expect(AppConfig.apiBaseUrl, AppConfig.devHostBaseUrl);
});
test('should_apply_stored_url_only_when_debug_is_enabled', () async {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.withData({
@@ -81,10 +134,7 @@ void main() {
isNull,
);
expect(
await ServerEndpoint.save(' http://192.168.31.187:8081/ '),
isTrue,
);
expect(await ServerEndpoint.save(' http://192.168.31.187:8081/ '), isTrue);
expect(
await SharedPreferencesAsync().getString(ServerEndpoint.preferenceKey),
'http://192.168.31.187:8081',