kivyMDチュートリアル其の弍什伍 Components - Refresh Layout篇

みなさん、おはこんばんは。
いかがお過ごしでしょうか。

梅雨が続いているなーと思ったら急に天気が良くなり、、とてもお天気さんは自由奔放で
気まぐれですね。まぁでも、天気が良くなることはいいことですね。

とくにとりとめもないですが、そろそろComponentsも終盤に近づいてきているので、
手頃なアプリを作ろうかなーと考えております。1つ、良い良くない、面白いかどうかは
置いておいて、アイデアはあったのですが少し事情があり停滞しています。なにか面白い
データなどあればよいのですが、こんなのもあるよーとかこれ使ってみては?というのが
あればコメントもらえればほっこりします。

で、展開が急ですがさっそく今日はRefresh Layout篇に移りたいと思います。
以前のGW企画で飛ばしたものになりますね。ではさっそく、レッツラゴ(Youtuber風)。

## Refresh Layout

今日はめずらしくMaterial Designeのリンク先がありません。
すいすい進んでいけそうですね。

概要を少し触れておくと、リストなどがいくつかあることが前提ですが、新項目が追加された
ときに画面上部あたりをんーーーって下に引っ張るときにぐるぐる×2グルコサミンというアイ
コンが出るときのものです(意味不明)。

おそらく何言っているかわかんね、という方が大半なのでさっそくコードと結果の方に移ります
か。先に結果を見たいという方は、コードを端折ってもらえればと思います。

### Example

では、さっそくコードの方に移りたいと思います。
特にマニュアルから変更点はありません。

```python:xxv/refreshlayout.py
from kivymd.app import MDApp
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.factory import Factory
from kivy.properties import StringProperty

from kivymd.uix.button import MDIconButton
from kivymd.icon_definitions import md_icons
from kivymd.uix.list import ILeftBodyTouch, OneLineIconListItem
from kivymd.theming import ThemeManager
from kivymd.utils import asynckivy

Builder.load_string('''
<ItemForList>
    text: root.text

    IconLeftSampleWidget:
        icon: root.icon


<Example@FloatLayout>

    BoxLayout:
        orientation: 'vertical'

        MDToolbar:
            title: app.title
            md_bg_color: app.theme_cls.primary_color
            background_palette: 'Primary'
            elevation: 10
            left_action_items: [['menu', lambda x: x]]

        MDScrollViewRefreshLayout:
            id: refresh_layout
            refresh_callback: app.refresh_callback
            root_layout: root

            MDGridLayout:
                id: box
                adaptive_height: True
                cols: 1
''')


class IconLeftSampleWidget(ILeftBodyTouch, MDIconButton):
    pass


class ItemForList(OneLineIconListItem):
    icon = StringProperty()


class Example(MDApp):
    title = 'Example Refresh Layout'
    screen = None
    x = 0
    y = 15

    def build(self):
        self.screen = Factory.Example()
        self.set_list()

        return self.screen

    def set_list(self):
        async def set_list():
            names_icons_list = list(md_icons.keys())[self.x:self.y]
            for name_icon in names_icons_list:
                await asynckivy.sleep(0)
                self.screen.ids.box.add_widget(
                    ItemForList(icon=name_icon, text=name_icon))
        asynckivy.start(set_list())

    def refresh_callback(self, *args):
        '''A method that updates the state of your application
        while the spinner remains on the screen.'''

        def refresh_callback(interval):
            self.screen.ids.box.clear_widgets()
            if self.x == 0:
                self.x, self.y = 15, 30
            else:
                self.x, self.y = 0, 15
            self.set_list()
            self.screen.ids.refresh_layout.refresh_done()
            self.tick = 0

        Clock.schedule_once(refresh_callback, 1)


Example().run()
```

すっっごく、量が多いですね。お肉だったら噛みごたえありそう。まぁ、そんな
意味不明なこと言っていますが、分けて触れ込みたいと思います。

#### import文

まずはimportで読み込むパッケージから見ていきます。

```python
from kivymd.app import MDApp
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.factory import Factory
from kivy.properties import StringProperty

from kivymd.uix.button import MDIconButton
from kivymd.icon_definitions import md_icons
from kivymd.uix.list import ILeftBodyTouch, OneLineIconListItem
from kivymd.theming import ThemeManager
from kivymd.utils import asynckivy
```

