【Flutter】ウィジェットの基本的な使い方メモ

アイコン一覧
https://qiita.com/hosikiti/items/6e460ad325ce59f026d0

画像の登録
ディレクトリ作成し、そこにドラッグ&ドロップ

![スクリーンショット 2024-07-15 12.37.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136142/1a46f219-25b5-fa8d-c145-bf0cdfca7db5.png)

pubspec.yamlのassetsのコメントを外し、追加した画像ファイルの相対パスを記載する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136142/901d981f-843d-e183-6c21-96fb23442bf8.png)

```dart:main.dart
            // インターネット上の画像を表示するウィジェット
            Image.network(
              'https://jumpneeyan.com/wp-content/uploads/2020/10/kurapika_img_1.png',
              width: 300,
              // アスペクト比を保持したまま小さいのサイズを優先
              height: 100,
            ),
            // プロジェクト内の画像を表示するウィジェット
            Image.asset(
              'assets/kurapika.jpg',
              width: 300,
              height: 500,
            )
```

ウィジェット実装サンプル

```dart:main.dart
// マテリアルデザインをFlutterで実装するための部品群
import 'package:flutter/material.dart';

// 一番最初に実行されるメソッド
void main() {
  // 引数はアプリの大元のウィジェット
  runApp(const MyApp());
}

// StatelessWidgetは状態を持たないウィジェット
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    // マテリアルデザインアプリを構築するための大元のウィジェット
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

// StatefulWidget状態をもつウィジェット
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  // _MyHomePageStateにMyHomePageの状態を継承させる
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    // setStateはUIに通知するメソッド
    setState(() {
      print('Before $_counter');
      _counter++;
      print('After $_counter');
    });
  }

  @override
  Widget build(BuildContext context) {
    // Scaffoldはアプリの最も基本的な土台。appBarやbodyというパートを持つ
    // floatingActionButton要素もある（次へボタンに相当）
    return Scaffold(
      // 背景色。ここで条件分を記載できる
      backgroundColor: _counter % 4 == 0 ? Colors.yellow : Colors.blue,
      // AppBarはナビバーの部分
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        // widget.titleには上位で指定された'Flutter Demo Home Page'が入っている
        title: Text(widget.title),
      ),
      // bodyは本文
      body:
          // これでスクロール可能になる（セルの再利用するにはListViewを使う）
          SingleChildScrollView(
        // scrollDirectionはスクロール方向を指定。※ Axis.verticalはデフォルトなので省略可能
        // scrollDirection: Axis.horizontal,
        // Columnは縦に並べるウィジェット
        child: Column(
          // Columnの場合：
          // MainAxisAlignment.center -> 縦方向の親要素の中央に配置
          // MainAxisAlignment.start -> 縦方向の親要素の上揃えに配置
          // MainAxisAlignment.end -> 縦方向の親要素の下揃えに配置
          // CrossAxisAlignment.center -> 横方向の一番広い要素の中央に配置
          // CrossAxisAlignment.start -> 横方向の一番広い要素の左揃えに配置
          // CrossAxisAlignment.end -> 横方向の一番広い要素の右揃えに配置
          // -> CrossAxisAlignmentで要素同士の整列ではなく、全要素を整列したいなら、
          // Column自体をAlignmentでラップしてtopRightなどを指定する
          // Rowの場合：
          // ↑の縦方向と横方向の考え方が逆になるだけ
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // 独自ウィジェット。Columnの中に並べるウィジェット1(静的なウィジェットはconstをつける)
            const HellWidget(),
            HellWidget2(counter: _counter),
            // Textウィジェット。Columnの中に並べるウィジェット2(静的なウィジェットはconstをつける)
            const Text(
              'TextStyleテストTextStyleテストTextStyleテストTextStyleテストTextStyleテストTextStyleテストTextStyleテストTextStyleテストTextStyleテスト',
              maxLines: 2,
              // TextOverflow.ellipsisで3点リーダを表示
              overflow: TextOverflow.ellipsis,
              style: TextStyle(
                color: Colors.pink,
                fontSize: 30,
                fontWeight: FontWeight.bold,
              ),
            ),
            // Textウィジェット。Columnの中に並べるウィジェット3(動的なウィジェットはconstをつけない)
            Text(
              '$_counter', // 変数_counterを使う
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            // インターネット上の画像を表示するウィジェット
            Image.network(
              'https://jumpneeyan.com/wp-content/uploads/2020/10/kurapika_img_1.png',
              width: 300,
              // アスペクト比を保持したまま小さいのサイズを優先
              height: 100,
            ),
            // プロジェクト内の画像を表示するウィジェット
            Image.asset(
              'assets/kurapika.jpg',
              width: 300,
              height: 300,
            ),
            // ColoredBoxは小要素に色をつける（Containerでも同じことができる）
            const ColoredBox(
              color: Color.fromARGB(255, 0, 255, 0),
              child:
                  // Paddingウィジェット
                  // EdgeInsets.all -> 全てにパディング
                  // EdgeInsets.symmetric(vertical, horizontal) -> 上下または左右の指定されたものをパディング
                  // EdgeInsets.only(top, left, right, bottom) -> 引数省略可。指定があったものだけパディング
                  // EdgeInsets.zero -> パディングなし
                  const Padding(
                padding: EdgeInsets.all(50.0),
                child: Text("paddingテスト"),
              ),
            ),
            // Alignは小要素の位置を好きな位置に配置できる
            Align(
              alignment: Alignment.centerRight,
              child: Text("Alignテスト"),
            ),
            // ElevatedButtonは影付きのボタン
            ElevatedButton(
                style: ElevatedButton.styleFrom(
                    // 角丸ボタンの場合
                    // shape: RoundedRectangleBorder(
                    //   // 引数は丸める半径を指定
                    //   borderRadius: BorderRadius.circular(5)
                    // )
                    // 円ボタンにする場合(paddingで円の大きさを大きくなる）
                    shape: CircleBorder(),
                    padding: EdgeInsets.all(34)),
                onPressed: () {
                  print('テキストボタンを押しました');
                  // ダイアログを表示
                  showDialog(
                      context: context,
                      builder: (context) {
                        return AlertDialog(
                          title: Text("お知らせ"),
                          content: Text("テキストボタンを押しました"),
                          actions: [
                            TextButton(
                                onPressed: () {
                                  // ダイアログを閉じる
                                  Navigator.of(context).pop();
                                },
                                child: Text('close'))
                          ],
                        );
                      });
                },
                child: Text("これはテキストボタン1")),
            // 枠線付きのテキストウィジェット
            OutlinedButton(
                onPressed: () {
                  _pushPage();
                },
                child: Text("次の画面へ"))
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter, // ボタンが押された時に呼び出す関数を指定
        tooltip: 'Increment',
        child: const Icon(Icons.add), // ボタンのアイコン.add意外にもIcons.starとかも指定できる
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  void _pushPage() {
    Navigator.push(
// アプリの状況や位置を保持している情報
      context,
// 画面遷移を行うための情報をまとめたもの
      MaterialPageRoute(
        builder: (context) {
          // 遷移先のページ
          return ListViewTestPage(
            pageTitle: '遷移先ページのタイトル',
          );
        },
        // モーダル繊維は以下コメントを外せばいい
        // fullscreenDialog: true,
      ),
    );
  }
}

// stlと打つだけでlive templateでほとんど自動で出てくる
class HellWidget extends StatelessWidget {
  const HellWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return const Text("こんにちは");
  }
}

// stfと打つだけでlive templateでほとんど自動で出てくる
class HellWidget2 extends StatefulWidget {
  // 引数にcounterをとる
  HellWidget2({super.key, required this.counter});

  int counter;

  @override
  State<HellWidget2> createState() => _HellWidget2State();
}

class _HellWidget2State extends State<HellWidget2> {
  @override
  Widget build(BuildContext context) {
    return widget.counter % 2 == 0 ? const Text('おはよう') : const Text('こんばんは');
  }
}

class ListViewTestPage extends StatefulWidget {
  ListViewTestPage({required this.pageTitle});

  final String pageTitle;

  @override
  State<ListViewTestPage> createState() => _ListViewTestPageState();
}

class _ListViewTestPageState extends State<ListViewTestPage> {
  bool isChecked = false;
  double _currentSliderValue = 20;
  String inputText = "";
  bool switchVal = false;
  final TextEditingController _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // ListViewTestPageの引数はこのようにwidget.でアクセスできる
        title: Text(widget.pageTitle),
      ),
      body: ListView(
        children: [
          Container(
            color: Colors.cyan,
            height: 100,
          ),
          Container(
            color: Colors.pink,
            height: 100,
          ),
          // ListTileはListViewのアイテムを定義
          ListTile(
            // 一番初めのアイコン、サムネイル画像など
            leading: Icon(Icons.smoking_rooms),
            // タイトル
            title: Text('タイトル'),
            // サブタイトル
            subtitle: Text('サブタイトル'),
            // 一番最後のアイコン、サムネイル画像など
            trailing: Icon(Icons.arrow_forward_ios),
            onTap: () {
              print("tapped item");
            },
          ),
          // 削除可能なセルはDismissibleでラップする
          Dismissible(
            key: UniqueKey(),
            onDismissed: (direction) {
              // 削除処理
              print("delete");
            },
            child: ListTile(
              // 一番初めのアイコン、サムネイル画像など
              leading: Icon(Icons.smoking_rooms),
              // タイトル
              title: Text('削除可能なセル'),
              // サブタイトル
              subtitle: Text('サブタイトル'),
              // 一番最後のアイコン、サムネイル画像など
              trailing: Icon(Icons.arrow_forward_ios),
              onTap: () {
                print("tapped item");
              },
            ),
          ),
          // チェックボックス付きのListTile。これはListView意外にもColumnの要素にも使える
          CheckboxListTile(
            // checkboxの位置をどこにするかを指定する
            controlAffinity: ListTileControlAffinity.leading,
            // タイトル
            title: Text('タイトル'),
            value: isChecked,
            onChanged: (value) {
              setState(() {
                isChecked = !isChecked;
              });
            },
            // サブタイトル
            subtitle: Text('サブタイトル'),
          ),
          Slider(
              value: _currentSliderValue,
              min: 0,
              max: 100,
              // divisionsは何段階にするかを指定
              divisions: 100,
              label: _currentSliderValue.round().toString(),
              onChanged: (double value) {
                setState(() {
                  _currentSliderValue = value;
                });
              }),
          // SizedBox使い方1:余白を作る
          SizedBox(height: 30.0),
          // SizedBox使い方2:小ウィジェットのサイズを指定する
          // TODO: widthはなぜか効かない（ListViewだから？）
          SizedBox(
              width: 300.0,
              height: 60.0,
              child: ColoredBox(
                color: Colors.yellow,
                child: Text('slider value is $_currentSliderValue'),
              )),
          TextField(
            controller: _controller,
            keyboardType: TextInputType.number,
            decoration: InputDecoration(
              labelText: 'テキストフィールド',
            ),
            onChanged: (text) {
              setState(() {
                inputText = text;
              });
              print('First text field: $text');
            },
          ),
          Text('TextField value is $inputText'),
          Switch(
              value: switchVal,
              onChanged: (newValue) {
                setState(() {
                  switchVal = newValue;
                });
              }),
          Text('Switch value is $switchVal'),
          ElevatedButton(
              onPressed: () {
                // 元の画面に戻る処理（プッシュもモーダルもこれでOK）
                Navigator.pop(context);
              },
              child: Text('戻る'))
        ],
      ),
    );
  }
}

```

