命中有時終須有 命中無時莫強求
使用 ReorderableList 解決 Bloc 在 ListView 中頻繁的關閉與創建問題
最近在開發 Flutter app 時遇到了一個問題:當 ListView
中,每個 child 都使用 BlocProvider
,例如:
class MyListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocSelector<RootCubit, RootState, List<SomeObj>>(
selector: (state) => state.items,
builder: (_, items) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (_, index) {
return BlocProvider(
create: (_) => MyCubit(),
child: ListTile(
key: ValueKey(items[index]),
title: Text(items[index].title),
),
);
},
);
},
);
}
}
如果列表的順序變更,child cubit 將會頻繁地被關閉與重新創建。此時會導致所有的 child 都不能再 emit state,否則會發生錯誤:
[ERROR: flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Bad state: Cannot emit new states after calling close
#0 BlocBase.emit (package:bloc/src/bloc_base.dart:97:9)
#1 MyCubit.fetchData.<anonymous closure> (package: myapp/features/hello/presentation/blocs/cubit.dart:203:13)
#2 GreetingRepository.fetchData(package:myapp/features/hello/infrastructure/repositories.dart:167:20)
‹asynchronous suspension>
#3 GreetingRepository.fetch.<anonymous closure (package:myapp/features/hello/infrastructure/repositories.dart:60:9)
<asynchronous suspension>
即便在每個 child 上增加了唯一的 key
,也無法解決這個問題。
解決在 Flutter 使用 Catcher 時遇到 WidgetsFlutterBinding 初始化的問題
最近引入 Catcher 來搜集 app 發生的錯誤訊息。按照 github 上的範例修改了 main function,初始化 Catcher:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
Catcher(
debugConfig: CatcherOptions(SilentReportMode(), [
ConsoleHandler(
enableApplicationParameters: false,
enableDeviceParameters: false,
enableCustomParameters: false,
enableStackTrace: true,
),
]),
releaseConfig: CatcherOptions(
SilentReportMode(),
[
HttpHandler(
HttpRequestType.post,
Uri.parse('https://x.y.z'),
enableApplicationParameters: true,
enableCustomParameters: false,
enableDeviceParameters: true,
enableStackTrace: false,
),
],
),
runAppFunction: initApp,
navigatorKey: myRootNavKey,
);
}
Future<void> initApp() async {
/*
此處省略一萬個 sdk 初始化步驟 .......
*/
runApp(const RootApp());
}
結果運行時 console 顯示錯誤:
flutter: [2023-12-29 20:47:21.680324 | Catcher | INFO] Setup localization lazily!
flutter: [2023-12-29 20:47:21.694254 | Catcher | INFO] ============================ CATCHER LOG ============================
flutter: [2023-12-29 20:47:21.694391 | Catcher | INFO] Crash occurred on 2023-12-29 20:47:21.687161
flutter: [2023-12-29 20:47:21.694430 | Catcher | INFO]
flutter: [2023-12-29 20:47:21.694508 | Catcher | INFO] ---------- ERROR ----------
flutter: [2023-12-29 20:47:21.694635 | Catcher | INFO] Zone mismatch.
The Flutter bindings were initialized in a different zone than is now being used. This will likely cause confusion and bugs as any zone-specific configuration will inconsistently use the configuration of the original binding initialization zone or this zone based on hard-to-predict factors such as which zone was active when a particular callback was set.
It is important to use the same zone when calling `ensureInitialized` on the binding as when calling `runApp` later.
To make this warning fatal, set BindingBase.debugZoneErrorsAreFatal to true before the bindings are initialized (i.e. as the first statement in `void main() { }`).
flutter: [2023-12-29 20:47:21.694679 | Catcher | INFO]
flutter: [2023-12-29 20:47:21.694899 | Catcher | INFO] ------- STACK TRACE -------
flutter: [2023-12-29 20:47:21.695005 | Catcher | INFO] #0 BindingBase.debugCheckZone.<anonymous closure> (package:flutter/src/foundation/binding.dart:503:29)
flutter: [2023-12-29 20:47:21.695041 | Catcher | INFO] #1 BindingBase.debugCheckZone (package:flutter/src/foundation/binding.dart:508:6)
flutter: [2023-12-29 20:47:21.695076 | Catcher | INFO] #2 runApp (package:flutter/src/widgets/binding.dart:1093:18)
flutter: [2023-12-29 20:47:21.695120 | Catcher | INFO] #3 initApp (package:arena/main.dart:225:3)
flutter: [2023-12-29 20:47:21.695152 | Catcher | INFO] <asynchronous suspension>
flutter: [2023-12-29 20:47:21.695183 | Catcher | INFO] #4 main.<anonymous closure>.<anonymous closure> (package:arena/main.dart:108:37)
flutter: [2023-12-29 20:47:21.695213 | Catcher | INFO] <asynchronous suspension>
flutter: [2023-12-29 20:47:21.695270 | Catcher | INFO]
flutter: [2023-12-29 20:47:21.695330 | Catcher | INFO] ======================================================================
flutter: [2023-12-29 20:47:21.695767 | Catcher | INFO] Report result: true
使用 Flutter Pigeon 開發 Objective-C plugin
最近在為 app 整合整合其他公司的 sdk,對方只提供 java / objective-c 的方案。因此得自己做 native plugin 開發。
Google 了一番 flutter native plugin 開發,基本都提到了 Pigeon 這個套件。使用 pigeon 可以:
- 同時規範 android/ios native plugin 的 api (長什麼樣子、怎麼傳參數)
- 自動產生 dart code
- 自動綁定 flutter 與 native 之間的通信
網路上的文章,大多都將 pigeon 用於單獨的 plugin 開發。而我則因為一些因素,將 peigon 用於現有的 project 裡。
Dart 轉換 json 中的 protobuf 結構
最近開始將 flutter app 的後端由 restful api 逐步轉成 grpc,轉換的過程中為了同時兼容舊的 api,在原本的 json 結構中替換並嵌入了 protobuf 結構。結果在 json 序列化時 (flutter packages pub run build_runner build
) 出現了類似以下錯誤:
[INFO] Generating build script completed, took 569ms
[INFO] Reading cached asset graph completed, took 77ms
[INFO] Checking for updates since last build completed, took 640ms
[SEVERE] json_serializable on lib/models/plustwo.dart:
Expecting a `fromJson` constructor with exactly one positional parameter. The only extra parameters allowed are functions of the form `T Function(Object?) fromJsonT` where `T` is a type parameter of the target type.
package:client/proto/calculator.pb.dart:40:28
╷
40 │ factory PlusTwoIntResult.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
│ ^^^^^^^^
╵
[INFO] Running build completed, took 5.1s
[INFO] Caching finalized dependency graph completed, took 48ms
[SEVERE] Failed after 5.2s
Compile OpenWRT With OpenClash
最近朋友希望弄一台翻牆路由器放在辦公室用,於是買了一台 Netgear R6300 v2。刷了 DD-WRT 後才發現問題:安裝不了 OpenClash,ipkg
連 update 都不行了,就更不用說安裝。
參考 DD-WRT 官網 development 的 Firmware Modification Kit,將 OpenClash 的 ipk 解壓縮後放進 firmware 一起重編。編譯完後的 fireware 貌似是含有 OpenClash,但是刷到 AP 後卻消失了,web 介面沒有相關的 UI、ssh 進去後也找不到。
重新拾回 OpenWRT,先刷了官網 for R6300v2 的 firmware,在 wifi 上遇到很大的問題:
- 5G 完全不能用
- 2.4G 可以用,但是目前實測很不穩 (ping 掉封包、高延遲)
實在不想花太多時間在翻牆上,而且電腦可以插網路線,不影響工作,於是就選擇 OpenWRT。
但是安裝 OpenClash 時又遇到的問題,luci-app-openclash_0.45.59-beta_all.ipk
的相依性 packages 包含了 dnsmasq-full
,而官網提供的 firmware 預設已經有了 dnsmasq
,這兩個是衝突的。如果 opkg install
加上 --nodeps
是可以解決,安裝完後的 OpenClash 也是可以使用的。不過對於系統潔癖/強迫症患者來說,這裡提供了重新編譯的解法。
在 Flutter 中使用 NestedScrollView 與 CustomScrollView
最近在修改 app 的個人頁,遇到的需求:
- Tab bar 切出圓角
- Tab bar 可以滑動,滑到 app bar 的時候固定住
用文字說不太清楚,下面直接看圖:
頁面的 components 有:
- App bar
- 個人資訊區
- Tab bar
- Tab view
Tab view 裡是可滾動的內容, 這裡放了兩種類型:grid 與 list。 當用戶向上滑動時,app bar 固定不動,tab bar 往上滑動到 tab bar 時固定不動,下方 tab view 保持可滑動。如下圖:
編譯 Flutter APK
本文主要紀錄使用 flutter 編譯 app apk 時遇到的問題與解法。
版本資訊:
# | Version |
---|---|
Flutter | 3.0.1 |
Host OS | MacOS Big Sur (11.3.1) |
Android Studio | 2020.3 (AI-203.7717.56.2031.7678000) |
Android Platform Tool | r33.0.2 |
OpenJDK | 18.0.1.1 |
Android (phone) | 11 |
客製化 Flutter Image Provider
之前在 flutter app 中使用 Optimized Cached Image 來展示圖片,選擇他的原因也很簡單,因為當時剛學 flutter 沒多久就要直接上陣了。先選一個看起來簡單好用的再說;而且他還可以設置 cache key,避免重複抓取,感覺起來就不錯。
然而後來圖片一多起來之後,就發現問題了,往往 app 打開滑沒幾下就 crash。看 log 是 OOM 問題,上網查了一下,有人說是 memory leak。大致上就是 image disposed 之後,沒有正確的釋放 ram,app 佔用的 ram 越來越多,就直接觸發 OOM killer。測試了一下原生的 NetworkImage 沒有這個問題。所以應該就是 Optimized Cached Image 的問題了。
另外就是在我的程式裡,widget 只拿到 media id,需要先到後端去查 media 的 presigned url 返回給 app,app 再用 presigned url 抓圖片資料。也就是原來的程式碼裡一直都是 future builder + image,這造成的問題就是頁面一刷新就要重新抓 presigned url,大大降低了效率,也大大提升了後端的負載。
因此決定客製化一個業務專用的 image provider。
OpenIM 整合(踩坑)筆記 01
OpenIM 號稱是由一群前微信等技術專家一起出來打造的開源 IM 系統。撇開政治不談,個人覺得微信在 IM 領域確實是佼佼者,因此決定整合 OpenIM 到現有的產品中,以減少浪費時間重複造輪子。
本篇目標是串連原有的帳號系統與 OpenIM 的帳號系統,完成 自動註冊用戶到 OpenIM (下圖中的第 3 ~ 4 步驟)。
在 Flutter MaterialApp 中使用指定顏色
使用 Flutter MaterialApp 時,如果直接使用 Color.fromRGBO
指定 ThemeData 的 primarySwatch
會出現類型錯誤。這是因為 primarySwatch
接收的類型是 MaterialColor
而不是 Color
: