kivyMD開発其の壱 初期公開説明弐篇

ハロー、Qiita！いかがお過ごしでしょうか。

はい、というわけで今週もKivyMDのお時間となりました。お正月気分は残って
いないでしょうか。投稿者はまんまと残っている限りであります。いやー、もう
1正月を迎えたい(切実)。

さて、新年早々のニュースとしては、とある企業が週休3日を考えているという
なんとも羨ましいニュースがありましたが、なんと言ってもこのニュースが話題を
呼んでいるのではないでしょうか。

https://www.itmedia.co.jp/news/articles/2201/11/news160.html

多くの方が恩恵を受けている中、ハッと目が覚めるようなニュースでしたね。当然
私も受けていますので、改めて寄付をしようかなとも思った次第ではあります。
だって、KivyMDがなければこの投稿もなかったもの。

まぁ、一旦個人の信条などは置いておいて、投稿することによって貢献をしていき
たいと思います(解決には至ってない)。今週はというと、先週に引き続き初期公開
したアプリについて解説をしていきたいと思っております。それではレッツラゴ。

## 動き(繰り返し)

コードだけ見ても、動きを見ておかないとなんだか分からんとなると思うので、
前回を一部だけ振り返っておきます。

あ、この文は先週と同じものです。はい。。

![todoapp.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/2f220a32-f85b-5b8f-38a0-d5f65395aabb.gif)

どんなアプリなのよ、と思われる方は上のキャプチャを見てもらえればなんとなく
雰囲気は伝わるかと思われます。(これも同じ)

## 説明

ここからが先週と少し変わっていて、まずはアプリのリポジトリを見てもらえれば
と思います。

https://github.com/Quiet396/TodoApp

大きく分けて、アプリとしてはmain.(py|kv)の2種類で構成されています。先週
においては、主にKV側を説明していましたが今週はpythonコードについて説明を
していきます。

### main.py

さっそくではありますが、pythonコードの全文を載せておきます。ちなみにですが、
コードを公開した日から変更はしておりません。

```python:main.py
# Kivy
from kivy.lang import Builder
from kivy.properties import StringProperty

# KivyMD
from kivymd.app import MDApp
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.tab import MDTabsBase
from kivymd.uix.list import ThreeLineAvatarIconListItem
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
from kivymd.uix.snackbar import Snackbar

# Standard
import datetime


class Tab(MDBoxLayout, MDTabsBase):
    text = StringProperty()


class Task(ThreeLineAvatarIconListItem):
    text = StringProperty()
    secondary_text = StringProperty()
    tertiary_text = StringProperty()


class DoneTask(ThreeLineAvatarIconListItem):
    text = StringProperty()
    secondary_text = StringProperty()
    tertiary_text = StringProperty()
 

# not use
class Content(MDBoxLayout):
    taskname = StringProperty()
    description = StringProperty()
    
    def get_taskname(self):
        return taskname

    def get_description(self):
        return description


class Main(MDApp):
    dialog = None
    task_dialog = None

    def __init__(self, **kwargs):
        super(Main, self).__init__(**kwargs)

    def build(self):
        self.root.ids.taskname.bind(
            on_text_validate=self.set_error_message,
            on_focus=self.set_error_message,
        )
    
    def set_error_message(self, instance_textfield):
        self.root.ids.taskname.error = True

    def on_tab_switch(
        self, instance_tabs, instance_tab, instance_tab_label, tab_text
    ):
        pass
        
    def send(self):
        taskname = self.root.ids.taskname.text
        description = self.root.ids.description.text
        if(len(taskname) == 0):
            Snackbar(text="Please input taskname!").open()
            return
        now = datetime.datetime.now()
        nowtime = now.strftime("%Y/%m/%d %H:%M:%S")
        self.root.ids.todo.add_widget(
                                        Task(
                                                text=taskname, 
                                                secondary_text=description, 
                                                tertiary_text=nowtime
                                            )
                                        )
        Snackbar(text="Task Created!").open()
        self.root.ids.taskname.text = ""
        self.root.ids.description.text = ""
        
    def remove_widget(self, instance):
        if("todo" == instance.parent.text):
            self.root.ids.todo.remove_widget(instance)
        elif("doing" == instance.parent.text):
            self.root.ids.doing.remove_widget(instance)
        else:
            self.root.ids.done.remove_widget(instance)
            Snackbar(text="Task Removed!").open()
        
    def change_task_status(self, instance):
        if not self.task_dialog:
            self.task_dialog = MDDialog(
                title="Move to Status?",
                buttons=[
                    MDFlatButton(
                        text="NO",
                        theme_text_color="Custom",
                        text_color=self.theme_cls.primary_color,
                    ),
                    MDFlatButton(
                        text="YES",
                        theme_text_color="Custom",
                        text_color=self.theme_cls.primary_color,
                        on_release=lambda x: self.move_to_next( x,
                                                                instance,
                                                                instance.text, 
                                                                instance.secondary_text,
                                                                instance.tertiary_text
                                                              )
                    )
                ],
            )
        self.task_dialog.open()
    
    def move_to_next(self, button_instance, task_instance, text, secondary_text, tertiary_text):
        if("todo" == task_instance.parent.text):
            self.root.ids.doing.add_widget(
                                            Task(
                                                    text=text, 
                                                    secondary_text=secondary_text, 
                                                    tertiary_text=tertiary_text
                                                )
                                          )
        else:
            self.root.ids.done.add_widget(
                                             DoneTask(
                                                        text=text, 
                                                        secondary_text=secondary_text, 
                                                        tertiary_text=tertiary_text
                                                     )
                                         )
        self.remove_widget(task_instance)
        self.task_dialog.dismiss()
        self.task_dialog = None
        Snackbar(text="Task Moved!").open()

Main().run()
```