いや、import文自体も長いっ。過去最多ではないでしょうか。

常連である、MDAppさんやBuilderさんはおなじみであったりしますが、Factoryさん
もたまに見かけたりしますね。あとはちょこちょこ来てもらったりする方もいますが、
新規でいうとThemeManagerさんやasynckivyさん、Clockさんだとかが来てもらった
りもしています。

ThemeManagerさんは一緒に来て、何か頼む(コードからは頼まれる方かな)かなと思って
いましたが、他で使われてなかったので中に入っただけになります。注文もしません。
たまに店でもこういう人いますよね。下手すれば無銭飲食になりかねませんが。。

importのあたりはそれくらいでしょうか。

#### ItemForList&IconLeftSampleWidgetのkv・クラス側

ここからは、少し迷いましたがkv側とクラス側を合わせて見てみます。

```python

=== kv側 ===

<ItemForList>
    text: root.text

    IconLeftSampleWidget:
        icon: root.icon

(略)

=== class側 ===

class IconLeftSampleWidget(ILeftBodyTouch, MDIconButton):
    pass


class ItemForList(OneLineIconListItem):
    icon = StringProperty()

```

ここは何をしているかというと、リストアイテムの定義をしているところになります。

少し触れ込むと、ItemForListはOneLineIconListItemを継承して、そのアイテム
(1行表示のリスト)が使用できるようになります。で、その中にあるIconLeftSample-
WidgetはILeftBodyTouchとMDIconButtonをmixinして継承しているので、左側に
アイコンを表示して、ボタンアクションを使用できるようにもなります。

具体的な仕様としては以下が参考になるかもしれません。まぁ、分かりにくっとなった
方はマニュアルのListを覗きに行ってもらえれば。

https://qiita.com/virty/items/3871b85bc67179d67b30

#### Exampleのkv側

```python
<Example@FloatLayout>

    BoxLayout:
        orientation: 'vertical'

        MDToolbar:
            title: app.title
            md_bg_color: app.theme_cls.primary_color
            background_palette: 'Primary'
            elevation: 10
            left_action_items: [['menu', lambda x: x]]

        MDScrollViewRefreshLayout:
            id: refresh_layout
            refresh_callback: app.refresh_callback
            root_layout: root

            MDGridLayout:
                id: box
                adaptive_height: True
                cols: 1
```

いや、またkvとクラスに分けるのかいっ!みたいな声が聞こえそうですが、その通りに
なります。一緒にするのにはコードの量が多すぎました。。

というどうでもよいことは置いておいて、ここは見覚えなんかあるぞ!という方も
いらっしゃるのではないでしょうか。そうです、これらもList篇でやりましたね。
ということで、詳細の方を知りたいという方はList篇を再読いただければ。。

少し差分もありますので触れ込んでおきますと、List篇で触れたのは以下のパターン
でした。

```
(BoxLayout)
　　- > ScrollView
　　　　- > MDList
　　　　　　- > 各リストの要素
```

ScrollViewからは必ず付きものであったのですが、今回ではScrollViewがMDScroll-
ViewRefreshLayoutに、MDListがMDGridLayoutに置き換わっていますね。再度コード
ブロックを書き換えてみると以下のように置き換わります。

```
(FloatLayout)
　　- > BoxLayout
　　　　- > MDScrollViewRefreshLayout
　　　　　　- > MDGridLayout
```

BoxLayoutがFLoatLayoutに完全に置き換わっているかというと、正直微妙なところに
なります。FloatLayoutって必要?という自問が自答出来ていません。。ですが、Float-
Layoutを別に、例えばBoxLayoutに変更して配下のBoxLayoutを削除するとうまく動か
なかったので、これは固定で考えた方がいいかもしれません。これは後の結果の方で。。

改めて俯瞰してみると、新たなパターンになりましたね。何パターンになるだろうか。。
MDScrollViewRefreshLayout、、いや長いですね、RefreshLayoutパターンと言える
でしょうか。

