LoginSignup
26
14

宣言的UIの副作用に対する注目すべきアイディア

Last updated at Posted at 2023-12-04

この記事は 株式会社ビットキー Advent Calendar 2023 4日目の記事です。


個人的に、今年1年は色々な宣言的UIフレームワークを触った年でした。 React/SwiftUI/Jetpack Compose/Flutter をそれぞれ触った中で、似ている概念やアイディアをまとめてみたいと思います。移植を行うときの参考にしていただければ幸いです。

非同期処理

PCやスマホ上で計算や条件分岐・ループといったCPUとメモリが動くペースに対して、ファイルを保存したり通信を行ったりするペースは遥かに遅く、様々な周辺機器に司令を出して動いているうちに他の処理を進めることができれば、より効率的に目的を達成することができます。

代表的な例として、 Node.js の fs モジュール があります。結果をコールバックで受け取る fs.open (Callback API) と、同期的に返される fs.openSync (Synchronous API) が存在します。

周辺機器の処理の完了を戻り値の代わりにコールバックで受け取るようにすることで、全体を効率的に動かすようにできるようにはなったのですが、コールバックのネストが大量に発生するようになります。可読性の悪さから、2010 年代前半あたりでは「コールバック地獄」と呼ばれていました。1

その後、このコールバック地獄問題は Promiseasync/await によって解決されます。また、JavaScript以外の多くの言語も、これらの課題に対処するアイディアが存在します。

非同期イテレータ

連続的に発生するイベントに対してListenerを登録して購読するパターンは、購読の中止まで含めて一つの概念として構築されるようになりました。また、文法に組み込まれて for 文で記述したりすることもあります。

加えて、 RxJS をはじめとする ReactiveXSwift Combine も関連するアイディアとして紹介しておくべきでしょう。 Subject や Kotlin Flowのhotとcoldの考え方は、連続的に発生するイベントを扱う上でのヒントになるはずです。

Cooperative Cancellation

直訳すると「協調的なキャンセル」で、これは非同期処理を途中で止まれるようにするときに出てくるアイディアです。

一連の非同期処理を含む処理の起動で、 Job (Kotlin) や Task (Swift) オブジェクトを作成します。このオブジェクトに対してキャンセルを要求すると、例外をスローしたりゼロ値や途中結果を返して終了するようになります。逆にキャンセルをサポートする側は、そのように実装しなければなりません。

JavaScriptやDartでは、非同期処理のキャンセルが文法や言語ではなくFetch APIなどの個々のAPIで定められています。

コンポーネントと非同期処理をつなぐ

非同期処理をUIと組み合わせる実装で気をつけるべきポイントの一つに、登録したまま終わらなかったり中止せずに溜まってしまうリークの懸念があります。購読と中止はなるべく自分で直接扱わずに、フレームワークやライブラリに任せてしまいたいところです。

スコープを作成する

コンポーネントを表示している期間で非同期処理のスコープ (CoroutineScope, Task) を構成し、非表示になったらキャンセルするという機能をUIライブラリとして標準で持っているパターンがあります。

Reactの場合は、例えば AbortControlleruseEffect の組み合わせで実現できます。

useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal;
  fetch(url, { signal })
    .then((response) => {
      console.log("Complete", response);
    })
    .catch((err) => {
      console.error(`Error: ${err.message}`);
    });
  return () => controller.abort();
}, []);

同様の組み合わせはDartなら State.dispose とDioの CancelToken などで実現することになるでしょう。

スナップショット化する

主にデータ取得処理で見られるパターンとして、表示する瞬間の状態をスナップショットとして取得できるようにする方法もあります。

連続的なイベントの発生を表す非同期イテレータも、スナップショット化の方法が用意されていることがあります。

View Modelの見直し

宣言的UIとView Modelという組み合わせは、個人的には警戒すべきという意味でややネガティブに考えています。PageやScreenといった大きなViewの状態をView Modelで管理してしまうと、コンポーネントの再利用性を著しく損なわれてしまいます。Viewが再利用性の低いView Modelに依存すると、そのコンポーネントは別のPageやScreenで再利用することが難しくなります。とはいえ、AndroidのXMLレイアウトやiOSのUIKit時代の資産や考え方の流用を考えると、今すぐ全部撤廃するのもそれはそれで難しいという感覚でいます。

Dependency Injection

状態管理を変化させる部分にRepositoryなどの依存するオブジェクトをどのように差し込むのか?という問題は、宣言的UIになっても依然として存在します。

Riverpodは、宣言的UIのいくつかの要素をまとめてカバーできる点でユニークな解決策を持ったライブラリだと感じています。元々が改善されたProviderというコンセプトなので、Provider/Contextの代替として利用できます。また、 ref.read/ref.watch で他の Provider を注入できることから、Dependency Injectionとしての側面も持ち合わせています。 FutureProviderStreamProvider を各コンポーネントで watch して Snapshot を得るという動作は、 useSWRuseReactQuery と似ている部分だと思います。

Context/Provider

どのフレームワークにも引数のバケツリレーを避けて子孫全体に値を受け渡す方法があります。見た目を整えるThemeや言語を切り替えるLocalizationオブジェクトを配る際に登場します。

Actions/Intent

Flutterの Actions API は、イベントハンドラによる子孫から祖先方向のバケツリレーをContextのような受け渡し方法で解決してしまうものです。祖先から子孫に情報を配るのとは逆に、子孫側で発生させたIntentを祖先側でハンドルすることができます。

乱用は危険そうですが、使い方によっては途中のグルーコードを省略できるため、うまく機能する状況もあるのではないかと考えています。

最後に

宣言的UIフレームワークで開発する際に出てくる要素をいくつかまとめてみました。「Reactの〇〇ってSwiftUIだと何て言うの?」みたいなときに参考にしていただければと思います。

5日目の 株式会社ビットキー Advent Calendar 2023@uminoooon18 が担当します!

  1. https://techblog.yahoo.co.jp/programming/js_callback/

26
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
14