#### import

パッケージなりをインポートしている部分を説明していきます。該当部分を再掲
します。

```python
# Kivy
from kivy.lang import Builder
from kivy.properties import StringProperty

# KivyMD
from kivymd.app import MDApp
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.tab import MDTabsBase
from kivymd.uix.list import ThreeLineAvatarIconListItem
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
from kivymd.uix.snackbar import Snackbar

# Standard
import datetime
```

大きく分けて、KivyとKivymd、Standardという3つで構成されています。
Kivyなどは字の通りですが、Standardはこれも名の通り標準パッケージと
しています。ちなみにですが、datetimeはタスク生成時の時刻を埋め込む
ために使っています。

#### カスタムレイアウトたち

これも該当部分を再掲しておきます。

```python
class Tab(MDBoxLayout, MDTabsBase):
    text = StringProperty()


class Task(ThreeLineAvatarIconListItem):
    text = StringProperty()
    secondary_text = StringProperty()
    tertiary_text = StringProperty()


class DoneTask(ThreeLineAvatarIconListItem):
    text = StringProperty()
    secondary_text = StringProperty()
    tertiary_text = StringProperty()

(略)
```

これらについてはカスタムで作るウィジェットクラスとなります。Tabに
ついてはあやしい部分がありますが、分けて書くのは少々面倒なので一緒
くた(出身がわかりそう)にしています。

おのおの、クラスを継承をしていることがわかるかと思います。あとは、
レイアウトで使うプロパティをここで書いています。タスクが2つ持って
いるのは、役割が異なっているので分けるようにしました。すみません、
嘘です。実装してるときに詰んだ詰んだ!となって2つとなりました。
\# タスクの種類については今後増えていく見込みです

また、Contentクラスについては、気になさらずでお願いしたいところ
です。これは先週伝えていたことですが、本当はこうやりたかったという
点とまさに繋がっているところになります。完全ダミークラスとなっていて
取り除くのを忘れていました。

#### Mainクラス

さて、今日のメインディッシュとなるところです。
こちらもおのおのメソッド単位のコードの方を再掲しておきます。

##### 初期状態

はい、いきなり矛盾が出てきて申し訳ないですが、initメソッドやbuild
メソッドなどオブジェクトを作成するときにお馴染みの定義がを以下のように
抜粋しています。

```python
class Main(MDApp):
    dialog = None
    task_dialog = None

    def __init__(self, **kwargs):
        super(Main, self).__init__(**kwargs)

    def build(self):
        self.root.ids.taskname.bind(
            on_text_validate=self.set_error_message,
            on_focus=self.set_error_message,
        )

    def set_error_message(self, instance_textfield):
        self.root.ids.taskname.error = True

    def on_tab_switch(
        self, instance_tabs, instance_tab, instance_tab_label, tab_text
    ):
        pass
```

