リストの要素が少なくてもプルリフレッシュできるようにする

作成日: 2025-08-24 /

はじめに

Flutter で ListView を使った画面を実装していると、こんな場面に出くわすことがある。

「要素が少ないとき、プルリフレッシュが効かない」

RefreshIndicator を使っているのに、リストの要素が少なくてスクロールが発生しない場合、引っ張っても何も起きない。ユーザーが更新しようとしても反応がなく、UX として不親切だ。本記事では、このよくある課題をシンプルなウィジェットで解決する方法を紹介する。

サンプルコード

import 'package:flutter/material.dart';

class SparedHeightRefreshIndicator extends StatelessWidget {
  const SparedHeightRefreshIndicator({
    required this.child,
    this.onRefresh,
    super.key,
  });

  final Widget child;
  final Future<void> Function()? onRefresh;

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: onRefresh ?? Future.value,
      // onRefresh が null の場合はリフレッシュを無効にする
      notificationPredicate: (_) => onRefresh != null,
      child: LayoutBuilder(
        builder: (context, constraints) {
          return ConstrainedBox(
            // 子ウィジェットの最小高さを画面いっぱいに広げる
            constraints: BoxConstraints(minHeight: constraints.maxHeight),
            child: child,
          );
        },
      ),
    );
  }
}

なぜリストの要素が少ないとプルリフレッシュできないのか

RefreshIndicator はスクロール可能なウィジェット(ListView など)の一番上でさらに引っ張ったときに発火する仕組みだ。

しかし、リストの要素が少ない場合、コンテンツの合計高さが画面の高さに満たないため、そもそもスクロールが発生しない。スクロールが発生しないということは、引っ張る動作を検知できず、リフレッシュが一切効かなくなってしまう。これはバグではなく Flutter の仕様だが、ユーザー視点では「更新できない」という体験になってしまうため、対処が必要だ。

解決策:最小高さを画面いっぱいに広げる

解決のアイデアはシンプルだ。コンテンツの高さが足りないなら、最小高さを画面いっぱいまで広げてしまえばいい。 そうすることで、コンテンツが少ない場合でもスクロール可能な領域が確保され、プルリフレッシュが正常に動作する。

LayoutBuilder で親の高さを取得する

LayoutBuilder を使うと、親ウィジェットが提供する制約(constraints)を取得できる。constraints.maxHeight は画面の高さ(または親の最大高さ)を表す。

ConstrainedBox で最小高さを指定する

ConstrainedBoxBoxConstraints(minHeight: constraints.maxHeight) を渡すことで、子ウィジェットの高さを制約できる。maxHeight を下回らないよう保証されるため、コンテンツが少なくてもスクロール領域が確保される。コンテンツが多い場合は自然にそれ以上の高さになるため、通常のスクロール動作も損なわれない。

onRefresh が null のときはリフレッシュを無効にする

notificationPredicate: (_) => onRefresh != null を指定することで、onRefresh が渡されていない場合はリフレッシュの発火自体を無効にしている。これにより、リフレッシュ不要な画面でこのウィジェットを使い回すことも可能だ。


使い方

SparedHeightRefreshIndicator(
  onRefresh: () async {
    await fetchData();
  },
  child: ListView(
    children: items.map((item) => ListTile(title: Text(item))).toList(),
  ),
)

既存の RefreshIndicatorListView の組み合わせを、このウィジェットに置き換えるだけで対応できる。

まとめ

  • RefreshIndicator はスクロールが発生しないとプルリフレッシュが効かない
  • LayoutBuilder + ConstrainedBox でコンテンツの最小高さを画面いっぱいに広げることで解決できる
  • コンテンツが多い場合は通常のスクロール動作のままで、影響はない
  • onRefresh の有無で制御できるため、汎用ウィジェットとして使い回しやすい

小さなウィジェットだが、地味に詰まりやすいポイントをスッキリ解決してくれる。同じ課題にぶつかった際はぜひ参考にしてほしい。