まだ、初出のところでふれてないところというと、MDScrollViewRefreshLayoutの
refresh_callbackプロパティになりますね。具体的なメソッドの方は、Exampleクラス
側で出てきます。あとは、root_layoutプロパティですかね。こちらはそのままルートウィ
ジェット(Example)を指定しています。

#### Exampleのクラス側

```python
class Example(MDApp):
    title = 'Example Refresh Layout'
    screen = None
    x = 0
    y = 15

    def build(self):
        self.screen = Factory.Example()
        self.set_list()

        return self.screen

    def set_list(self):
        async def set_list():
            names_icons_list = list(md_icons.keys())[self.x:self.y]
            for name_icon in names_icons_list:
                await asynckivy.sleep(0)
                self.screen.ids.box.add_widget(
                    ItemForList(icon=name_icon, text=name_icon))
        asynckivy.start(set_list())

    def refresh_callback(self, *args):
        '''A method that updates the state of your application
        while the spinner remains on the screen.'''

        def refresh_callback(interval):
            self.screen.ids.box.clear_widgets()
            if self.x == 0:
                self.x, self.y = 15, 30
            else:
                self.x, self.y = 0, 15
            self.set_list()
            self.screen.ids.refresh_layout.refresh_done()
            self.tick = 0

        Clock.schedule_once(refresh_callback, 1)
```

Exampleクラス側を再掲しておきます。

クラスの中ではグローバルっぽいローカル変数4つ(title,screen,x,y)と、build
メソッドは割愛しておきます。これまでも登場してきましたし、x・yに関してはこの
あと出てきて触れるからになります。

で、まず順番的(同クラスの上から)にはset_listメソッドが先ですが、kv側でrefre-
sh_callbackメソッドに入っていきます。

こちらは、まずClock.schedule_onceが初めに呼び出されますが、中の引数として
refresh_callbackの内部メソッド、もうひとつは1とあります。こちらは何をやって
いるかというと、1秒でイベントをスケジュール、すなわち1秒でrefresh_callbackを
呼び出すということになります。

すごく分かった感を出していますが、こちらはマニュアルからの引用になります。そんな
こと今までの投稿見たらわかるよ、っていう声も聞こえてきそうですが、一旦どうでもよい
ことは置いておいてより詳しいマニュアルは以下になります。英語わからん！って方はちょ
っとバージョンが古いですが、翻訳済みのマニュアルも参考になります。

\# たまにバージョン間の記載でギャップが生じるのでここでは
　kivy2.0のマニュアルだけを引用します
\# 要は自分で調べて!ってこと

https://kivy.org/doc/stable/api-kivy.clock.html

そしてどこまでいったか忘れてしまいそうですが、内部メソッドのところからですね。
こちらは、clear_widgetsが最初に呼び出されます。これは何をやっているかというと、
リフレッシュする際におまじないみたいなものですが、これをしないと上手く再描画されなく
なるのですね。これも後の結果の方で触れておきます。

あとは、何をやっているかというと、端的に言えばリストの切り替えになります。よく
わからんってばよという方はある意味普通な方かもしれません。これだけでピンとくる
方はちょっと変態?的な要素を含んでいるかもしれません。

リストの切り替えというとしっくりこないかもですが、後でもわかることでビルドされて
初期で表示されるリストは15個あります。x・yで指定している0とか15とかはまさにそれ
なのですが、これはアイコンのインデックス値という役割を担っています。リフレッシュ
されたときに15個分切り替える形になっているのですね。1-15、リフレッシュ後に16-30
という具合に。

んで、後はset_listメソッドが呼び出されさらにsetlistrefresh_layout側のrefresh_
doneメソッドが呼び出され、kivyのスレッド(tick)を0にしているという流れになります。

いやー長くなってきたぜ！はい、どうでもよい雄叫びでした。
残るは最後のset_listメソッドですね。

こちらは、割愛しようと思いましたが、見慣れないものもあるので触れておきます。
こちらも先程と同様に、asynckivyが持っているstartメソッドにて内部メソッドのset_
listを呼び出す形となります。このasynckivy以外の中身自体は、アイコンを取り出して
リストに当てはめているだけなのでとりとめもありませんが、重要なところは非同期(async)
のところになります。

