dely tech blog

クラシル・TRILLを運営するdely株式会社の開発ブログです

理想のページングを実装する 前編

f:id:meilcli:20220325101416j:plain

こんにちは、クラシルAndroidエンジニアの@MeilCliです。近々、クラシル内のレシピ保存機能においてクラシルショートとレシピカードも保存できるようにするという変更が入ります。それの開発に際して、ページングのあるAPIにおいて更新されうるコンテンツをどう表示していくかを開発チーム内で話し合い、理想と思うものを実装したのでそれの共有を行います

当記事は前後編の前編にあたり、どう表示していくかの考え方についてご説明します

更新されうるコンテンツの理想的なUX

まず、どういうことが問題になっているのか説明します

ユーザーが保存タブを開いたときの表示、つまり自分が保存しているレシピ一覧についてですが、保存タブからそのレシピの詳細画面を開き保存を解除したとします。この時点で保存タブを開いていたときから保存しているレシピ一覧が変わっていることになります。そして保存タブに戻ってきた時の表示はどのようなものが理想的なUXとなるのか、という問題です

f:id:meilcli:20220322125133p:plain
画像にした図

開発チームで話したときに出たアイデアとそれのメリット・デメリットは以下のようなものでした

アイデア メリット・デメリット
画面表示時にAPIを叩き、コンテンツを再表示する ページングAPIなのでコンテンツを再表示すると2ページ目以降だった場合にスクロール位置を維持できない
アプリ側は表示していたコンテンツを変えずに、ユーザーのPullToRefreshによる更新に頼る 実装は楽だが、PullToRefreshはユーザーの能動的な操作に頼ってしまっている
アプリ側は表示すべきコンテンツが変わったことを検知し、ユーザーに更新がありますよと教えてあげる 実装は重くなるが、更新を通知することでユーザーの意図したタイミングで変更を適用できる、しかし能動的な操作を行ってもらう必要がある
アプリ側は表示すべきコンテンツが変わったことを検知し、自動で変更を適用する 実装は重くなるが、自動で更新される、それがユーザーの意図した挙動なのかが焦点

いろんなアプリを触りながら理想的なUXを検討したところ、変更を自動で適用するというUXが一番良いのではないかという結論になりました。自動で変更されるアプリは多く、特にSNS系のアプリでは自動で変更が適用されることがユーザーの意図した挙動でありそうだったからです

ページング方式

我々が理想とするUXが決まったら次はそれを実現するための設計です。そこで最初に出てきた問題としてページングの問題でした

f:id:meilcli:20220318141350p:plain

このようなリスト要素と1ページあたり10件のページングを考えます。クラシルでよく使用されていた手法としては1ページあたりの件数とページ番号を指定してリクエストする方式でした。今までユーザー操作による要素の増減があまりなかったのでこの手法では問題にならなかったのですが、ページングを完了する前にユーザー操作が行われると問題になる場面があります

f:id:meilcli:20220318174410p:plain

画像のように、1ページ目をリクエストしたあとに要素の削除が行われると2ページ目を取得したときの結果に含まれない要素が出てきてしまいます。また、リストの先頭に要素を追加するなどした場合には2ページ目を取得したときの結果に1ページ目を取得したときの結果が含まれるようにもなってしまいます。そのため、1ページあたりの件数とページ番号を指定してリクエストする方式では問題のあることがわかりました

では、他に取れるページング方式には何があるでしょうか。多様な方式があると思いますが、ページの起点となる要素に着目すると以下の2パターンがよくあるものかなと思います

  • 要素のIDを指定し、それ以降・以前の要素を返す
  • 要素の作成・更新された時刻を指定し、それ以降・以前の要素を返す

どちらの方式も1ページ目を取得した結果の末尾の要素を基準点とし、次の2ページ目のリクエストに指定するという形になります

f:id:meilcli:20220318173955p:plain

そうすると画像のように要素の減少に対応したページングを行うことができます

そして、どちらの方式を採用するかですが、UI上の話をすると、保存機能は保存順や閲覧順といったソートができるUIを提供する予定です。そのため、実際のレスポンスはID順とはならないため、時刻ベースのページングである必要がありました。最終的な仕様としては時刻ベースのページングを行うCursorをレスポンスに追加し、クライアント側はCursorを用いて次ページのリクエストをするという形になりました

要素の追加・移動があった場合の対応

前述のページング手法では主に要素の削除に対応した方式について話しました。しかし、ユーザー操作は要素の削除だけではありません。要素の追加や移動といった操作がありえるのです

これについてはクライアント側でユーザー操作に応じた差分反映をリストに対して行うしかありません

しかし、設計上の難しいところはありません。なぜならクライアント側でユーザー操作が行われるのですから、ユーザーがどのような操作をしたかを記録し、それに応じて差分を反映すればよいからです

まとめ

クラシルでは更新されうるコンテンツについては変更を自動で適用するというUXが理想形ではないかということになりました
サーバー側は時刻ベースのCursorを用いたページングAPIの実装、クライアント側は要素の追加・移動などのユーザー操作を記録し、その差分反映をリストに対して行うという実装を行うことになりました

次回はAndroid側の実際の実装に関して掘り下げていこうと思います