18
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Flutter】DraggableScrollableSheetを使ってみた

Last updated at Posted at 2023-04-13

はじめに

普段、使ったことないWidgetを使って記事を書こうと思いましたので、一度も使ったことがないDraggableScrollableSheetを使ってコードを書いてみました。

公式の記事と動画の説明がわかりやすかったのでURLを貼っておきます。

作ったもの

ショッピングアプリで、商品リストを見ながら、カートの画面を見れるという画面です。

sample.gif

コード

作ったコードを貼り付けておきます。
表示部分しか作ってないので、商品を追加したり、削除したりというのはできません。
DraggableScrollableSheetの使い方については次の章で説明します。

product_list_page.dart
// 表示している画面
class ProductListPage extends StatelessWidget {
  const ProductListPage();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Product List'),
      ),
      body: Container(
        padding: const EdgeInsets.all(16),
        child: Stack(
          children: [
            // 商品一覧表示
            const ProductListView(),
            // カートを表示
            DraggableScrollableSheet(
              initialChildSize: 0.1,
              minChildSize: 0.1,
              maxChildSize: 0.9,
              builder:
                  (BuildContext context, ScrollController scrollController) {
                // カートの商品一覧を表示
                return Container(
                  decoration: const BoxDecoration(
                    color: Color.fromARGB(255, 218, 243, 255),
                    borderRadius: BorderRadius.only(
                      topLeft: Radius.circular(16),
                      topRight: Radius.circular(16),
                    ),
                  ),
                  // カート商品表示のタイトルとアイテム一覧を表示
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      // カート商品表示のタイトル
                      const SizedBox(
                        height: 40,
                        child: Center(
                          child: Text(
                            "Cart",
                            style: TextStyle(
                              fontSize: 24,
                              fontWeight: FontWeight.w700,
                            ),
                          ),
                        ),
                      ),
                      // カートのアイテム一覧を表示
                      Expanded(
                        child: ListView.builder(
                          controller: scrollController,
                          itemCount: productsInCart.length,
                          itemBuilder: (context, index) {
                            final product = productsInCart[index];
                            return Padding(
                              padding: const EdgeInsets.all(8.0),
                              child: CartItem(
                                title: product.name,
                                price: product.price,
                              ),
                            );
                          },
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

// 商品一覧表示の個別のアイテム
class ProductItem extends StatelessWidget {
  final String title;
  final String description;
  final int price;
  const ProductItem({
    required this.title,
    required this.description,
    required this.price,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          flex: 3,
          child: Container(
            height: 100,
            color: Colors.grey.withOpacity(0.7),
            child: const Center(
                child: Text(
              "Image",
              style: TextStyle(
                color: Colors.white,
              ),
            )),
          ),
        ),
        const SizedBox(
          width: 8,
        ),
        Expanded(
          flex: 7,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              ProductTitleText(title),
              ProductDescriptionText(description),
              ProductPriceText(price),
            ],
          ),
        ),
      ],
    );
  }
}

// カート一覧に表示する個別のアイテム
class CartItem extends StatelessWidget {
  final String title;
  final int price;

  const CartItem({
    required this.title,
    required this.price,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          flex: 3,
          child: Container(
            height: 100,
            color: Colors.grey.withOpacity(0.7),
            child: const Center(
                child: Text(
              "Image",
              style: TextStyle(
                color: Colors.white,
              ),
            )),
          ),
        ),
        const SizedBox(
          width: 8,
        ),
        Expanded(
          flex: 7,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              ProductTitleText(title),
              ProductPriceText(price),
            ],
          ),
        ),
        IconButton(
          onPressed: () {},
          icon: const Icon(
            Icons.delete,
          ),
        ),
      ],
    );
  }
}

// 商品一覧表示
class ProductListView extends StatelessWidget {
  const ProductListView({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: products.length,
      itemBuilder: (context, index) {
        final product = products[index];
        return Padding(
          padding: const EdgeInsets.only(top: 8, bottom: 8),
          child: ProductItem(
            title: product.name,
            description: product.description,
            price: product.price,
          ),
        );
      },
    );
  }
}

// ProductItemのタイトルに使うテキスト
class ProductTitleText extends StatelessWidget {
  final String title;
  const ProductTitleText(this.title, {super.key});

  @override
  Widget build(BuildContext context) {
    return Text(
      title,
      textAlign: TextAlign.start,
      style: const TextStyle(
        fontSize: 20,
        fontWeight: FontWeight.w700,
      ),
    );
  }
}

// ProductItemの説明に使うテキスト
class ProductDescriptionText extends StatelessWidget {
  final String description;
  const ProductDescriptionText(this.description, {super.key});

  @override
  Widget build(BuildContext context) {
    return Text(
      description,
      textAlign: TextAlign.start,
      style: const TextStyle(
        fontSize: 16,
      ),
    );
  }
}

// ProductItemの価格に使うテキスト
class ProductPriceText extends StatelessWidget {
  final int price;
  const ProductPriceText(this.price, {super.key});

  @override
  Widget build(BuildContext context) {
    return Text(
      "¥$price",
      textAlign: TextAlign.start,
      style: const TextStyle(
        fontSize: 20,
        fontWeight: FontWeight.w700,
        color: Colors.red,
      ),
    );
  }
}

