Closed25
Flutter の Cupertino Widgets を使ってみる
はじめに
このスクラップでは Cupertino Widgets の中から Material Widgets とは異なりそうなものをピックアップして使ってみる
Material Widgets 版
過去に Material Widgets を使ってみたスクラップを作成した
前回
前回は下記3点の VSCode の Flutter デバッグ機能を使ってみた
- Inspector
 - Outline
 - Performance
 
Counter
Flutter のカウンターアプリを Material → Cupertino にする
準備コマンド
プロジェクトの作り方は Material の場合と同じ
flutter create hello_cupertino
cd hello_cupertino
追加のパッケージのインストールなども必要ない
コード
全体的に変更する必要がある
hello_cupertino/lib/main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(
        brightness: Brightness.light,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  
  State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text(widget.title),
        trailing: CupertinoButton(
          onPressed: _incrementCounter,
          child: const Icon(CupertinoIcons.add),
        ),
      ),
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text('$_counter'),
          ],
        ),
      ),
    );
  }
}
実行コマンド
flutter run
実行結果

CupertinoActionSheet
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const CupertinoApp(
    home: MyApp(),
    theme: CupertinoThemeData(brightness: Brightness.light),
  ));
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoButton(
          onPressed: () => _showActionSheet(context),
          child: const Text('Show acton sheet'),
        ),
      ),
    );
  }
  Future<void> _showActionSheet(BuildContext context) async {
    final result = await showCupertinoModalPopup<String>(
      context: context,
      builder: (context) {
        return CupertinoActionSheet(
          title: const Text('Title'),
          message: const Text('Message'),
          actions: <CupertinoActionSheetAction>[
            CupertinoActionSheetAction(
              onPressed: () {
                Navigator.of(context).pop("result");
              },
              child: const Text('Action'),
            ),
          ],
        );
      },
    );
    print(result);
  }
}
実行結果


CupertinoActivityIndicator
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const CupertinoApp(
    home: MyApp(),
    theme: CupertinoThemeData(brightness: Brightness.light),
  ));
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoActivityIndicator(),
      ),
    );
  }
}
実行結果

CupertinoAlertDialog
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const CupertinoApp(
    home: MyApp(),
    theme: CupertinoThemeData(brightness: Brightness.light),
  ));
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoButton(
          child: const Text("Show dialog"),
          onPressed: () async {
            final answer = await showCupertinoModalPopup<String>(
              context: context,
              builder: (context) {
                return CupertinoAlertDialog(
                  title: const Text('title'),
                  content: const Text('content'),
                  actions: <CupertinoDialogAction>[
                    CupertinoDialogAction(
                      onPressed: () {
                        Navigator.of(context).pop("no");
                      },
                      isDefaultAction: true,
                      child: const Text('No'),
                    ),
                    CupertinoDialogAction(
                      onPressed: () {
                        Navigator.of(context).pop("yes");
                      },
                      child: const Text('Yes'),
                    ),
                  ],
                );
              },
            );
            print(answer);
          },
        ),
      ),
    );
  }
}
実行結果

CupertinoButton
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const CupertinoApp(
    home: MyApp(),
    theme: CupertinoThemeData(brightness: Brightness.light),
  ));
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            CupertinoButton(onPressed: () {}, child: const Text('Enabled')),
            const SizedBox(height: 15),
            const CupertinoButton(onPressed: null, child: Text('Disabled')),
            const SizedBox(height: 15),
            CupertinoButton.filled(onPressed: () {}, child: const Text('Enabled')),
            const SizedBox(height: 15),
            const CupertinoButton.filled(onPressed: null, child: Text('Disabled')),
          ],
        ),
      ),
    );
  }
}
実行結果

CupertinoContextMenu
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(
        brightness: Brightness.light,
      ),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoContextMenu(
          actions: [
            CupertinoContextMenuAction(
              onPressed: () {},
              child: const Text('Action'),
            ),
          ],
          child: Container(
            color: CupertinoColors.activeBlue,
            width: 100,
            height: 100,
            alignment: Alignment.center,
            child: const Text('Text'),
          ),
        ),
      ),
    );
  }
}
実行結果

CupertinoDatePicker
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(
        brightness: Brightness.light,
      ),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoButton.filled(
          onPressed: () {
            showCupertinoModalPopup(
              context: context,
              builder: (context) {
                return Container(
                  height: 216,
                  color: CupertinoColors.systemBackground.resolveFrom(context),
                  child: SafeArea(
                    child: CupertinoDatePicker(
                      onDateTimeChanged: (value) {},
                    ),
                  ),
                );
              },
            );
          },
          child: const Text('Show date picker'),
        ),
      ),
    );
  }
}
実行結果

CupertinoNavigationBar
コード
MaterialApp でも CupertinoNavigationBar は使える
タイトルを中央に配置したい時に便利
main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AppBar title')),
      body: Column(
        children: [
          CupertinoNavigationBar(
            leading: OutlinedButton(
              onPressed: () {},
              child: const Text('Button'),
            ),
            middle: const Text('Title'),
          ),
        ],
      ),
    );
  }
}
実行結果

CupertinoPicker
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoButton(
          onPressed: () {
            showCupertinoModalPopup(
              context: context,
              builder: (context) {
                return Container(
                  height: 216,
                  color: CupertinoColors.systemBackground.resolveFrom(context),
                  child: SafeArea(
                    child: CupertinoPicker(
                      itemExtent: 32,
                      onSelectedItemChanged: (value) {},
                      children: List.generate(3, (index) => const Text('Item')),
                    ),
                  ),
                );
              },
            );
          },
          child: const Text('Show picker'),
        ),
      ),
    );
  }
}
実行結果