非同期については以下が詳しいので、参考資料をリンクしておきます。

https://github.com/gottadiveintopython/asynckivy/blob/main/README_jp.md

なんとこの開発者は「水戸う 納豆齋」さんという方で、Clockモジュールで処理をしていた
ことを非同期処理として簡潔に書けるようにライブラリ開発を成し遂げ、ついにはkivymdの
非同期ライブラリとして取り込められるようになった超人的なすごすごプログラマーという
お方です。説明間違ってたら申し訳ありません。。すぐさま訂正するようにいたします。。。

ということで難しいことは上記資料にお任せして、ここではそのような非同期が使用されて
いるという程度に留めておきます。このあと、この値(asynckivy.sleep()の引数)を変更
してどのような挙動になるかを見てみます。

## API

使ってるものがそれほどないということと、マニュアルの量が少ないということからここは
省略することとします。ご了承くだされば。

## 結果

ここからはマニュアルでのコードの結果を載せることとさっきの触れておいた伏線を回収する
こととします。

![118.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/a8d88324-a2b6-ec41-02ba-6af0ef7aa095.png)

まずは初期画面です。すべて見えてないですが、ちゃんと15個配置はされています。

![119.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/4283e89d-40fa-261c-bdbd-5f7b0fe6dacb.png)

リフレッシュする様子が見えなく申し訳ないですが、リフレッシュ後はリストが
入れ替わりこのような配置になります。

ではここからは実験のようすです。まずは直近のところで、非同期から。asynckivy.sleep(0)
であったところをasynckivy.sleep(10)にしてみます。

![120.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/e1cda389-b4f0-a6db-c517-b21cc2768f7a.png)

画像からは見えにくいですね。。\# 動画に変更しようかな
これは何が起こっているかというと、10秒ごとにリストが1個1個積み重なっている
一部のようすとなります。なので、上記は30秒ほど経った様子になりますね。何か
スローテンポでコンテンツを表示させたいという用途だと重宝するかもしれません。

次はさらに上に遡り、clear_widgetsをやらなかった場合はどうなるかということを
見てみます。

![122.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/76a946dd-cf60-653a-d210-42c80ab89a0d.png)

初期表示は問題ありません。

![123.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/3cf265d1-44bd-5600-2cae-82f2d6e3a0c3.png)

しかし、リフレッシュ後に初期表示はそのまま画面上部に表示され(厳密にいうと少し
上にずれているが)、そのまま画面下に移動してみると、、

![124.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/b9a0e34d-9e6d-a7f1-2ec7-7d92a9b5c3e2.png)

あらま、そのまま下部に切り替えるアイテム項目が追加されているではありませんか。
これはいけませんねー(どこかの野球選手みたいに)。で、さらにリフレッシュをして
みると、、

![125.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/1f9cfa44-1b5c-d14b-688f-9573215a8dc8.png)

さらに、初期表示されていた15個の項目が下部に追加されました。。
完全なるアンチパターンですね。

最後の実験としては、kv側で触れていたBoxLayoutがFLoatLayoutに置き換えられる
のかどうかを見てみます。

![121.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/c8b0d935-f874-1a0a-3d9d-fe73de3db22d.png)

上記としては、リフレッシュ後の画面になりますが半分より下はなぜか見えなくなって
いますね。。これは原因がはっきりと見えなかったのですが、FloatLayoutを使用しなく
なったことで影響があるのは間違いありません。なるべくサンプルからは変更しない方が
よいのかもしれません。

## まとめ

はい、以上になります！いかがだったでしょうか。

少し予想より長くなりましたが、UIのところで取り入れると幅が広がることは間違いあり
ません。というかネットと連携して何かを表示するという形だと必須なような気もしますね。

少々難しいというか、私も実は完全に理解しているってことはないんですけど、このパターン
をそのまま取り入れたほうがいいのではと思えるコンポーネントでした。

ということで、今週はここまで！来週はScreen篇となりますが、サンプルもないし量もそれ
ほど多くないということで変わった取り組みをしていきたいと思っています。ご期待あれ！

それでは、ごきげんよう。

## 参照

> Components » Refresh Layout
> https://kivymd.readthedocs.io/en/latest/components/refresh-layout/