Slide 1

Slide 1 text

Copyright © Bitkey Inc. All rights reserved. Sponsored Session キャンセルします!処理を 「協調的キャンセル」 を Flutter に持ち込むには FlutterKaigi 2024 Masatoshi Tsushima

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

3つのhubシリーズ - 私たちの「⽣活」における全てに価値提供を 「暮らし」は、もっと便利にできる 働き⽅に、⾃由とパワーを ⼈⽣は、感動的だ ⼈⽣すべてのシーンで体験性を向上させることを⽬指し、⼈⽣における体験をHome(暮らし)、Workspace(働く)、Experience(⾮⽇常)と3つの領域に分けて設計。 「⾮⽇常体験」 ⾃分の「好き」に没頭しよう その体験は、もっと特別な時間に 「仕事∕働く」 あらゆる「働く空間」とそこで「働く⼈や業務」をつなぎ、 仕事をもっと快適に 「暮らし」 あなたの暮らしは、テクノロジーで変わる 住宅の⽇常は、より便利な体験へ

Slide 4

Slide 4 text

ソフトウェアの領域を超えて リアルとデジタルの「分断」を解消する 「分断」は、リアルでもデジタル上でも存在します。だからこそ、 私たちはソフトウェアの領域にはとまりません。ハードウェアも同 時に開発‧共創することで、デジタルで解決されていないリアルの 体験も含めて、シームレスな体験を実現します。

Slide 5

Slide 5 text

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おじさん

Slide 6

Slide 6 text

Outline Copyright © Bitkey Inc. All rights reserved. 1. 協調的キャンセルと Dart 2. Swift/Kotlin のキャンセル 3. MethodChannel 4. アプリに取り入れる

Slide 7

Slide 7 text

Copyright © Bitkey Inc. All rights reserved. 協調的キャンセルとは

Slide 8

Slide 8 text

Copyright © Bitkey Inc. All rights reserved. よく見るキャンセルボタン Xcode Android Studio Visual Studio Code Google Chrome

Slide 9

Slide 9 text

Copyright © Bitkey Inc. All rights reserved. よく見るキャンセルボタン Xcode Android Studio Visual Studio Code Google Chrome あっ、間違えた! あれ〜、思ったより 時間かかるな…

Slide 10

Slide 10 text

Copyright © Bitkey Inc. All rights reserved. キャンセルの言い換え ● タイムアウト ○ ボタンの代わりに時間で ● 中断する ● 停止する ● あきらめる ● 破棄する

Slide 11

Slide 11 text

Copyright © Bitkey Inc. All rights reserved. キャンセルボタンへの期待 処理が止まる リソースの開放 ● CPU/メモリ ● ディスクの読み書き ● ネットワーク データの保証 ● 不完全な結果の破棄 ● キャンセルを記録

Slide 12

Slide 12 text

Copyright © Bitkey Inc. All rights reserved. 処理がキャンセルされる流れ UI 処理 開始して!

Slide 13

Slide 13 text

Copyright © Bitkey Inc. All rights reserved. 処理がキャンセルされる流れ UI 処理 完了! そのまま成功する場合

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Copyright © Bitkey Inc. All rights reserved. 処理がキャンセルされる流れ UI 処理 可能な限り早くキャンセル時の処理に分岐

Slide 16

Slide 16 text

Copyright © Bitkey Inc. All rights reserved. 処理がキャンセルされる流れ UI 処理 後始末(リソースの開放、データの保護)を行って応答を返す キャンセルされました

Slide 17

Slide 17 text

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(); }

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Copyright © Bitkey Inc. All rights reserved. 例2: async/await とキャンセル 協調性のある delayed 関数を作る Future 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 の早い方

Slide 22

Slide 22 text

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 を使う

Slide 23

Slide 23 text

Copyright © Bitkey Inc. All rights reserved. CancelToken 代表的な実装例 ● dio (by flutter.cn) ● cancellation_token (jonathancole.dev)

Slide 24

Slide 24 text

Copyright © Bitkey Inc. All rights reserved. キャンセルされた非同期関数 どうするべき? 1. null やデフォルト値を返す 2. 例外を throw する 実用上は CancellationException を自作 いずれかを DartDoc に書く

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Copyright © Bitkey Inc. All rights reserved. Swift/Kotlin のキャンセル

Slide 27

Slide 27 text

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()

Slide 28

Slide 28 text

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()

Slide 29

Slide 29 text

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 を利用

Slide 30

Slide 30 text

Copyright © Bitkey Inc. All rights reserved. MethodChannel

Slide 31

Slide 31 text

Copyright © Bitkey Inc. All rights reserved. Flutter の MethodChannel にするには? 🤔

Slide 32

Slide 32 text

Copyright © Bitkey Inc. All rights reserved. MethodChannel のおさらい ● ネイティブの関数を呼び出す仕組み ● バイナリデータ の送受信 ○ 基本は StandardMessageCodec で変換 直接バイナリに(シリアライズ化)できないオブジェクトはどうする? Future, CancelToken, Job (Kotlin), Task (Swift)

Slide 33

Slide 33 text

Copyright © Bitkey Inc. All rights reserved. 提案: 3 つに分けるアプローチ Start Await Cancel Task/Job を起動して ID を発行 Task/Job を待つ Task/Job を キャンセルする

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Copyright © Bitkey Inc. All rights reserved. Cancel: ID に対応した Job をキャンセルする Start Await Cancel val jobs: Map jobs[id].cancel() result.success() cancelFoo(id);

Slide 38

Slide 38 text

Copyright © Bitkey Inc. All rights reserved. 3 つの Platform Method を 1 つにして提供する Future foo(CancelToken token) async { final id = await startFoo(); await token.run( awaitFoo(id), onCancel: () => cancelFoo(id), ); } ● 開始 ● キャンセルを登録 ● 待つ

Slide 39

Slide 39 text

Copyright © Bitkey Inc. All rights reserved. キャンセルを アプリに取り入れる

Slide 40

Slide 40 text

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 ... }, ),

Slide 41

Slide 41 text

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; });

Slide 42

Slide 42 text

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; }); ここでキャンセルが発動したら何が起こる…?

Slide 43

Slide 43 text

Copyright © Bitkey Inc. All rights reserved. 監視との統合 キャンセルによって発生した例外はどう扱うか? ● Error Tracking ○ 「操作がキャンセルされました」 → Handled ○ 特別扱いはせずに Unhandled になったら送る ● Metrics ○ 件数 ○ キャンセルまでの時間

Slide 44

Slide 44 text

Copyright © Bitkey Inc. All rights reserved. トークンの受け渡しでインタフェースを変えたくない final token = CancelToken(); runZoned(() { runSomething(); }, zoneValues: { #cancelToken: token, }); Future runSomething() { final token = Zone.current[#cancelToken] as CancelToken; final response = await dio.get( 'path', cancelToken: token, ); }

Slide 45

Slide 45 text

Copyright © Bitkey Inc. All rights reserved. まとめ

Slide 46

Slide 46 text

Copyright © Bitkey Inc. All rights reserved. まとめ: 協調的キャンセル 呼び出し側からのキャンセルに応じて、呼び出された側が処理 を安全に切り上げるパターン ● Swift/Kotlin ↔ Flutter ○ Start/Wait/Cancel に分けて Method Channel にする ○ Dart では Cancel Token を作って管理しよう ● 「やっぱりやめる」選択肢 ○ 不要な処理を切り上げて、エコなモバイルアプリを

Slide 47

Slide 47 text

Copyright © Bitkey INC. All rights reserved.