在上一篇文章中,我與你分享了如何為一個 Flutter 工程編寫自動化測試用例。設計一個測試用例的基本步驟可以分為 3 步,即定義、執行和驗證,而 Flutter 提供的單元測試和 UI 測試框架則可以幫助我們簡化這些步驟。
其中,通過單元測試,我們可以很方便地驗證單個函數、方法或類的行為,還可以利用 mockito 定制外部依賴返回任意數據,從而讓測試更可控;而 UI 測試則提供了與 Widget 交互的能力,我們可以模仿用戶行為,對應用進行相應的交互操作,確認其功能是否符合預期。
通過自動化測試,我們可以把重復的人工操作變成自動化的驗證步驟,從而在開發階段更及時地發現問題。但終端設備的碎片化,使得我們終究無法在應用開發期就完全模擬出真實用戶的運行環境。所以,無論我們的應用寫得多么完美、測試得多么全面,總是無法完全避免線上的異常問題。
這些異常,可能是因為不充分的機型適配、用戶糟糕的網絡狀況;也可能是因為 Flutter 框架自身的 Bug,甚至是操作系統底層的問題。這些異常一旦發生,Flutter 應用會無法響應用戶的交互事件,輕則報錯,重則功能無法使用甚至閃退,這對用戶來說都相當不友好,是開發者最不愿意看到的。
所以,我們要想辦法去捕獲用戶的異常信息,將異常現場保存起來,并上傳至服務器,這樣我們就可以分析異常上下文,定位引起異常的原因,去解決此類問題了。那么今天,我們就一起來學習下 Flutter 異常的捕獲和信息采集,以及對應的數據上報處理。
## Flutter 異常
Flutter 異常指的是,Flutter 程序中 Dart 代碼運行時意外發生的錯誤事件。我們可以通過與 Java 類似的 try-catch 機制來捕獲它。但**與 Java 不同的是,Dart 程序不強制要求我們必須處理異常**。
這是因為,Dart 采用事件循環的機制來運行任務,所以各個任務的運行狀態是互相獨立的。也就是說,即便某個任務出現了異常我們沒有捕獲它,Dart 程序也不會退出,只會導致當前任務后續的代碼不會被執行,用戶仍可以繼續使用其他功能。
Dart 異常,根據來源又可以細分為 App 異常和 Framework 異常。Flutter 為這兩種異常提供了不同的捕獲方式,接下來我們就一起看看吧。
## App 異常的捕獲方式
App 異常,就是應用代碼的異常,通常由未處理應用層其他模塊所拋出的異常引起。根據異常代碼的執行時序,App 異常可以分為兩類,即同步異常和異步異常:同步異常可以通過 try-catch 機制捕獲,異步異常則需要采用 Future 提供的 catchError 語句捕獲。
這兩種異常的捕獲方式,如下代碼所示:
~~~
// 使用 try-catch 捕獲同步異常
try {
throw StateError('This is a Dart exception.');
}
catch(e) {
print(e);
}
// 使用 catchError 捕獲異步異常
Future.delayed(Duration(seconds: 1))
.then((e) => throw StateError('This is a Dart exception in Future.'))
.catchError((e)=>print(e));
// 注意,以下代碼無法捕獲異步異常
try {
Future.delayed(Duration(seconds: 1))
.then((e) => throw StateError('This is a Dart exception in Future.'))
}
catch(e) {
print("This line will never be executed. ");
}
~~~
需要注意的是,這兩種方式是不能混用的。可以看到,在上面的代碼中,我們是無法使用 try-catch 去捕獲一個異步調用所拋出的異常的。
同步的 try-catch 和異步的 catchError,為我們提供了直接捕獲特定異常的能力,而如果我們想集中管理代碼中的所有異常,Flutter 也提供了 Zone.runZoned 方法。
我們可以給代碼執行對象指定一個 Zone,在 Dart 中,Zone 表示一個代碼執行的環境范圍,其概念類似沙盒,不同沙盒之間是互相隔離的。如果我們想要觀察沙盒中代碼執行出現的異常,沙盒提供了 onError 回調函數,攔截那些在代碼執行對象中的未捕獲異常。
在下面的代碼中,我們將可能拋出異常的語句放置在了 Zone 里。可以看到,在沒有使用 try-catch 和 catchError 的情況下,無論是同步異常還是異步異常,都可以通過 Zone 直接捕獲到:
~~~
runZoned(() {
// 同步拋出異常
throw StateError('This is a Dart exception.');
}, onError: (dynamic e, StackTrace stack) {
print('Sync error caught by zone');
});
runZoned(() {
// 異步拋出異常
Future.delayed(Duration(seconds: 1))
.then((e) => throw StateError('This is a Dart exception in Future.'));
}, onError: (dynamic e, StackTrace stack) {
print('Async error aught by zone');
});
~~~
因此,如果我們想要集中捕獲 Flutter 應用中的未處理異常,可以把 main 函數中的 runApp 語句也放置在 Zone 中。這樣在檢測到代碼中運行異常時,我們就能根據獲取到的異常上下文信息,進行統一處理了:
~~~
runZoned<Future<Null>>(() async {
runApp(MyApp());
}, onError: (error, stackTrace) async {
//Do sth for error
});
~~~
接下來,我們再看看 Framework 異常應該如何捕獲吧。
## Framework 異常的捕獲方式
Framework 異常,就是 Flutter 框架引發的異常,通常是由應用代碼觸發了 Flutter 框架底層的異常判斷引起的。比如,當布局不合規范時,Flutter 就會自動彈出一個觸目驚心的紅色錯誤界面,如下所示:
:-: 
圖 1 Flutter 布局錯誤提示
這其實是因為,Flutter 框架在調用 build 方法構建頁面時進行了 try-catch 的處理,并提供了一個 ErrorWidget,用于在出現異常時進行信息提示:
~~~
@override
void performRebuild() {
Widget built;
try {
// 創建頁面
built = build();
} catch (e, stack) {
// 使用 ErrorWidget 創建頁面
built = ErrorWidget.builder(_debugReportException(ErrorDescription("building $this"), e, stack));
...
}
...
}
~~~
這個頁面反饋的信息比較豐富,適合開發期定位問題。但如果讓用戶看到這樣一個頁面,就很糟糕了。因此,我們通常會重寫 ErrorWidget.builder 方法,將這樣的錯誤提示頁面替換成一個更加友好的頁面。
下面的代碼演示了自定義錯誤頁面的具體方法。在這個例子中,我們直接返回了一個居中的 Text 控件:
~~~
ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails){
return Scaffold(
body: Center(
child: Text("Custom Error Widget"),
)
);
};
~~~
運行效果如下所示:
:-: 
圖 2 自定義錯誤提示頁面
比起之前觸目驚心的紅色錯誤頁面,白色主題的自定義頁面看起來稍微友好些了。需要注意的是,ErrorWidget.builder 方法提供了一個參數 details 用于表示當前的錯誤上下文,為避免用戶直接看到錯誤信息,這里我們并沒有將它展示到界面上。但是,我們不能丟棄掉這樣的異常信息,需要提供統一的異常處理機制,用于后續分析異常原因。
為了集中處理框架異常,**Flutter 提供了 FlutterError 類,這個類的 onError 屬性會在接收到框架異常時執行相應的回調**。因此,要實現自定義捕獲邏輯,我們只要為它提供一個自定義的錯誤處理回調即可。
在下面的代碼中,我們使用 Zone 提供的 handleUncaughtError 語句,將 Flutter 框架的異常統一轉發到當前的 Zone 中,這樣我們就可以統一使用 Zone 去處理應用內的所有異常了:
~~~
FlutterError.onError = (FlutterErrorDetails details) async {
// 轉發至 Zone 中
Zone.current.handleUncaughtError(details.exception, details.stack);
};
runZoned<Future<Null>>(() async {
runApp(MyApp());
}, onError: (error, stackTrace) async {
//Do sth for error
});
~~~
## 異常上報
到目前為止,我們已經捕獲到了應用中所有的未處理異常。但如果只是把這些異常在控制臺中打印出來還是沒辦法解決問題,我們還需要把它們上報到開發者能看到的地方,用于后續分析定位并解決問題。
關于開發者數據上報,目前市面上有很多優秀的第三方 SDK 服務廠商,比如友盟、Bugly,以及開源的 Sentry 等,而它們提供的功能和接入流程都是類似的。考慮到 Bugly 的社區活躍度比較高,因此我就以它為例,與你演示在抓取到異常后,如何實現自定義數據上報。
### Dart 接口實現
目前 Bugly 僅提供了原生 Android/iOS 的 SDK,因此我們需要采用與第 31 篇文章“[如何實現原生推送能力](https://time.geekbang.org/column/article/132818)?”中同樣的插件工程,為 Bugly 的數據上報提供 Dart 層接口。
與接入 Push 能力相比,接入數據上報要簡單得多,我們只需要完成一些前置應用信息關聯綁定和 SDK 初始化工作,就可以使用 Dart 層封裝好的數據上報接口去上報異常了。可以看到,對于一個應用而言,接入數據上報服務的過程,總體上可以分為兩個步驟:
1. 初始化 Bugly SDK;
2. 使用數據上報接口。
這兩步對應著在 Dart 層需要封裝的 2 個原生接口調用,即 setup 和 postException,它們都是在方法通道上調用原生代碼宿主提供的方法。考慮到數據上報是整個應用共享的能力,因此我們將數據上報類 FlutterCrashPlugin 的接口都封裝成了單例:
~~~
class FlutterCrashPlugin {
// 初始化方法通道
static const MethodChannel _channel =
const MethodChannel('flutter_crash_plugin');
static void setUp(appID) {
// 使用 app_id 進行 SDK 注冊
_channel.invokeMethod("setUp",{'app_id':appID});
}
static void postException(error, stack) {
// 將異常和堆棧上報至 Bugly
_channel.invokeMethod("postException",{'crash_message':error.toString(),'crash_detail':stack.toString()});
}
}
~~~
Dart 層是原生代碼宿主的代理,可以看到這一層的接口設計還是比較簡單的。接下來,我們分別去接管數據上報的 Android 和 iOS 平臺上完成相應的實現。
### iOS 接口實現
考慮到 iOS 平臺的數據上報配置工作相對較少,因此我們先用 Xcode 打開 example 下的 iOS 工程進行插件開發工作。需要注意的是,由于 iOS 子工程的運行依賴于 Flutter 工程編譯構建產物,所以在打開 iOS 工程進行開發前,你需要確保整個工程代碼至少 build 過一次,否則 IDE 會報錯。
> 備注:以下操作步驟參考[Bugly 異常上報 iOS SDK 接入指南](https://bugly.qq.com/docs/user-guide/instruction-manual-ios/?v=20190712210424)。
**首先**,我們需要在插件工程下的 flutter\_crash\_plugin.podspec 文件中引入 Bugly SDK,即 Bugly,這樣我們就可以在原生工程中使用 Bugly 提供的數據上報功能了:
~~~
Pod::Spec.new do |s|
...
s.dependency 'Bugly'
end
~~~
**然后**,在原生接口 FlutterCrashPlugin 類中,依次初始化插件實例、綁定方法通道,并在方法通道中先后為 setup 與 postException 提供 Bugly iOS SDK 的實現版本:
~~~
@implementation FlutterCrashPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
// 注冊方法通道
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"flutter_crash_plugin"
binaryMessenger:[registrar messenger]];
// 初始化插件實例,綁定方法通道
FlutterCrashPlugin* instance = [[FlutterCrashPlugin alloc] init];
// 注冊方法通道回調函數
[registrar addMethodCallDelegate:instance channel:channel];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if([@"setUp" isEqualToString:call.method]) {
//Bugly SDK 初始化方法
NSString *appID = call.arguments[@"app_id"];
[Bugly startWithAppId:appID];
} else if ([@"postException" isEqualToString:call.method]) {
// 獲取 Bugly 數據上報所需要的各個參數信息
NSString *message = call.arguments[@"crash_message"];
NSString *detail = call.arguments[@"crash_detail"];
NSArray *stack = [detail componentsSeparatedByString:@"\n"];
// 調用 Bugly 數據上報接口
[Bugly reportExceptionWithCategory:4 name:message reason:stack[0] callStack:stack extraInfo:@{} terminateApp:NO];
result(@0);
}
else {
// 方法未實現
result(FlutterMethodNotImplemented);
}
}
@end
~~~
至此,在完成了 Bugly iOS SDK 的接口封裝之后,FlutterCrashPlugin 插件的 iOS 部分也就搞定了。接下來,我們去看看 Android 部分如何實現吧。
### Android 接口實現
與 iOS 類似,我們需要使用 Android Studio 打開 example 下的 android 工程進行插件開發工作。同樣,在打開 android 工程前,你需要確保整個工程代碼至少 build 過一次,否則 IDE 會報錯。
> 備注:以下操作步驟參考[Bugly 異常上報 Android SDK 接入指南](https://bugly.qq.com/docs/user-guide/instruction-manual-android/)
**首先**,我們需要在插件工程下的 build.gradle 文件引入 Bugly SDK,即 crashreport 與 nativecrashreport,其中前者提供了 Java 和自定義異常的的數據上報能力,而后者則是 JNI 的異常上報封裝 :
~~~
dependencies {
implementation 'com.tencent.bugly:crashreport:latest.release'
implementation 'com.tencent.bugly:nativecrashreport:latest.release'
}
~~~
**然后**,在原生接口 FlutterCrashPlugin 類中,依次初始化插件實例、綁定方法通道,并在方法通道中先后為 setup 與 postException 提供 Bugly Android SDK 的實現版本:
~~~
public class FlutterCrashPlugin implements MethodCallHandler {
// 注冊器,通常為 MainActivity
public final Registrar registrar;
// 注冊插件
public static void registerWith(Registrar registrar) {
// 注冊方法通道
final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutter_crash_plugin");
// 初始化插件實例,綁定方法通道,并注冊方法通道回調函數
channel.setMethodCallHandler(new FlutterCrashPlugin(registrar));
}
private FlutterCrashPlugin(Registrar registrar) {
this.registrar = registrar;
}
@Override
public void onMethodCall(MethodCall call, Result result) {
if(call.method.equals("setUp")) {
//Bugly SDK 初始化方法
String appID = call.argument("app_id");
CrashReport.initCrashReport(registrar.activity().getApplicationContext(), appID, true);
result.success(0);
}
else if(call.method.equals("postException")) {
// 獲取 Bugly 數據上報所需要的各個參數信息
String message = call.argument("crash_message");
String detail = call.argument("crash_detail");
// 調用 Bugly 數據上報接口
CrashReport.postException(4,"Flutter Exception",message,detail,null);
result.success(0);
}
else {
result.notImplemented();
}
}
}
~~~
在完成了 Bugly Android 接口的封裝之后,由于 Android 系統的權限設置較細,考慮到 Bugly 還需要網絡、日志讀取等權限,因此我們還需要在插件工程的 AndroidManifest.xml 文件中,將這些權限信息顯示地聲明出來,完成對系統的注冊:
~~~
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hangchen.flutter_crash_plugin">
<!-- 電話狀態讀取權限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 網絡權限 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 訪問網絡狀態權限 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 訪問 wifi 狀態權限 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 日志讀取權限 -->
<uses-permission android:name="android.permission.READ_LOGS" />
</manifest>
~~~
至此,在完成了極光 Android SDK 的接口封裝和權限配置之后,FlutterCrashPlugin 插件的 Android 部分也搞定了。
FlutterCrashPlugin 插件為 Flutter 應用提供了數據上報的封裝,不過要想 Flutter 工程能夠真正地上報異常消息,我們還需要為 Flutter 工程關聯 Bugly 的應用配置。
### 應用工程配置
在單獨為 Android/iOS 應用進行數據上報配置之前,我們首先需要去[Bugly 的官方網站](https://bugly.qq.com),為應用注冊唯一標識符(即 AppKey)。這里需要注意的是,在 Bugly 中,Android 應用與 iOS 應用被視為不同的產品,所以我們需要分別注冊:
:-: 
圖 3 Android 應用 Demo 配置
:-: 
圖 4 iOS 應用 Demo 配置
在得到了 AppKey 之后,我們需要**依次進行 Android 與 iOS 的配置工作**。
iOS 的配置工作相對簡單,整個配置過程完全是應用與 Bugly SDK 的關聯工作,而這些關聯工作僅需要通過 Dart 層調用 setUp 接口,訪問原生代碼宿主所封裝的 Bugly API 就可以完成,因此無需額外操作。
而 Android 的配置工作則相對繁瑣些。由于涉及 NDK 和 Android P 網絡安全的適配,我們還需要分別在 build.gradle 和 AndroidManifest.xml 文件進行相應的配置工作。
**首先**,由于 Bugly SDK 需要支持 NDK,因此我們需要在 App 的 build.gradle 文件中為其增加 NDK 的架構支持:
~~~
defaultConfig {
ndk {
// 設置支持的 SO 庫架構
abiFilters 'armeabi' , 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
}
}
~~~
**然后**,由于 Android P 默認限制 http 明文傳輸數據,因此我們需要為 Bugly 聲明一項網絡安全配置 network\_security\_config.xml,允許其使用 http 傳輸數據,并在 AndroidManifest.xml 中新增同名網絡安全配置:
~~~
//res/xml/network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 網絡安全配置 -->
<network-security-config>
<!-- 允許明文傳輸數據 -->
<domain-config cleartextTrafficPermitted="true">
<!-- 將 Bugly 的域名加入白名單 -->
<domain includeSubdomains="true">android.bugly.qq.com</domain>
</domain-config>
</network-security-config>
//AndroidManifest/xml
<application
...
android:networkSecurityConfig="@xml/network_security_config"
...>
</application>
~~~
至此,Flutter 工程所需的原生配置工作和接口實現,就全部搞定了。
接下來,我們就可以在 Flutter 工程中的 main.dart 文件中,**使用 FlutterCrashPlugin 插件來實現異常數據上報能力了**。當然,我們**首先**還需要在 pubspec.yaml 文件中,將工程對它的依賴顯示地聲明出來:
~~~
dependencies:
flutter_push_plugin:
git:
url: https://github.com/cyndibaby905/39_flutter_crash_plugin
~~~
在下面的代碼中,我們在 main 函數里為應用的異常提供了統一的回調,并在回調函數內使用 postException 方法將異常上報至 Bugly。
而在 SDK 的初始化方法里,由于 Bugly 視 iOS 和 Android 為兩個獨立的應用,因此我們判斷了代碼的運行宿主,分別使用兩個不同的 App ID 對其進行了初始化工作。
此外,為了與你演示具體的異常攔截功能,我們還在兩個按鈕的點擊事件處理中分別拋出了同步和異步兩類異常:
~~~
// 上報數據至 Bugly
Future<Null> _reportError(dynamic error, dynamic stackTrace) async {
FlutterCrashPlugin.postException(error, stackTrace);
}
Future<Null> main() async {
// 注冊 Flutter 框架的異常回調
FlutterError.onError = (FlutterErrorDetails details) async {
// 轉發至 Zone 的錯誤回調
Zone.current.handleUncaughtError(details.exception, details.stack);
};
// 自定義錯誤提示頁面
ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails){
return Scaffold(
body: Center(
child: Text("Custom Error Widget"),
)
);
};
// 使用 runZone 方法將 runApp 的運行放置在 Zone 中,并提供統一的異常回調
runZoned<Future<Null>>(() async {
runApp(MyApp());
}, onError: (error, stackTrace) async {
await _reportError(error, stackTrace);
});
}
class MyApp extends StatefulWidget {
@override
State<StatefulWidget> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
// 由于 Bugly 視 iOS 和 Android 為兩個獨立的應用,因此需要使用不同的 App ID 進行初始化
if(Platform.isAndroid){
FlutterCrashPlugin.setUp('43eed8b173');
}else if(Platform.isIOS){
FlutterCrashPlugin.setUp('088aebe0d5');
}
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Crashy'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: Text('Dart exception'),
onPressed: () {
// 觸發同步異常
throw StateError('This is a Dart exception.');
},
),
RaisedButton(
child: Text('async Dart exception'),
onPressed: () {
// 觸發異步異常
Future.delayed(Duration(seconds: 1))
.then((e) => throw StateError('This is a Dart exception in Future.'));
},
)
],
),
),
);
}
}
~~~
運行這段代碼,分別點擊 Dart exception 按鈕和 async Dart exception 按鈕幾次,可以看到我們的應用以及控制臺并沒有提示任何異常信息。
:-: 
圖 5 異常攔截演示示例(iOS)
:-: 
圖 6 異常攔截演示示例(Android)
**然后**,我們打開[Bugly 開發者后臺](https://bugly.qq.com/v2/workbench/apps),選擇對應的 App,切換到錯誤分析選項查看對應的面板信息。可以看到,Bugly 已經成功接收到上報的異常上下文了。
:-: 
圖 7 Bugly iOS 錯誤分析上報數據查看
:-: 
圖 8 Bugly Android 錯誤分析上報數據查看
## 總結
好了,今天的分享就到這里,我們來小結下吧。
對于 Flutter 應用的異常捕獲,可以分為單個異常捕獲和多異常統一攔截兩種情況。
其中,單異常捕獲,使用 Dart 提供的同步異常 try-catch,以及異步異常 catchError 機制即可實現。而對多個異常的統一攔截,可以細分為如下兩種情況:一是 App 異常,我們可以將代碼執行塊放置到 Zone 中,通過 onError 回調進行統一處理;二是 Framework 異常,我們可以使用 FlutterError.onError 回調進行攔截。
在捕獲到異常之后,我們需要上報異常信息,用于后續分析定位問題。考慮到 Bugly 的社區活躍度比較高,所以我以 Bugly 為例,與你講述了以原生插件封裝的形式,如何進行異常信息上報。
需要注意的是,Flutter 提供的異常攔截只能攔截 Dart 層的異常,而無法攔截 Engine 層的異常。這是因為,Engine 層的實現大部分是 C++ 的代碼,一旦出現異常,整個程序就直接 Crash 掉了。不過通常來說,這類異常出現的概率極低,一般都是 Flutter 底層的 Bug,與我們在應用層的實現沒太大關系,所以我們也無需過度擔心。
如果我們想要追蹤 Engine 層的異常(比如,給 Flutter 提 Issue),則需要借助于原生系統提供的 Crash 監聽機制。這,就是一個很繁瑣的工作了。
幸運的是,我們使用的數據上報 SDK Bugly 就提供了這樣的能力,可以自動收集原生代碼的 Crash。而在 Bugly 收集到對應的 Crash 之后,我們需要做的事情就是,將 Flutter Engine 層對應的符號表下載下來,使用 Android 提供的 ndk-stack、iOS 提供的 symbolicatecrash 或 atos 命令,對相應 Crash 堆棧進行解析,從而得出 Engine 層崩潰的具體代碼。
關于這些步驟的詳細說明,你可以參考 Flutter[官方文檔](https://github.com/flutter/flutter/wiki/Crashes)。
我把今天分享涉及的知識點打包到了[GitHub](https://github.com/cyndibaby905/39_crashy_demo)中,你可以下載下來,反復運行幾次,加深理解與記憶。
## 思考題
最后,我給你留下兩道思考題吧。
第一個問題,請擴展 \_reportError 和自定義錯誤提示頁面的實現,在 Debug 環境下將異常數據打印至控制臺,并保留原有系統錯誤提示頁面實現。
~~~
// 上報數據至 Bugly
Future<Null> _reportError(dynamic error, dynamic stackTrace) async {
FlutterCrashPlugin.postException(error, stackTrace);
}
// 自定義錯誤提示頁面
ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails){
return Scaffold(
body: Center(
child: Text("Custom Error Widget"),
)
);
};
~~~
第二個問題,并發 Isolate 的異常可以通過今天分享中介紹的捕獲機制去攔截嗎?如果不行,應該怎么做呢?
~~~
// 并發 Isolate
doSth(msg) => throw ConcurrentModificationError('This is a Dart exception.');
// 主 Isolate
Isolate.spawn(doSth, "Hi");
~~~
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略