Tagged "flutter"

使用 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,也無法解決這個問題。

Read more...

解決在 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

Read more...

使用 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 裡。

Read more...

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

Read more...

在 Flutter 中使用 NestedScrollView 與 CustomScrollView

最近在修改 app 的個人頁,遇到的需求:

  1. Tab bar 切出圓角
  2. Tab bar 可以滑動,滑到 app bar 的時候固定住

用文字說不太清楚,下面直接看圖:

Imgur

頁面的 components 有:

  • App bar
  • 個人資訊區
  • Tab bar
  • Tab view

Tab view 裡是可滾動的內容, 這裡放了兩種類型:grid 與 list。 當用戶向上滑動時,app bar 固定不動,tab bar 往上滑動到 tab bar 時固定不動,下方 tab view 保持可滑動。如下圖:

Imgur

Read more...

編譯 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

Read more...

客製化 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。

Read more...

在 Flutter MaterialApp 中使用指定顏色

使用 Flutter MaterialApp 時,如果直接使用 Color.fromRGBO 指定 ThemeData 的 primarySwatch 會出現類型錯誤。這是因為 primarySwatch 接收的類型是 MaterialColor 而不是 Color

Imgur

Read more...