Chapter 03

Widgetの基本的な使い方

今回は Flutter の特徴である Widget について解説していきます。

Flutter大学のYoutubeでは下記です。文章にしていきますが、多分今回の内容は動画で実際に動いている様子を見た方がわかりやすいかなと思います笑

Widget の種類

WidgetにはAndroidアプリ向けの「Material」系統のWidgetとiOSアプリ向けの「Cupertino」系統のWidgetがあります。

Material系統のWidgetを使ったサンプル

Cupertino系統のWidgetを使ったサンプル

更にWidget は大きく分けて以下の2通りに分けられます。

  • 見た目をデザインするための Widget
  • レイアウトや状態管理をするための Widget

前回の章で紹介したScaffoldTextは見た目をデザインするための画面上に実際に表示されるWidgetで、CenterColumnは子Widgetをレイアウトをするための画面上には表示されないWidgetです。

Widgetの種類はMaterial系統のWidgetの方が豊富なため、AndroidもiOSもどちらもMaterial系統のWidgetを使って開発することが多いです。

この記事でもMaterialApp系統のWidgetを使って解説していきます。

Widget実装の基本

WidgetにはそれぞれのWidget毎にプロパティが予め用意されており、色を変えたり、大きさを調整したり、更にWidgetを追加したりなどはすべてWidgetが持つプロパティに書いていきます。プロパティとはWidgetがどんなものか表す情報のことです。

例えばこの画面は次のコードで構成されています。

Scaffold(
  backgroundColor: Colors.yellow,
  body: Center(
    child: Text(
      'Welcome to KBOYs Flutter University!!',
      style: TextStyle(color: Colors.blue),
    ),
  ),
);

それぞれ、

  • Scaffold : ①プロパティ「body」にCenterを設置、②プロパティ「backgroundColor」で背景色を黄色に指定
  • Center : プロパティ「child」にTextを設置
  • Text : プロパティ「style」にTextSyleを設置
  • TextStyel : プロパティ「color」 で文字の色を青に指定

このようにプロパティに必要なWidgetを設定していきます。

そのため、Widgetを把握することと、そのWidgetがどんなプロパティを持っていてどんな振る舞いをするかを把握することが非常に大事な要素となります。

なお、プロパティは「body」、「child」、「appBar」といったように小文字から始まり(lower camel case)、Widgetは「Text」、「Container」といったように大文字から始まる(upper camel case)というルールで用意されています。

また、AndroidStudioの場合、キーボードで「control + Space」と打つと、以下の画像のように追記可能なプロパティを確認することができます。

Widgetを実装してみよう

プロジェクトを新規作成したときのデモアプリをベースにWidgetを実装してみましょう。

プロジェクト作成時の初期コードと画面は次のようになっています!(2020年12月2日最新版のFlutter現在)

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
        // This makes the visual density adapt to the platform that you run
        // the app on. For desktop platforms, the controls will be smaller and
        // closer together (more dense) than on mobile platforms.
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  
  _MyHomePageState createState() => _MyHomePageState();
}

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

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug painting" (press "p" in the console, choose the
          // "Toggle Debug Paint" action from the Flutter Inspector in Android
          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

上記コードからコメント(//が左側にある、実行はしないがコメントだけつけれるもの)を削除すると、以下のようなコードになります。

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

  
  _MyHomePageState createState() => _MyHomePageState();
}

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

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

こちらをベースに変更していきながら、Widgetについて解説していきます。

Scaffold

Flutter OutlineでWidgetツリーを確認してみましょう。

上から4番目のScaffoldというWidgetからCenter、AppBar、FloatingActionButtonのWidgetにツリーが派生していますね。

次に先ほどのコードを見てみましょう。

    Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
       // 省略
      ),
      floatingActionButton: FloatingActionButton(
     // 省略
      ), 
    );

ScaffoldにはappBar、body、floatingActionButtonにそれぞれWidgetを配置しているのがわかります。

このappBarやbodyなどをScaffoldのプロパティと呼びます。
各Widgetにはそれぞれのプロパティが用意されていてカスタマイズすることができます。

そして、これらのWidgetは実際の画面上では以下の画像のような対応となっています。

このようにアプリの基本的な画面のレイアウトを構成してくれるのがScaffoldで、よく使われるWidgetです。とりあえずScaffold使っとけばいいと思います。笑

AppBar

続いてAppBarというWidgetを変更し、アプリのタイトル部分をカスタマイズしてみましょう。

アプリの下記の部分になります。

コードのこの部分を、、、

    Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
    // 省略
    );

以下みたいに変更すると、タイトルがKBOYのFlutter大学になります。

     Scaffold( 
    appBar: AppBar(
     title: Text('KBOYのFlutter大学'), 
    ),  
    // 省略 
   );

スマホ画面は↓みたいなかんじ。

