RecycleView攻略 １日目

# はじめに

RecycleView(以降はRVと略す)はやっかいなwidgetだと思います。説明が不十分で不具合も多い[^1]のに、他に替えが利かず使わざるを得ないからです。ただやってる事自体は素晴らしく、私のdesktopではたとえ一万件ぐらいのデータがあったとしても快適にscrollして閲覧できるようにしてくれます。そのような離れ業をやってのけるwidgetは他にありません[^2]。今回はそんなRVを調べてみました。

# 環境

- Linux Mint 19.3
- CPython 3.8.1
- Kivy 2.0.0rc4

~~RVへは最新安定版(1.11.1)以降に一つ面白い機能が加えられていて使ってみたいので開発版を選びました。~~ この記事を投稿して数日後にKivy2.0.0が正式にreleaseされました。2.0.0rc4から2.0.0にかけてはRVに変更は無かったと思うのでこの記事の内容は多分2.0.0でも通ると思います。

# RVを用いない場合

まず最初にRVを用いずに大量のデータを表示しようとするとどうなるのか確めておきます。大元のデータとしては次のような物を用意しました。

```python3
import itertools

def data_src():
    from random import randint, choice
    genders = ('男', '女', )
    return (
        (id, choice(genders), randint(0, 100), )
        for id in itertools.count()
    )

for record in itertools.islice(data_src(), 100):
    print(record)
```

```
(0, '女', 42)
(1, '女', 17)
(2, '男', 69)
(3, '男', 35)
.
.
```

`data_src()`は`(id, 性別, 年齢)`からなるtupleを無限に作り出すiteratorを返します。データに意味付けがされてなくて扱いにくいですが、そのような状況も実際にあると思ってあえてそうしました。そしてこのデータ1件分を表示するwidgetを次のように定義しました。

```python3
from kivy.properties import BoundedNumericProperty, OptionProperty

KV_CODE = '''
<Person>:
    text: 'id: {}\\nage: {}'.format(self.id, self.age)
    canvas.before:
        Color:
            rgba: (.5, 0, 0, 1) if self.gender == '女' else (0, 0, .5, 1)
        Rectangle:
            pos: self.pos
            size: self.size
'''

class Person(Label):
    id = BoundedNumericProperty(0, min=0)
    gender = OptionProperty('男', options=('男', '女', ))
    age = BoundedNumericProperty(0, min=0)
```

以下のように`id`と`年齢`は文字列で、`性別`は背景色で表すようになっています。
![screenshot0001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144673/6a68cf91-d20a-50cc-793e-6d448d4b23d8.png)
これを使って実際に大量のデータを表示してみます。

```python3
import itertools
from kivy.properties import BoundedNumericProperty, OptionProperty
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.label import Label


def data_src():
    # 省略


class Person(Label):
    # 省略


KV_CODE = '''
<Person>:
    # 省略

ScrollView:
    BoxLayout:
        id: layout
        spacing: 8
        padding: 8
        orientation: 'vertical'
        size_hint_y: None
        height: self.minimum_height
'''


class SampleApp(App):
    def build(self):
        return Builder.load_string(KV_CODE)
    def on_start(self):
        layout_add_widget = self.root.ids.layout.add_widget
        for record in itertools.islice(data_src(), 10000):  # 一万件!!
            layout_add_widget(Person(
                size_hint_y=None, height=80,
                id=record[0], gender=record[1], age=record[2],
            ))


if __name__ == '__main__':
    SampleApp().run()

```

![Screenshot at 2020-12-07 15-56-59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144673/3598f68d-50df-344f-3796-6691800f642f.png)
すると起動に20秒ぐらいかかるはscroll中に4fpsぐらいしか出ないはでとても耐えられるものではありませんでした。desktopでこの<ruby>様<rt>ざま</rt></ruby>なのでmobileではもっと悲惨でしょう。さらにですが、この例ではデータ1件を一つのwidgetで表せていますが実際には複数のwidgetで表す事もあるので、その場合はwidgetの数が倍増してさらに重くなります。

というわけで普通にデータの件数分のwidgetを作っていると重くて使い物にならない事がわかりました。次はRVの力を借りて同じ物を実装してみます。

# RVを用いる場合

