Upgrade to Pro — share decks privately, control downloads, hide ads and more …

キャンセルします!処理を / Cancels the process!

キャンセルします!処理を / Cancels the process!

登壇者名:Masatoshi Tsushima
登壇したイベントタイトル:FlutterKaigi 2024
登壇したイベントのURL:https://2024.flutterkaigi.jp/
登壇したイベントの登壇者URL:https://2024.flutterkaigi.jp/session/a1ba9bfd-87c8-47b2-b5af-a50e0c64c300

More Decks by 株式会社ビットキー / Bitkey Inc.

Other Decks in Technology

Transcript

  1. Copyright © Bitkey Inc. All rights reserved. Sponsored Session キャンセルします!処理を

    「協調的キャンセル」 を Flutter に持ち込むには FlutterKaigi 2024 Masatoshi Tsushima
  2. Copyright © Bitkey Inc. All rights reserved. 津島 雅俊 Masatoshi

    Tsushima 2004 ソフトウェア開発との出会い Webサイトや携帯アプリを作って遊んでいました 2018秋 Bitkeyに参画 当初はbitkey platformの開発を担当 主にGoでサーバサイドを開発 2019秋 ファームウェアチームを立ち上げ bitlock LITEのファームウェアを内製化 現在に続く開発スタイルを構築 2020夏 第二世代ロックデバイスの開発 内製を前提としたファームウェア開発 2021春リリース 2022夏 Individual contributor (?) Android/iOS/Flutter、デバイスQA用アプリ、社 内仕様の標準化、Makefile職人、CIおじさん
  3. Outline Copyright © Bitkey Inc. All rights reserved. 1. 協調的キャンセルと

    Dart 2. Swift/Kotlin のキャンセル 3. MethodChannel 4. アプリに取り入れる
  4. Copyright © Bitkey Inc. All rights reserved. よく見るキャンセルボタン Xcode Android

    Studio Visual Studio Code Google Chrome あっ、間違えた! あれ〜、思ったより 時間かかるな…
  5. Copyright © Bitkey Inc. All rights reserved. キャンセルの言い換え • タイムアウト

    ◦ ボタンの代わりに時間で • 中断する • 停止する • あきらめる • 破棄する
  6. Copyright © Bitkey Inc. All rights reserved. キャンセルボタンへの期待 処理が止まる リソースの開放

    • CPU/メモリ • ディスクの読み書き • ネットワーク データの保証 • 不完全な結果の破棄 • キャンセルを記録
  7. Copyright © Bitkey Inc. All rights reserved. 処理がキャンセルされる流れ UI 処理

    キャンセルして! キャンセルを要求した場合
  8. Copyright © Bitkey Inc. All rights reserved. 処理がキャンセルされる流れ UI 処理

    後始末(リソースの開放、データの保護)を行って応答を返す キャンセルされました
  9. Copyright © Bitkey Inc. All rights reserved. 例1: Timer のキャンセル

    Callback を使う場合 void main() async { final t = Timer(Duration(seconds: 2), () { print("It's time!"); }); await Future.delayed(Duration(seconds: 1)); t.cancel(); }
  10. Copyright © Bitkey Inc. All rights reserved. 例1: Timer のキャンセル

    main Timer main と Timer が協調的に動作する final t = Timer() t.cancel(); もう Callback は呼ばない 1s
  11. Copyright © Bitkey Inc. All rights reserved. 例2: async/await とキャンセル

    async/await で書いたコイツを止めたい Future<void> printAfter2s() async { await Future.delayed(Duration(seconds: 2)); print("It's time!"); }
  12. Copyright © Bitkey Inc. All rights reserved. 例2: async/await とキャンセル

    でも協調性が無いヤツは止められない 💢 Future<void> printAfter2s() async { await Future.delayed(Duration(seconds: 2)); print("It's time!"); }
  13. Copyright © Bitkey Inc. All rights reserved. 例2: async/await とキャンセル

    協調性のある delayed 関数を作る Future<void> delayed(Completer cancel, Duration duration) async { final c = Completer(); final t = Timer(duration, c.complete); try { await Future.any([c.future, cancel.future]); } finally { t.cancel(); } } Timer と cancel の早い方
  14. Copyright © Bitkey Inc. All rights reserved. 例2: async/await とキャンセル

    void main() async { final cancel = Completer(); printAfter2s(cancel); await Future.delayed( Duration(seconds: 1)); cancel.completeError( Exception('キャンセル')); } void printAfter2s(cancel) async { await delayed( cancel, Duration(seconds: 2)); print("It's time!"); } 協調性のある delayed を使う
  15. Copyright © Bitkey Inc. All rights reserved. CancelToken 代表的な実装例 •

    dio (by flutter.cn) • cancellation_token (jonathancole.dev)
  16. Copyright © Bitkey Inc. All rights reserved. キャンセルされた非同期関数 どうするべき? 1.

    null やデフォルト値を返す 2. 例外を throw する 実用上は CancellationException を自作 いずれかを DartDoc に書く
  17. Copyright © Bitkey Inc. All rights reserved. “Cancellation is Cooperative”

    呼び出す側 • 開始を要求 • キャンセルを要求 呼び出される側 • 処理を開始 • 後始末をして切り上げる キャンセルを達成するために お互いが協調的に動作する
  18. Copyright © Bitkey Inc. All rights reserved. Swift の Task

    let t = Task { do { try await doSomething() } catch { guard !Task.isCancelled else { return } // TODO: Handle error } } t.cancel()
  19. Copyright © Bitkey Inc. All rights reserved. Kotlin の Job val

    job = launch { try { doSomething() } catch (e: Exception) { if (e !is CancellationException) throw e // TODO: Handle exception } } job.cancel()
  20. Copyright © Bitkey Inc. All rights reserved. Swift vs Kotlin

    vs Dart 非同期処理の比較 Swift Kotlin Dart 関数のキーワード async suspend async 呼び出しのキーワード await (不要) await Callback からの移行 withChecked[Throwi ng]Continuation suspend[Cancellabl e]Coroutine Future 繰り返し AsyncIterator Flow Stream キャンセルの単位 Task Job CancelToken* * 自作 or Third-party を利用
  21. Copyright © Bitkey Inc. All rights reserved. MethodChannel のおさらい •

    ネイティブの関数を呼び出す仕組み • バイナリデータ の送受信 ◦ 基本は StandardMessageCodec で変換 直接バイナリに(シリアライズ化)できないオブジェクトはどうする? Future, CancelToken, Job (Kotlin), Task (Swift)
  22. Copyright © Bitkey Inc. All rights reserved. 提案: 3 つに分けるアプローチ

    Start Await Cancel Task/Job を起動して ID を発行 Task/Job を待つ Task/Job を キャンセルする
  23. Copyright © Bitkey Inc. All rights reserved. Start: Job を起動して

    ID を発行 Start Await Cancel val jobs: Map<Int, Job> jobs[newId] = launch { // Do something } result.success(newId) int id = await startFoo();
  24. Copyright © Bitkey Inc. All rights reserved. Start: Job を起動して

    ID を発行 Start Await Cancel val jobs: Map<Int, Job> jobs[newId] = launch { // Do something } result.success(newId) int id = await startFoo();
  25. Copyright © Bitkey Inc. All rights reserved. Await: ID に対応した

    Job を待つ Start Await Cancel val jobs: Map<Int, Job> try { jobs[id].join() result.success() } catch (e: Exception) { result.error(e) } await awaitFoo(id);
  26. Copyright © Bitkey Inc. All rights reserved. Cancel: ID に対応した

    Job をキャンセルする Start Await Cancel val jobs: Map<Int, Job> jobs[id].cancel() result.success() cancelFoo(id);
  27. Copyright © Bitkey Inc. All rights reserved. 3 つの Platform

    Method を 1 つにして提供する Future<void> foo(CancelToken token) async { final id = await startFoo(); await token.run( awaitFoo(id), onCancel: () => cancelFoo(id), ); } • 開始 • キャンセルを登録 • 待つ
  28. Copyright © Bitkey Inc. All rights reserved. StatefulWidget との組み合わせ final

    token = CancelToken(); void initState() { super.initState(); future = runSomething(); } void dispose() { token.cancel(); super.dispose(); } FutureBuilder( future: future, builder: (context, snapshot) { return ... }, ),
  29. Copyright © Bitkey Inc. All rights reserved. Riverpod との組み合わせ final

    myProvider = FutureProvider.autoDispose((ref) async { final token = CancelToken(); ref.onDispose(() => token.cancel()); final response = await dio.get('path', cancelToken: token); // リクエストが成功したらステートを維持する ref.keepAlive(); return response; });
  30. Copyright © Bitkey Inc. All rights reserved. ところで:レビューで確認したいポイント final myProvider

    = FutureProvider.autoDispose((ref) async { final token = CancelToken(); ref.onDispose(() => token.cancel()); final response = await dio.get('path', cancelToken: token); // リクエストが成功したらステートを維持する ref.keepAlive(); return response; }); ここでキャンセルが発動したら何が起こる…?
  31. Copyright © Bitkey Inc. All rights reserved. 監視との統合 キャンセルによって発生した例外はどう扱うか? •

    Error Tracking ◦ 「操作がキャンセルされました」 → Handled ◦ 特別扱いはせずに Unhandled になったら送る • Metrics ◦ 件数 ◦ キャンセルまでの時間
  32. Copyright © Bitkey Inc. All rights reserved. トークンの受け渡しでインタフェースを変えたくない final token

    = CancelToken(); runZoned(() { runSomething(); }, zoneValues: { #cancelToken: token, }); Future<void> runSomething() { final token = Zone.current[#cancelToken] as CancelToken; final response = await dio.get( 'path', cancelToken: token, ); }
  33. Copyright © Bitkey Inc. All rights reserved. まとめ: 協調的キャンセル 呼び出し側からのキャンセルに応じて、呼び出された側が処理

    を安全に切り上げるパターン • Swift/Kotlin ↔ Flutter ◦ Start/Wait/Cancel に分けて Method Channel にする ◦ Dart では Cancel Token を作って管理しよう • 「やっぱりやめる」選択肢 ◦ 不要な処理を切り上げて、エコなモバイルアプリを