plotly Dash でapp.pyからコンポーネントとコールバックを切り出す

# やりたいこと

- dashでまあまあな規模のウェブページを作る
- app.pyに全部書くのは整備性がやばいので分割したい

例のごとく英語でもドキュメントが全然見つからないので併記します

splitting callback and components in multiple files
use the : [deepl](https://www.deepl.com/translator)


ちなみに公式のサンプル自体は[大量に存在する](https://dash-gallery.plotly.host/Portal/)が、どいつもこいつもapp.pyが数百行あってやばい

dash触ってまだ2ヶ月くらいなので、よりよい解決法があれば教えて下さい

# やったこと

## ファイルの構造を考える

- 最低限コンポーネントとコールバックは切り出したい
- どんどん増えるであろうコンポーネントとコールバックをそれぞれ管理する奴も必要そう


早い話 ↓これを

```
.
├── app.py
├── assets
│   ├── common.css
│   └── default.css
└── src
    └── some_utils.py
```

↓こんなかんじにすればよさそう

```
.
├── app.py
├── assets
│   ├── common.css
│   └── default.css
│
└── src
    ├── callback.py
    ├── callbacks
    │   ├── hoge.py
    │   ├── fuga.py
    │   └── poyo.py
    │
    ├── layout.py
    ├── components
    │   ├── fizz.py
    │   ├── buzz.py
    │   ├── boo.py
    │   └── bar.py
    │
    ├── some_utils.py
    └── utils
```


## app.pyの整理

せっかくなので自分の別記事のソースを拝借する

[Dashでdcc.CheckListのチェック状態を全選択・全解除するボタンの実装](https://qiita.com/ikentoh/items/919b1604546e526a5c1e)

```
python app.py
```
で起動し、http://localhost:8050 で閲覧できる


↓こんな画面になる
<img width="821" alt="スクリーンショット 2020-02-19 15.43.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529606/5936b1ae-5030-26ca-8896-3402d3076e89.png">

```app.py
import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_table
from dash.dependencies import Input, Output, State
from flask import Flask, request

import time

server = Flask(__name__)
app = dash.Dash(__name__, server=server)

app.title = 'checklist-test'

selected_key = None


checklists = dcc.Checklist(
    id='checklist-states',
    options=[
        {'label': 'New York City', 'value': 'NYC'},
        {'label': 'Montréal', 'value': 'MTL'},
        {'label': 'San Francisco', 'value': 'SF'}
    ],
    value=['MTL', 'SF']
)


app.layout = html.Div(id='main',children=[
    html.H1(children='チェックリストのテスト'),
    dcc.Location(id='location', refresh=False),
    html.Div(className='main-block', children=[checklists]),
    html.Div(className='second', children=[
        html.Button('全選択', id='filter-check-button', className='filter_button'),
        html.Button('全解除', id='filter-remove-button', className='filter_button')
        ])
    ])


@app.callback(
    [Output('checklist-states', 'value')],
    [Input('filter-check-button', 'n_clicks_timestamp'),
    Input('filter-remove-button', 'n_clicks_timestamp')],
    [State('checklist-states', 'value')]
)
def update_check(all_check, all_remove, checking):

        if not all_check is None:
            if (time.time() * 1000 - all_check) < 1000:
                return [['NYC', 'MTL', 'SF']]

        if not all_remove is None:
            if (time.time() * 1000 - all_remove) < 1000:
                return [[]]

        if all_check is None and all_remove is None:
            return [checking]



if __name__ == '__main__':
    app.run_server(host='0.0.0.0', debug=True)
```



dashオブジェクトの`app`にレイアウトとコールバックが紐付いているので、別の関数に`app`を直接渡すことで処理してくれそう

というわけで未来の自分に仕事を投げつつapp.pyを削れるだけ削る

```app.py
import dash
from flask import Flask

from src.layout import layout
from src.callback import callback


server = Flask(__name__)
app = dash.Dash(__name__, server=server)

app.title = 'checklist-test'

# componentsとcallback定義
app = layout(app)
callback(app)


if __name__ == '__main__':
    app.run_server(host='0.0.0.0', debug=True)
```
#### tips:
コールバックの登録は絶対にコンポーネント定義の後である必要がある
importされた瞬間に入力値がnullのコールバックが走るため、Input/Output/Stateで見ているコンポーネントが存在しないとエラーになる


## コンポーネントとレイアウト
言うまでもないが、ファイル名は適当に自分の納得したやつに変えてくださいね


### レイアウトの切り出し
引数として受け取った`app`の属性layoutに対して、html要素を代入する

```layout.py
import dash_core_components as dcc
import dash_html_components as html


def layout(app):

    checklists = dcc.Checklist(
        id='checklist-states',
        options=[
            {'label': 'New York City', 'value': 'NYC'},
            {'label': 'Montréal', 'value': 'MTL'},
            {'label': 'San Francisco', 'value': 'SF'}
        ],
        value=['MTL', 'SF']
    )

    # app.layoutに要素を代入
    app.layout = html.Div(id='main', children=[
        html.H1(children='チェックリストのテスト'),
        dcc.Location(id='location', refresh=False),
        html.Div(className='main-block', children=[checklists]),
        html.Div(className='second', children=[
            html.Button('全選択', id='filter-check-button', className='filter_button'),
            html.Button('全解除', id='filter-remove-button', className='filter_button')
        ])
    ])

    return app
```


### コールバックの切り出し

####tips:
直に`update_check`を呼ぼうとするとデコレータ部分をうまく解釈できないので、一段階深くしてデコレータごと別の関数で囲う必要がある
(要は`@app.callback()`の`app`が名前解決できる必要がある)

```callback.py
from dash.dependencies import Input, Output, State

import time


def callback(app):

    @app.callback(
        [
            Output('checklist-states', 'value')
        ],
        [
            Input('filter-check-button', 'n_clicks_timestamp'),
            Input('filter-remove-button', 'n_clicks_timestamp')
        ],
        [
            State('checklist-states', 'value')
        ]
    )
    def update_check(all_check, all_remove, checking):

        if all_check is not None:
            if (time.time() * 1000 - all_check) < 1000:
                return [['NYC', 'MTL', 'SF']]

        if all_remove is not None:
            if (time.time() * 1000 - all_remove) < 1000:
                return [[]]

        if all_check is None and all_remove is None:
            return [checking]
```


## コンポーネントとコールバックが増えたときの対処

### コンポーネントのさらなる分割
タイトル・チェックリスト・ボタンの3つを別々に管理する
layoutではブロックとスペーシングの管理のみ行う想定

tips:
`html.Div()`に属性`id`は必須ではない


```layout.py
from src.components import title, checklist, button

import dash_core_components as dcc
import dash_html_components as html


def layout(app):

    app.layout = html.Div(id='main', children=[

        html.Div(id='title-block', children=[title.layout()]),

        dcc.Location(id='location', refresh=False),

        html.Div(id='center-block', children=[
            html.Div(children=checklist.layout()),
            html.Div(children=button.layout())
        ])
    ])

    return app
```


```title.py
import dash_html_components as html


def layout():

    return html.H1(id='title', children='チェックリストのテスト')
```


```checklist.py
import dash_core_components as dcc


def layout():

    return dcc.Checklist(
        id='checklist-states',
        options=[
            {'label': 'New York City', 'value': 'NYC'},
            {'label': 'Montréal', 'value': 'MTL'},
            {'label': 'San Francisco', 'value': 'SF'}
        ],
        value=['MTL', 'SF']
    )
```


```button.py
import dash_html_components as html


def layout():

    return html.Button('全選択', id='filter-check-button', className='filter_button'), html.Button('全解除', id='filter-remove-button', className='filter_button')
```


### コールバックのさらなる分割

同じように機能ごとに切り出す

`callback.py`

```python
from src.callbacks import check_and_remove # hoge, fuga...

def callback(app):

    check_and_remove.register(app)
    # hoge.register(app)
    # fuga.register(app)
```


`check_and_remove.py`

```python
from dash.dependencies import Input, Output, State

import time


def register(app):

    @app.callback(
        [
            Output('checklist-states', 'value')
        ],
        [
            Input('filter-check-button', 'n_clicks_timestamp'),
            Input('filter-remove-button', 'n_clicks_timestamp')
        ],
        [
            State('checklist-states', 'value')
        ]
    )
    def update_check(all_check, all_remove, checking):

        if all_check is not None:
            if (time.time() * 1000 - all_check) < 1000:
                return [['NYC', 'MTL', 'SF']]

        if all_remove is not None:
            if (time.time() * 1000 - all_remove) < 1000:
                return [[]]

        if all_check is None and all_remove is None:
            return [checking]
```


## 最終的な構造

```
.
├── app.py
└── src
    ├── callback.py
    ├── callbacks
    │   └── check_and_remove.py
    ├── components
    │   ├── button.py
    │   ├── checklist.py
    │   └── title.py
    └── layout.py
```