```python
import itertools
from kivy.properties import BoundedNumericProperty, OptionProperty
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.label import Label


def data_src():
    # 省略


class Person(Label):
    # 省略


KV_CODE = '''
<Person>:
    # 省略

RecycleView:
    viewclass: 'Person'
    RecycleBoxLayout:
        spacing: 8
        padding: 8
        orientation: 'vertical'
        size_hint_y: None
        height: self.minimum_height
        default_size_hint: 1, None
        default_size: 0, 80
'''


class SampleApp(App):
    def build(self):
        return Builder.load_string(KV_CODE)
    def on_start(self):
        rv = self.root
        rv.data = (
            {'id': record[0], 'gender': record[1], 'age': record[2], }
            for record in itertools.islice(data_src(), 10000)
        )


if __name__ == '__main__':
    SampleApp().run()
```

(画像は同じなので省きます)。
今度は起動時間は何もデータを表示しない場合とほぼ同じで、scroll中の画面のカクつきも感じませんでした。やはりRVの力は偉大です。

# RVは何をしている？

何故RVを使うと大量のデータをカクつかせずに見れるのかというと、名前から想像できるかもしれませんが何かを再利用(recycle)しているからです。その何かとは`view widget`(上の例でいう`Person`)で、具体的にはとあるデータがscroll操作によって表示する必要が無くなった時、それに使っていた`view widget`を別のデータの表示に使いまわします。そうする事で`view widget`の作る数を一度に画面上に表れる数程度に抑えて負荷を軽くしています。

でもその再利用するという特徴ゆえにデータと`view widget`の同期に注意が必要で、これを誤ると *(1)* 正しいデータが表示されなかったり *(2)* 配置が崩れたり *(3)* 異様に重くなったり するようです。私としてはここら辺の対策をしっかりと公式docに書いてくれればなと思っています。

# 使い方

RVの基本的な使い方は以下のような感じだと思います。

## 1. 大元のデータを用意

上のcodeで

```python
        rv.data = (
            {'id': record[0], 'gender': record[1], 'age': record[2], }
            for record in itertools.islice(data_src(), 10000)
        )
```

としているように大元のデータは`辞書のiterable`として用意してあげます。

## 2. viewclass(データを表示する為のwidget)を用意

辞書のkeyと同じ名前のpropertyを持ったwidgetを定義してあげます。

```python
from kivy.properties import BoundedNumericProperty, OptionProperty

KV_CODE = '''
<Person>:
    text: 'id: {}\\nage: {}'.format(self.id, self.age)
    canvas.before:
        Color:
            rgba: (.5, 0, 0, 1) if self.gender == '女' else (0, 0, .5, 1)
        Rectangle:
            pos: self.pos
            size: self.size
'''

class Person(Label):
    id = BoundedNumericProperty(0, min=0)
    gender = OptionProperty('男', options=('男', '女', ))
    age = BoundedNumericProperty(0, min=0)
```

## 3. RVのlayoutとviewclassを必ずKv言語上で指定

widget階層の構築やwidgetのpropertyへの書き込みは普通python上でもKv言語上でも行えますが、このRVに関しては何故かpython上だとうまくいかない事があります。なので上のcodeのように

```yaml
RecycleView:
    viewclass: 'Person'  # viewclassを指定
    RecycleBoxLayout:  # layoutを指定(現状はRecycleBoxLayoutとRecycleGridLayoutの二択)
```

とKv言語上で必ず指定しています。

## 4. RVにデータを渡す

```python
        rv.data = 大元のデータ
```

上の例ではpython上で渡していますがKv言語上でも可能です。

```yaml:
RecycleView:
    data: 大元のデータ
```

因みにどのようなiterableを渡してもlistのような物に変換されるので、後で`rv.data.append()`, `rv.data.pop()`等として編集することが可能です。

```python
        print(type(rv.data)) #=> <class 'kivy.properties.ObservableList'>
```

# おわりに

`Person`の例のように *(1)* `view widget`の大きさが揃っていて *(2)* データの編集を行わない 場合はRVの使い方で難しい所は特に無いと思います。ただそうでなくなった途端に牙を向いてきてやっかいなので次回はその場合の使い方を見ていきます。

[kivy_discord]:https://chat.kivy.org/
[matham_rv_detail]:https://github.com/kivy/kivy/issues/7197#issuecomment-720781746
[^1]: そもそもまだ実験段階(highly experimental)のwidgetなので仕方ない。
[^2]: かつてはListViewという物がありましたが、私はこれを使ったことが一度も無くどんな物かよく分かっていません。