Badge(バッヂ)の実装

はじめに

Flutter 3.7になり、Badge(バッヂ)のWidgetが追加されました。 それまでは独自で作ったり、サードパーティパッケージで実装してました。

クラス定義

クラス定義は下記のようになっています。

Badgeに表示する内容はlabel引数にWidgetで指定するようです。

その他、 ・背景色や文字色などの色指定 ・サイズ指定(labelを指定しなかった場合に適用される) などができるようになっています。

Badge Badge({
  Key? key,
  Color? backgroundColor,
  Color? textColor,
  double? smallSize,
  double? largeSize,
  TextStyle? textStyle,
  EdgeInsetsGeometry? padding,
  AlignmentDirectional? alignment,
  Widget? label,
  bool isLabelVisible = true,
  Widget? child,
})

実装

通常のバッヂ(labelのみ指定)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'バッヂサンプル',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('バッヂサンプル'),
      ),
      body: const Center(
        child: Badge(
          label: Text('1'),
        ),
      ),
    );
  }
}

アイコン上にバッヂ表示(labelとchild指定)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'バッヂサンプル',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('バッヂサンプル'),
      ),
      body: const Center(
        child: Badge(
          label: Text('1'),
          child: Icon(Icons.notifications)
        ),
      ),
    );
  }
}

最後に

詳しくは公式ドキュメントを参照

api.flutter.dev

色を選択するピッカーを実装する

はじめに

ToDOなどをカテゴリに分類することがあります。 カテゴリは名前だけでなく、色を指定することで、一目でどのカテゴリであるかわかりやすくしたいです。 色を指定するカラーピッカー機能を作るために自作でウィジェットを作ることを考えましたが手間がかかりそう。 その機能を実現してくれるパッケージを探したところ、flutter_colorpickerというパッケージが見つかったので、試してみました。

pub.dev

MaterialPicker

BlockPicker

MultipleChoiceBlockPicker

その他、様々な表示が用意されています。 下記は公式ページに記載されていたものです。

実装

  1. パッケージの導入

Terminalから追加する場合

# 下記をターミナルで実行
flutter pub add flutter_colorpicker

pubspec.ymlに直接追記する場合

# 下記をymlに追加し、pub getする
dependencies:
  flutter_colorpicker: ^1.0.3
  1. import文の追加

ピッカーを表示したい画面のWidgetに下記を追記

import 'package:flutter_colorpicker/flutter_colorpicker.dart';
  1. ピッカー表示処理の実装 例として、TextButtonタップ時のコードを記載しています。 BlockPickerを使い、色をひとつ選択します。
TextButton(
    onPressed: () {
      showDialog(
          builder: (context) => AlertDialog(
            title: const Text('色を選択'),
            content: SingleChildScrollView(
              child: BlockPicker(
                pickerColor: Colors.red,
                onColorChanged: (color) {
                  // TODO: 変更時処理
                },
              ),
            ),
          ), context: context);
    },
    child: const Text('カラーピッカーを表示する'),
),

実行

「カラーピッカーを表示する」というボタンをタップすると下記のピッカーが表示されます。

最後に

色を選択し、Colorクラスを得ることができました。 次はこれを永続化したいと思います。 Colorクラスをそのまま保存することはできないので、何らかの変換が必要となりそうです。 実装したらその記事を書きたいと思います。

SnackBarでメッセージ表示する

はじめに

データの保存/修正/削除などの処理の成功・失敗をユーザーに表示する。 SnackBarを使うのはその方法のひとつ。

SnackBarとは下記画像のような画面下でメッセージを表示するものです。

Scaffoldの中でSnackBarを表示することができます。 実装方法は

  1. Scaffoldを使ったWidgetを実装する
  2. Scaffoldを使ったWidget配下でボタンなどの処理の中でSnackBarを作成&送信する

実装

下記は「タップ」ボタンをタップすると「エラーが発生しました」というSnackBarが表示されます

See the Pen SnackBar by fjsw (@fjsw) on CodePen.

選択メニューの実装

はじめに

入力フォーム画面で使われることが多い選択メニューの実装方法です。

DropdownButtonとDropdownButtonFormFieldを使う2つの方法があります。 違いは表示を見ればわかると思いますが、DropdownButtonFormFieldはTextFieldのような表示がされます。 入力フォーム画面でTextFiledが多く使われている場合、DropdownButtonFormFieldを使ったほうがデザイン上の違和感がないです。

実装

See the Pen Untitled by fjsw (@fjsw) on CodePen.

See the Pen Untitled by fjsw (@fjsw) on CodePen.

GridViewで変な余白ができてレイアウトが崩れる

やりたいこと

GridViewに4つのバナー画像を画面幅いっぱいに2行2列で表示したい。

下記画像のような想定です。 f:id:fjswkun:20210723122646p:plain

プレースホルダーのバナー画像は下記のサイトを使って取得しています。 Placeholder.com: Placeholder.com – The Free Image Placeholder Service Favoured By Designers

問題

軽く実装してみたところバナーの上下に想定外の余白ができてしまいました。 f:id:fjswkun:20210723122436p:plain Imageウィジェットの高さの計算がうまくいってないようです。

実装

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'サンプルApp',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'サンプル'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const imageUrls = [
    'https://via.placeholder.com/350x80/0000FF?Text=Banner01',
    'https://via.placeholder.com/350x80/FF0000?Text=Banner02',
    'https://via.placeholder.com/350x80/FFFF00?Text=Banner03',
    'https://via.placeholder.com/350x80/000000?Text=Banner04',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Container(
          child: Container(
              child: GridView.count(
            crossAxisCount: 2,
            children: [for (final imageUrl in imageUrls) makeBanner(imageUrl)],
          )),
        ));
  }

  Widget makeBanner(String imageUrl) {
    return Image.network(imageUrl);
  }
}

対応

GridViewのchildAspectRatioはデフォルトで「1.0」が設定されていました。

GridView.count({
    Key? key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController? controller,
    bool? primary,
    ScrollPhysics? physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry? padding,
    required int crossAxisCount,
    double mainAxisSpacing = 0.0,
    double crossAxisSpacing = 0.0,
    double childAspectRatio = 1.0,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double? cacheExtent,
    List<Widget> children = const <Widget>[],
    int? semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
    ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
    String? restorationId,
    Clip clipBehavior = Clip.hardEdge,
  })

バナーの幅と高さが同じになるようにウィジェットのサイズが計算されているようです。 対応方法としてはGridViewのchildAspectRatioにバナー画像のアスペクト比を設定したところ、想定していた通りの表示になりました。

f:id:fjswkun:20210723123638p:plain

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'サンプルApp',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'サンプル'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const imageUrls = [
    'https://via.placeholder.com/350x80/0000FF?Text=Banner01',
    'https://via.placeholder.com/350x80/FF0000?Text=Banner02',
    'https://via.placeholder.com/350x80/FFFF00?Text=Banner03',
    'https://via.placeholder.com/350x80/000000?Text=Banner04',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Container(
          child: Container(
              child: GridView.count(
            crossAxisCount: 2,
            childAspectRatio: 350/80,
            children: [for (final imageUrl in imageUrls) makeBanner(imageUrl)],
          )),
        ));
  }

  Widget makeBanner(String imageUrl) {
    return Image.network(
      imageUrl,
    );
  }
}