ダークモード対応

```dart main.dart

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

  ThemeData _lightTeme() {
    return ThemeData(
      brightness: Brightness.light,
      primarySwatch: Colors.cyan,
    );
  }

  ThemeData _darkTeme() {
    return ThemeData(
      brightness: Brightness.dark,
      primarySwatch: Colors.cyan,
    );
  }

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: _lightTeme(),
      darkTheme: _darkTeme(),
      // ここをThemeMode.darkにするとダークモードになる
      themeMode: ThemeMode.light,
      home: const MyHomePage(),
    );
  }
}

```

WebView
pubspec.yamlにwebview_flutterを追加してから

```dart
class WebViewPage extends StatefulWidget {
  const WebViewPage({super.key});

  @override
  State<WebViewPage> createState() => _WebViewPageState();
}

class _WebViewPageState extends State<WebViewPage> {
  //　lateは、後で初期化するという意味
  late final WebViewController webViewController;

  @override
  void initState() {
    super.initState();
    webViewController = WebViewController()
      // ..はカスケード記法。↑のコードを;で終わらせず、生成したオブジェクトのメソッドを使うという意味
      // JavaScript有効
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..loadRequest(Uri.parse('https://google.com'));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: WebViewWidget(
          controller: webViewController,
        ),
      ),
    );
  }
}
```