お馴染みと言っているにも関わらず、set_error_messageやon_tab_
switchメソッドなんかはお馴染みではないだろ!と思われる方はその通り
でらっしゃいます。

まず、buildメソッドとset_error_messageメソッドなんかはcreate
タブのtasknameプロパティと結びついています。これは単にバリデーション
をしているだけで、空値のままタスクの生成をしないようにしています。この
あたりの詳しい触れ込みについては以下もしくは該当マニュアルが詳しいです。

https://qiita.com/virty/items/338d3c97c7fdd5fb1dda

続いて、on_tab_switchメソッドですが、これもTab篇ではお馴染みなのですが、
タブをスイッチするときに欠かせないものです。最初はタブスイッチするときに特に
やることないので、省略していたのですが無くすると動かなくなるので定義をしてい
ます。こちらも以下もしくは該当マニュアルが詳しいです。

https://qiita.com/virty/items/a90196b2f991f49ecb70

後説明するところというと、このあと使うtask_dialog変数にNoneを入れ込んで
いるだけになります。dialogというのもあるけど??ということも当てはまっており
まして、これも単に消し忘れの部分になりますw

##### sendメソッド

```python
    def send(self):
        taskname = self.root.ids.taskname.text
        description = self.root.ids.description.text
        if(len(taskname) == 0):
            Snackbar(text="Please input taskname!").open()
            return
        now = datetime.datetime.now()
        nowtime = now.strftime("%Y/%m/%d %H:%M:%S")
        self.root.ids.todo.add_widget(
                                        Task(
                                                text=taskname, 
                                                secondary_text=description, 
                                                tertiary_text=nowtime
                                            )
                                        )
        Snackbar(text="Task Created!").open()
        self.root.ids.taskname.text = ""
        self.root.ids.description.text = ""
```

ようやく、当初言っていることに返ってきて、メソッドの説明になります。

これは大まかに何やっているかをいうと、createタブでタスクを入力したあとで
SENDボタンを押したときに発動する中身となります。やっていることも大したこと
なく、単に受け取った値をTaskクラスに使用してタスク生成して、作ったあとはSn-
ackbarインスタンスを生成して作ったよということを通知しています。

最後に空文字を入れているのは、createタブの中身を一旦クリアしているだけになり
ます。あと触れていないところでいうと、tasknameはバリデーションしているのですが、
なぜかSENDボタンを押すとこのメソッドは発動されてしまうのでチェック処理を入れて
いるのと、importのところで言っていたdatetimeオブジェクトをここで使用しています。

参照としては以下もしくは該当マニュアルが詳しいですね。

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

##### remove_widgetメソッド

続いてはremove_widgetメソッドになります。

```python
    def remove_widget(self, instance):
        if("todo" == instance.parent.text):
            self.root.ids.todo.remove_widget(instance)
        elif("doing" == instance.parent.text):
            self.root.ids.doing.remove_widget(instance)
        else:
            self.root.ids.done.remove_widget(instance)
            Snackbar(text="Task Removed!").open()
```

ここについては言うまでもないくらいに大したことはやっていませんが、それぞれ(todo
とかdoingとか)のステータスの際に分岐をしてタスクを消去していることをやっています。

タスクウィジェットが1つであれば、こんな処理は必要ないですがここに関しては今後の
課題ですかね。もしかするとやらないかもだけど。。あと、この部分に関してはsendメソ
ッドのところでも貼り付けてあったList篇などが参照となります。

##### change_task_statusメソッド

さらに続きますが、こちらはchange_task_statusメソッドになります。

```python
    def change_task_status(self, instance):
        if not self.task_dialog:
            self.task_dialog = MDDialog(
                title="Move to Status?",
                buttons=[
                    MDFlatButton(
                        text="NO",
                        theme_text_color="Custom",
                        text_color=self.theme_cls.primary_color,
                    ),
                    MDFlatButton(
                        text="YES",
                        theme_text_color="Custom",
                        text_color=self.theme_cls.primary_color,
                        on_release=lambda x: self.move_to_next( x,
                                                                instance,
                                                                instance.text, 
                                                                instance.secondary_text,
                                                                instance.tertiary_text
                                                              )
                    )
                ],
            )
        self.task_dialog.open()
```

これも長々と書いていますが、特に複雑なことはやっていません。

あとで参照を貼り付けはしますが、本当に参照の部分とほとんど変わんねーじゃんか
と言われそうなくらいそのままのコードとなっています。というかKivyMDの特徴でも
あるようなところでもありますね。実装に迷いがでなくなるというか。

