虎視眈々と

Flutter × Firebaseを研究するアプリエンジニア

Flutterでネストが深くなってしまう問題をどのように対応するか

Flutterでネストが深くなってしまう問題をどのように対応するか

こんにちは、Flutter大好きマンの @yshogo87

Flutterが魅力にハマり過去にこんな記事を書いています。

主に初心者向けに書いていますので、よければどうぞ

FlutterのViewは適当にやると簡単にネスト地獄が起こってしまうことがあると思います。

自分も上の記事の中でこんなコードを書いてしまっています。(これはまだましです。)

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ListView(
            children: List.generate(10, (index) {
          return InkWell(
            onTap: (){},
            child: Column(
              children: <Widget>[
                ListTile(
                  title: Text("Index ユーザー $index"),
                  leading: Image.asset(
                    "assets/icon$index.png",
                    width: 30.0,
                    height: 30.0,
                  ),
                  subtitle: Text("こんにちはTwitterへ!!\nこちらはTwitterのクローンを作成しています"),
                ),
                SizedBox(height: 20.0,),
                Divider(height: 5.0,),
              ],
            ),
          );
        })),
      ),
    );
  }
}

今回はプロダクトコードでこのネスト地獄についてどのように解決しているのかを書いてみたいと思います。 この問題に関して自分もまだまだ研究中ですので、もし何かアドバイスあれば教えてください。(https://twitter.com/yshogo87)

今やっている対策

上のTwitterでも書いていますが、今こんなことをやっています

  • columを使った時はその中のWidgetは全て別メソッドにする
  • 別メソッドに分けたWidgetは上からViewの順番通りにおく
  • Viewとロジックは明確に分ける
  • できるだけif文やfor文は書かない

一つ一つ説明していきます。

colunmを使った時はその中のWidgetは全て別メソッドにする

Columnというか Widgetの配列 は必ず別メソッドにするということです。 これをやるだけでだいぶすっきりします。

例えばこんなコードがあったとします。 CustomScrollView の中の配列にWidgetをそのまま書いているせいでネストが大きくなっています。

class _SandBoxState extends State<SandBox> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton.extended(
          onPressed: () {}, icon: Icon(Icons.add), label: Text("追加")),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      body: CustomScrollView(
        slivers: <Widget>[
          SliverAppBar(
            pinned: true,
            expandedHeight: 200.0,
            flexibleSpace: FlexibleSpaceBar(
                background: Container(
              color: Colors.green,
            )),
          ),
          SliverList(
            delegate: SliverChildListDelegate(<Widget>[
              SizedBox(
                width: 200.0,
                height: 100.0,
                child: Shimmer.fromColors(
                  baseColor: Colors.red,
                  highlightColor: Colors.yellow,
                  child: Text(
                    'Shimmer',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontSize: 40.0,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
            ]),
          ),
        ],
      ),
    );
  }
}

配列の中のWidgetをひとつひとつしっかりメソッドに切り分けていくとこうなります。

class _SandBoxState extends State<SandBox> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton.extended(
          onPressed: () {}, icon: Icon(Icons.add), label: Text("追加")),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      body: CustomScrollView(
        slivers: <Widget>[
          _appbar(),
          _sliverList(),
        ],
      ),
    );
  }

  Widget _appbar() {
    return SliverAppBar(
      pinned: true,
      expandedHeight: 200.0,
      flexibleSpace: FlexibleSpaceBar(
          background: Container(
            color: Colors.green,
          )),
    );
  }

  Widget _sliverList() {
    return SliverList(
      delegate: SliverChildListDelegate(<Widget>[
        SizedBox(
          width: 200.0,
          height: 100.0,
          child: Shimmer.fromColors(
            baseColor: Colors.red,
            highlightColor: Colors.yellow,
            child: Text(
              'Shimmer',
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 40.0,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ),
      ]),
    );
  }
}

こうやって

slivers: <Widget>[
  _appbar(),
  _sliverList(),
],

こんな感じでWidgetの配列は別メソッドに分けておくと、その配列がどんなコンポーネントを示しているかメソッド名から推測できたりして、だいぶ可読性が上がると思います。 Row や、 StackColumn に関しても同じようなことをしています。

別メソッドに分けたWidgetは上からViewの順番通りにおく

例えばこんなViewがあった時に

View1

上からこんな感じでメソッドを分けています。

View2

線を引いたごとにメソッドを分けて受けから順番に並べていきます。

............
    Column(
      children: <Widget>[
        _groupInfo(),
        _groupJoinCount(),
        _joinButton()
      ],
    );
............

  Widget _groupInfo() {
    return ....;
  }

  Widget _groupJoinCount() {
    return ....;
  }

  Widget _joinButton() {
    return ....;
  }

後から見た時に少しばかりか追うのが楽になる感じがしています。

逆にメソッドをめちゃくちゃに分けたせいで自分が痛い目を見た経験があります。

Viewとロジックは明確に分ける

これはみんな大好きアーキテクチャの話です。 いろんなアーキテクチャがあると思いますが、FlutterではBLoCが結構有名な印象があります。

BLoC関してはこの辺りをどうぞ Build reactive mobile apps in Flutter — companion article

この辺りはどのプラットフォームでも言えることだと思います。

できるだけif文やfor文は書かない

こちらに関してもViewでは分岐を書くとテストコードが書きづらくなってしまったりするのでできるだけやめましょう BLoCの概念ではビジネスロジックではOS毎の分岐は書かないという条件があります

逆に言うと、Viewでの分岐はOS毎の分岐だけにしましょうと自分は解釈しています。

Widgetに分岐があるとその分だけネストが増えてしまうのでできるだけ避ける努力をするのがいいと思います。

昔に書いたクソコード

最後に

いかがでしたでしょうか。 他にもいい対処方法とか、ご指摘はTwitterの方によろしくお願いします。

@yshogo87