■上部のタブバー
- DefaultTabController
https://www.kamo-it.org/blog/25/

■下部のタブバー
- BottomNavigationBar
https://zenn.dev/urasan/articles/5bb85a54fb23fb

■Expanded
https://qiita.com/nannany_stores/items/d4114f615e4d53964121
https://flutter.salon/widget/expanded/
https://zenn.dev/pressedkonbu/articles/flexible-vs-expanded

■ウィジェットを傾ける
https://note-tmk.hatenablog.com/entry/2022/08/15/221820

■svgの表示
SvgPicture
https://zenn.dev/joo_hashi/articles/c6940c20ce06f7

■ウィジェットの配置

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136142/99429768-b489-c0a4-e46e-24e431ed3def.png)

```dart
      Container(
      width: 300, // ListViewの中では無効
      height: 300, // ListViewの中でも有効
      color: Colors.grey,
      child:
      Stack(
        children: [
            Align(
              alignment: Alignment.centerLeft,
              child: Container(
                height: 100, // このサイズを指定しない場合、デフォルトで親と同じ高さ = 黄色のviewがそうなってる
                color: Colors.blue,
                child: Column(
                  children: <Widget>[
                    // Expanded(child:
                    Text('centerLeft1'), // デフォルトで親の左上に合わせる
                    // ),
                    // const SizedBox(height: 10),
                    // Expanded(child:
                    Text('centerLeft2'),
                    // ),
                  ],
                ),
              ),
            ),

          // const SizedBox(width: 40),
          Align(
            alignment: Alignment.bottomRight,
            child: Container(
              color: Colors.yellow,
              child: Column(
                children: <Widget>[
                  // Expanded(child:
                  Container(
                    color: Colors.red,
                      child: Text('bottomRight1')), // デフォルトで親の左上に合わせる
                  // ),
                  // const SizedBox(height: 10),
                  // Expanded(child:
                  Container(
                    color: Colors.white,
                      child: Text('bottomRight2')),
                  // ),
                ],
              ),
            ),
          ),

        ],
      ),
    );


```