改めてちゃんと説明すると、単にこちらはタスクの左にあるドキュメントアイコンを
押したときに発動する中身となります。アイコンを押すと、上記のようにダイアログを
生成させてタスクスタータス移動する？という案内を出します。今のところNOボタンを
押しても何もならないですが、これは今思うとダイアログをdismissしなければいけま
せんでした。。これはあとあとの課題にしよっと。

もちろんYESボタンを押すと、TODOはDOINGに、DOINGはDONEにという感じでステー
タスが変わっていきます。この中のon_releaseメソッドがミソになりますが、lambda
を使ってタスクの中身が保持されるよう、次に説明するmove_to_nextメソッドに受け
継がれます。instanceとかは移動元のタスクインスタンスとなりますね。

ここも課題は課題なのですが、本当はアプリを閉じた場合に情報を保持しなければいけない
ので、このやり方はあまり相応しくはありません。でもpythonはlocalStorageとかもない
しでどうやるんだろ。まぁ色々考えなければいけません。

参照としては以下もしくは該当マニュアルが詳しいです。

https://qiita.com/virty/items/27d4d0d798c01e024889

##### move_to_nextメソッド

最後はというと、先程でもあったmove_to_nextメソッドとなります。

```python
    def move_to_next(self, button_instance, task_instance, text, secondary_text, tertiary_text):
        if("todo" == task_instance.parent.text):
            self.root.ids.doing.add_widget(
                                            Task(
                                                    text=text, 
                                                    secondary_text=secondary_text, 
                                                    tertiary_text=tertiary_text
                                                )
                                          )
        else:
            self.root.ids.done.add_widget(
                                             DoneTask(
                                                        text=text, 
                                                        secondary_text=secondary_text, 
                                                        tertiary_text=tertiary_text
                                                     )
                                         )
        self.remove_widget(task_instance)
        self.task_dialog.dismiss()
        self.task_dialog = None
        Snackbar(text="Task Moved!").open()
```

こちらは先程のダイアログでもあった、YESボタンを押下したときに発動される
中身となります。

ここもそれほど複雑ではなく、現状ではtodoタスクかdoingタスクの場合にそれぞれ
doingリストもしくはdoneリストにタスクを追加することが前半部分です。今思うと、
何かしらラッパーを作れば良かったかなと思いました。これはリファクタリングとして
の課題になりますね。

そして、作ったあとはもともとあったタスクを消去して、ダイアログも閉じてNoneを入れ、
最後にSnackbarで消したよ!と通知を出すという感じですね。やっていることは単純です。
まぁ、これも本来ならタスクが作られたことを確認して通知をした方がいいのもあるのです
が、、課題ばっかりですね。あとみなさんもこれどうなの?とかここが分からんということが
あればコメント頂ければと思います。

## まとめ

はい、今日も長々見て頂きありがとうございました。

これまでのことが分かっていれば、超ミニマムアプリなんかは作れるということが分かられた
のではないでしょうか。本当にオリジナルの実装となったのは多くても1割程度であったような。。

まぁでも、逆にたくさんある課題も浮き彫りになったのも事実ではあります。これについては
おいおいこなしていく所存ではあります。

課題を潰していくというのはもちろんあるのですが、実はというとやりたいことはこれだけでは
ありません。というのも、やりたいことは他にもあってそのためにこのアプリを作ったということ
がありました。やりたいことは以下の通りですね。

* アプリビルド
* Linterや自動テスト実施
* CIなどで固定作業効率化
* firebase連携
* etc..

というわけで、やりたいことは他にもあったようなとど忘れしてしまっているものもありますが、
ざっとやりたいことは上記の通りです。なんと言っても、アプリをビルドしないと実際にモバイル
でどのようになるのかとか分かりませんし、みなさんも開発などで試すことが出来ません。なので、
これだけは早急にやりたいなぁと思っています。なんかこういうことやってくれ!というのがあり
ましたら、コメントどしどしお寄せください。

はい、ということで今日は以上でアプリについての説明も以上となります。来週からはBehivors
篇に戻っていくこととします。ということで、最後まで見て頂きありがとうございましたー！

それではごきげんよう。

## 参照

> KivyMD
> https://kivymd.readthedocs.io/en/latest/