メモ
SafeArea って何だろうと思って調べたらわかりやすい記事があった
iPhone 画面の上端や下端にあるものを上手に避けてくれるらしい
CupertinoPopupSurface
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoButton(
          onPressed: () {
            showCupertinoModalPopup(
              context: context,
              builder: (context) {
                return CupertinoPopupSurface(
                  child: Container(
                    height: 200,
                    color: CupertinoColors.systemBackground.resolveFrom(context),
                    child: Center(
                      child: CupertinoButton(
                        onPressed: () {
                          Navigator.of(context).pop();
                        },
                        child: const Text('Close'),
                      ),
                    ),
                  ),
                );
              },
            );
          },
          child: const Text('Show picker'),
        ),
      ),
    );
  }
}
実行結果

メモ
実装手順の素晴らしい動画を見つけた
CupertinoScrollbar
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});
  
  State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  final ScrollController _scrollController = ScrollController();
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: CupertinoScrollbar(
        thumbVisibility: true,
        controller: _scrollController,
        child: ListView.builder(
          controller: _scrollController,
          itemCount: 100,
          itemBuilder: (context, index) {
            return const Padding(
              padding: EdgeInsets.all(10),
              child: Text('Title'),
            );
          },
        ),
      ),
    );
  }
}
実行結果

メモ
ScrollController を Scrollbar と ListView の両方に設定しないとエラーになる
CupertinoSearchTextField
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoSearchTextField(),
      ),
    );
  }
}
実行結果

CupertinoSegmentedControl
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoSegmentedControl<String>(
          onValueChanged: (value) {
            
          },
          children: {
            for (final segment in ["1", "2", "3"])
              segment: Padding(
                padding: const EdgeInsets.all(10),
                child: Text(segment),
              ),
          },
        ),
      ),
    );
  }
}
実行結果

メモ
実際に使う場合は StatefulWidget, setState,  groupValue を使う
CupertinoSlider
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});
  
  State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  double slider = 1;
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoSlider(
          min: 0,
          max: 10,
          value: slider,
          onChanged: (value) {
            setState(() {
              slider = value;
            });
          },
        ),
      ),
    );
  }
}
実行結果

CupertinoSlidingSegmentedControl
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});
  
  State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  String _segment = "1";
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoSlidingSegmentedControl<String>(
          groupValue: _segment,
          onValueChanged: (value) {
            setState(() {
              _segment = value!;
            });
          },
          children: {
            for (final segment in ["1", "2", "3"])
              segment: Padding(
                padding: const EdgeInsets.all(10),
                child: Text(segment),
              ),
          },
        ),
      ),
    );
  }
}
実行結果

メモ
CupertinoSegmentedControl とほぼ同じ
CupertinoSliverNavigationBar
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoPageScaffold(
      child: CustomScrollView(
        slivers: [
          CupertinoSliverNavigationBar(
            leading: Icon(CupertinoIcons.person),
            largeTitle: Text('Navigation bar title'),
            trailing: Icon(CupertinoIcons.plus),
          )
        ],
      ),
    );
  }
}
実行結果

メモ
下記の記事が興味深い
CupertinoSwitch
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});
  
  State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  bool _value = true;
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoSwitch(
          value: _value,
          onChanged: (value) {
            setState(() {
              _value = value;
            });
          },
        ),
      ),
    );
  }
}
実行結果

CupertinoTabScaffold + Bar + View
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});
  
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      tabBar: CupertinoTabBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.search_circle_fill),
            label: 'Explore',
          ),
        ],
      ),
      tabBuilder: (context, index) {
        return CupertinoTabView(
          builder: (context) {
            return CupertinoPageScaffold(
              navigationBar: CupertinoNavigationBar(
                middle: Text('Page 1 of tab $index'),
              ),
              child: Center(
                child: CupertinoButton(
                  onPressed: () {
                    Navigator.of(context).push(
                      CupertinoPageRoute<void>(
                        builder: (context) {
                          return CupertinoPageScaffold(
                              navigationBar: CupertinoNavigationBar(
                                middle: Text('Page 2 of tab $index'),
                              ),
                              child: Center(
                                child: CupertinoButton(
                                  onPressed: () {
                                    Navigator.of(context).pop();
                                  },
                                  child: const Text('Back'),
                                ),
                              ));
                        },
                      ),
                    );
                  },
                  child: const Text('Next page'),
                ),
              ),
            );
          },
        );
      },
    );
  }
}
実行結果

メモ
タブを変更した時に元のタブのページがちゃんと保存されているのがすごい
CupertinoTextField
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});
  
  State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  late TextEditingController _textEditingController;
  
  void initState() {
    super.initState();
    _textEditingController = TextEditingController(text: "initial text");
  }
  
  void dispose() {
    _textEditingController.dispose();
    super.dispose();
  }
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoTextField(
          controller: _textEditingController,
        ),
      ),
    );
  }
}
実行結果

メモ
inital text を設定するのがとても大変
CupertinoTimerPicker
コード
main.dart
import 'package:flutter/cupertino.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});
  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Navigation bar title'),
      ),
      child: Center(
        child: CupertinoButton(
          onPressed: () {
            showCupertinoModalPopup(
              context: context,
              builder: (context) {
                return Container(
                  color: CupertinoColors.systemBackground.resolveFrom(context),
                  height: 216,
                  child: CupertinoTimerPicker(
                    onTimerDurationChanged: (value) {},
                  ),
                );
              },
            );
          },
          child: const Text('Show time picker'),
        ),
      ),
    );
  }
}
実行結果

おわりに
以上で一旦クローズ、次は何を調べようかな
つづき
Flutter の carousel_slider を使ってみる、のスクラップを作成しました
このスクラップは2023/01/10にクローズされました