resources.dart
// 商品データのモデルクラス
class Product {
  String name;
  String description;
  int price;

  Product({required this.name, required this.description, required this.price});
}

// 商品一覧に表示する商品リスト
List<Product> products = [
  Product(
      name: "掃除機",
      description: "スムーズな吸引力と高効率なフィルターが特徴。小型で軽量なので、家庭のあらゆる場所で使用できます。",
      price: 8000),
  Product(
      name: "よく吸う掃除機",
      description:
          "パワフルなモーターと高性能のフィルターを搭載し、毛やダストを徹底的に吸い取ります。コンパクトなデザインで、収納にも便利です。",
      price: 15000),
  Product(
      name: "エアクリーナー",
      description:
          "空気中の微粒子を取り除くための高性能フィルターを搭載した空気清浄機。コンパクトで静音なので、寝室やオフィスでも使用できます。",
      price: 25000),
  Product(
      name: "コーヒーメーカー",
      description: "豆から挽いたコーヒー豆で本格的なコーヒーを簡単に作れる。自動シャットオフ機能付きで、安心して使用できます。",
      price: 12000),
  Product(
      name: "電気ケトル",
      description:
          "ワンタッチでお湯を沸かすことができる電気ケトル。高速で沸騰するので、忙しい朝にも便利です。コンパクトなデザインで、省スペースに収納できます。",
      price: 5000),
  Product(
      name: "10000mAhモバイルバッテリー",
      description:
          "スマートフォンやタブレットの充電に使用できる10000mAhのモバイルバッテリー。コンパクトなデザインで、持ち運びにも便利です。",
      price: 3000),
  Product(
      name: "照明器具",
      description: "優れた省エネ性能を持ち、快適な明るさを提供する照明器具。調光機能があり、ライトの明るさを自由に調整できます。",
      price: 6000),
  Product(
      name: "空気清浄機",
      description:
          "高性能のフィルターで空気中の花粉やほこりを取り除く空気清浄機。赤ちゃんやアレルギー持ちの方にもおすすめ、使い勝手の良いデザインで、リビングや寝室などのあらゆる場所で使用できます。",
      price: 20000),
  Product(
      name: "Bluetoothスピーカー",
      description:
          "高品質な音質とワイヤレス接続による快適な操作性が特徴のBluetoothスピーカー。コンパクトで持ち運びに便利なデザインです。",
      price: 7000),
  Product(
      name: "キッチンスケール",
      description:
          "正確な計量ができるキッチンスケール。液晶ディスプレイにより、計量結果を見やすく表示します。コンパクトなデザインで、キッチンの収納にも適しています。",
      price: 4000),
  Product(
      name: "ノイズキャンセリングヘッドフォン",
      description:
          "ノイズキャンセリング機能により、周囲の騒音を最大限に軽減するヘッドフォン。高音質なサウンドで、音楽や映画をより楽しめます。",
      price: 25000),
];

// カートに入っている商品リスト
List<Product> productsInCart = List.generate(10, (index) => products[index]);

DraggableScrollableSheetの使い方

前章のコードでは要点を掴みにくいと思いますので、DraggableScrollableSheetに焦点を当てたコードを掲載します。

DraggableScrollableSheet(
  // 初期の表示割合
  initialChildSize: 0.2,
  // 最小の表示割合
  minChildSize: 0.1,
  // 最大の表示割合
  maxChildSize: 0.9,
  // ドラッグを離した時に一番近いsnapSizeになるか
  snap: true,
  // snapで止める時の割合
  snapSizes: const [0.1, 0.5, 0.9],
  builder: (BuildContext context, ScrollController scrollController) {
    // 表示したいWidgetを返す
    return Container();
  },
),

コード内の下記の引数は省略可能です

  • initialChildSize(デフォルトは0.5)
  • minChildSize(デフォルトは0.25)
  • maxChildSize(デフォルトは1.0)
  • snap(デフォルトはfalse)
  • snapSized(nullable)

DraggableScrollableSheetを使用する際の注意点は、builder内でScrollControllerを使用しないと、シートはinitialChildSizeのままになるという点です。
したがって、シートのサイズをドラッグ操作したい場合、builderで返すWidgetにはListViewSingleChildScrollViewを含めるようにしましょう。

他の用途

今回は、商品一覧とカート内の商品を一画面で見るという用途で使用してみました。
他に使えそうな用途としては、以下が挙げられると思います

  • 一覧画面内で、画面遷移せずに詳細を見たい場合
    詳細の表示にDraggableScrollableSheetを使うことで遷移せずに詳細が確認できます。
  • 設定画面で設定を変更した時のフィードバックを即座に確認したい場合
    例えば、設定画面でメイン画面のアイコンの大きさを変えれると仮定して、設定画面とメイン画面が別画面だと、設定を変えた時に、メイン画面に戻るまでどのような画面になるかがわかりませんが、メイン画面にDraggableScrollableSheetを使って設定できるようにすると、反映が即座に確認できます。

おわりに

使う場面は少ないかもしれませんが、お役に立てれば幸いです。

18
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?