Flutterベースのモバイル向けタイムラインの作成

## 初めて：[Quire](https://quire.io)タイムライン（モバイル向け）の構造を初公開

2018年に初めてFlutterベースのアプリを作ったときは、とても楽しく興奮しました。それから3年経ち、Quireアプリもかなり充実して、従来のモバイル向けプロジェクト管理アプリの域を超えるまでになりました。Quireモバイルアプリの現行バージョンは、階層表示、ボード表示だけでなく、タイムライン表示にも対応しています。

モバイルアプリ向けのタイムライン表示の作成を決めたときは、簡単にできるとは思いませんでした。当時は類似の既成コンポーネントもなかったためですが、驚いたのは、インターネットでタイムライン表示の構造についての情報も見当たらなかったことです。そこで、いちかばちか、自分たちで作ってみることにしました。

##### [Quire](https://quire.io)モバイルアプリ用のタイムラインでは、以下を計画していました。

1. 横方向への無限日付スクロール
2. レンダリングオンデマンド（ROD）。ビューポートにあるときのみ実行されるWidgetのState
3. 任意の位置に素早く配置
4. 操作がかんたんで使いやすいインターフェースと、スムーズなユーザーエクスペリエンス

数週間で初期開発が完了し、以下のような構造になりました。

![](https://storage.googleapis.com/zenn-user-upload/52c3e5779da7-20220105.png)

1. タイムラインペインのコアベース（週、週末のセクションなど）
2. タスクリスト（階層構造のタスクリスト）
3. タイムラインペインのビューポートベース双方向リスト
4. 1ペインのみのとき、2ペインにまたがるときの両方に対応した期間の横棒
5. 期間の横棒上の固定ラベル

上図のように、タスクごとにタイムラインペインが割り当てられ、すべてのタイムラインのスクロール位置は互いに同期されます。

## インデックスベースのスクロールビュー
Google Flutter Widgetに似たインデックスベースのスクロールビューを作るために、Centerに引数のあるカスタムスクロールビューを使用します。実装すると、任意の位置まで素早くスクロールできるようになります。スクロール中のどの時点でも、各位置とインデックスを表示できます。

イメージ的には、少しスクロールした時点で、新しいCenterの引数でタイムラインをリロードしてビューポートの外に移し、またスクロールするとビューポート内に配置される、という感じです。

## タイムラインペイン
タイムラインをスムーズに使えるように、インデックスベースのスクロール表示と似た発想で、横方向にスクロールできるカスタム「無限双方向スクロールビュー」を実現しました。実装すると、タイムラインをなめらかにスクロールできます。

無限双方向スクロールビューには、Flutterの強力なViewportの考え方を活用しました。そして、Backwardリストのインデックスを-1から始まる負の数に変更しました。Index 0に当たる日付が分かるようにフラグも設定して、任意の日付まで素早くスクロールできるようにしました。

```dart
Widget forwardList = SliverList(
  delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
    return cellBuilder(context, _getIndex(forward: true, index: index));
  })
);

Widget backwardList = SliverList(
  delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
    return cellBuilder(context, _getIndex(forward: false, index: index));
  }),
);

Scrollable(
  viewportBuilder: (BuildContext context, ViewportOffset offset) {
    return Viewport(
      offset: offset,
      center: forwardListKey,
      slivers: [
        backwardList,
        forwardList,
      ]
    );
  },
)
```


## 2ペインにまたがるときの問題と解決方法
ビューポートには無限のリスト2つがスクロールされるため、期間の横棒が2つのリストにまたがることもあります。そこで、どちらのリストにも完全に同じ期間の横棒を作成し、ぴったり重ねて、リストがビューポートの外に移動してもリスト内のアンカーが壊れないようにしました。

![](https://storage.googleapis.com/zenn-user-upload/a56ed0583195-20220105.gif)

## 固定ラベルで解決
モバイル機器の小さい画面ではプロジェクトのどこを見ているかが分かりにくく、使っていると混乱してきます。この問題は、できるだけ多くの情報を提供することで軽減できます。そこで便利なのが固定ラベルです。

最初はとにかくシンプルにするため、スクロールビューのスクロール通知に従って、位置を取得してから配置されたラベルに設定していました。固定ラベルを各タイムラインペインの開始位置に表示するには、期間の横棒の現在位置の計算がベースとなります。

しかし、新しく配置されたラベルは次のフレームまでしか更新されず、スクロールビューと同じ時間枠で同期されないため、ずれて見えてしまいました。

幸いFlutterコミュニティーが、レンダリングレイヤー固定ヘッダーというすばらしい解決方法を教えてくれました。つまりレイアウトのタイミングによる方法です。レンダリングレイヤーにすべてのWidgetをサイズとともに入れるだけでなく、そのピクセルすべてを計算する必要があります。最後にlocalToGlobal関数を、スクロール位置、および2ペインにまたがるときのペイン切り替えに基づいた演算操作と置き換えて、パフォーマンスを向上させました。

## 始まりはこれから
今は大変な時代ですが、だからこそテクノロジーの分野で貢献したいと考えています。タイムライン表示の作成でまず考えたのは、どうやってFlutterの強力なフレームワークを活用して、ビューコンポーネントを一から作り直すことなく、軽く安定したゴージャスなUIを実現するか、ということでした。

各日付単位はインデックスとして、FlutterのSliverに組み込まれています。ほとんどのものはWidgetレイヤーの高レベルの開発概念に留まり、固定ビューのときのみレンダリングレベルに移動します。

[Quireアプリ](https://quire.io)をインストールして、Flutterベースのモバイルアプリを使ってみませんか。Quireタイムラインについて気になることは、コメントを投稿するか、[@quire_io](https://twitter.com/quire_io)でツイートしてお知らせください！

※転載許可済み: https://zenn.dev/quireteam/articles/2b2c44c3e49fac