また、AppBarにボタン(のようなもの)を追加してみたいと思います。以下のようにactionsプロパティにIconを2つ追加してみます。

      appBar: AppBar(
        title: Text(widget.title),
        actions: [
          Icon(Icons.add),
          Icon(Icons.share),
        ],
      ),

すると、↓のように、右側に2つのアイコンが並びました。今回はIconを並べましたが、FlatButtonやTextのWidgetも追加することができるので、いろいろ試してみてください。

Column

次は、Scaffoldのbodyプロパティの中身をいじりながら、ColumnというWidgetを紹介したいと思います。

body部分は現在以下みたいになっています。

      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),

今は、2つのTextウィジェットが、Columの中に入っていて、縦に二つ並んでいます。このようにColumnは縦に複数のウィジェットを並べて配置する時に使います。

これを例えば以下のように変更すると、

      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'KBOYさんの説明はとてもわかりやすい',
            ),
            Text(
              '↑わかる',
            ),
            Text(
              '↑しかもかっこよい',
            ),
          ],
        ),
      ),

下の画像のようになります。複数のWidgetを , で区切って配置してみましょう。

ちなみに、mainAxisAlignment: MainAxisAlignment.center, の部分は中央寄せという意味を持ちます。これを例えば、MainAxisAlignment.start に変更してみると、以下のように上寄せになります。

他にもMainAxisAlignmentには種類があるので、いろいろいじってみてください。

Row

次は、Columnに似たRowについて紹介します。先ほどのColumnの部分をRowに変更してみましょう。コードは以下のようになります。

      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            Text(
              'KBOYさんの説明はとてもわかりやすい',
            ),
            Text(
              '↑わかる',
            ),
            Text(
              '↑しかもかっこよい',
            ),
          ],
        ),
      ),

すると、プレビューとしては以下みたいになります。

先ほどは縦に並んでたのに対して、横並びになりました。

このように、Columnは縦並びに複数のWidgetを配置できるWidgetで、Rowは横並びに配置できるWidgetです。

Padding

次は周りに余白を作るWidgetのPaddingを紹介したいと思います。

今って、文字が画面の横にギリギリで張り付いてしまって、なんかダサいですよね。まわりに余白を作ってデザイン的にみやすくした方が良いと思います。そこで使うのがPaddingです。

Rowの左にカーソルを合わせつつ、キーボードで「option + Enter」を押してみてください。これで簡単に上からWidgetを囲むことができます。ここでPaddingを選択しましょう。

そしたら、以下のようになるはずです。

      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.start,
            children: <Widget>[
              Text(
                'KBOYさんの説明はとてもわかりやすい',
              ),
              Text(
                '↑わかる',
              ),
              Text(
                '↑しかもかっこよい',
              ),
            ],
          ),
        ),
      ),

画面としてはこんなかんじ。iPhone12ProMaxの画面サイズから少しはみ出したので、黄色いエラーが出ていますが、今回はWidgetについて学ぶのが目的なので一旦スルーしましょうw

とりあえず、左右に8ptの余白が空いたのが確認できるかと思います。

もっと余白を開けたい場合は、Paddingの中のpaddingプロパティのEdgeInsets.all(8.0)の8.0をもっとデカくすれば良いです。

例えば、32にしたら以下みたいな見た目になります。(はみ出してるの気になるけどw)

Container

先ほどと同様にしてPaddingのまわりに、Containerを追加してみましょう。すると、以下のようなコードになります。

      body: Center(
        child: Container(
          child: Padding(
            padding: const EdgeInsets.all(32),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.start,
              children: <Widget>[
                Text(
                  'KBOYさんの説明はとてもわかりやすい',
                ),
                Text(
                  '↑わかる',
                ),
		Text(
                  '↑しかもかっこよい',
                ),
              ],
            ),
          ),
        ),
      ),

ここで、Containerのプロパティのcolorをいじって色を赤くしてみましょう。

そしたら以下のようになります。(共通部分のコードは一部省略)

      body: Center(
        child: Container(
          color: Colors.red,
          child: Padding(
            // 省略
          ),
        ),
      ),

ちなみにContainerには、widthやheightのようなサイズを指定するプロパティもあります。例えばheightを400と指定してみると以下のようになります。

      body: Center(
        child: Container(
          color: Colors.red,
	  height: 400,
          child: Padding(
            // 省略
          ),
        ),
      ),

赤い部分の高さが変わりました。Containerは赤い部分なので、そのContainerの高さが400になって高くなった感じです。

ちなみに、height: double.infinity にすると、高さは画面最大になって画面全てが赤くなります。

widthも同様の要領で変更できるので試してみてください。

まとめ

今回の記事は以上です!

この中でもColumn、Row、Paddingは多用するWidgetですので、是非使い方を覚えて次に進みましょう!

参考資料

https://flutter.dev/docs/development/ui/widgets