Z軸にウィジェットを並べるStack

今日はStack について学習します。

Stackレイアウトについて

Stack は縦でも横でもなくウィジェットを重ねるためのレイアウトです。 基本的なイメージは前回のColumnやRowと同じ機能で複数のウィジェットを配置するために用いられます。

つまり、ColumnやRowと全く同じものになります。 ウィジェットの配置が違うだけで、仕組みは同じというのがわかります。

と思ったのですが、ColumnやRow みたいなプロパティが存在していませんでした。

つまり、

  • MainAxisAlignment
  • CrossAxisAlignment
  • MainAxisSize

のプロパティが見られませんでした。

使えるプロパティは

  Stack({
    Key key,
    this.alignment = AlignmentDirectional.topStart,
    this.textDirection,
    this.fit = StackFit.loose,
    this.overflow = Overflow.clip,
    List<Widget> children = const <Widget>[],
  })

のようになっています。

サンプルコードでStackの使い方の雰囲気を掴みます。

stackview.dart

import 'package:flutter/material.dart';

class StackView extends StatefulWidget {
  @override
  _StackViewState createState() => _StackViewState();
}

class _StackViewState extends State<StackView> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Stack Sample'),
      ),
      body: Stack(
        children: _stackViews,
      ),
    );
  }

  var _stackViews = <Widget>[
    Stack(
      children: <Widget>[
        Container(
          color: Colors.red,
          width: 300.0,
          height: 300.0,
          child: Text(
            "1st View",
            style: TextStyle(
                fontSize: 30.0,
                fontWeight: FontWeight.w400,
                fontFamily: "Roboto"),
          ),
        ),
        Container(
          color: Colors.green,
          width: 200.0,
          height: 200.0,
          child: Text(
            "2nd View",
            style: TextStyle(
                fontSize: 30.0,
                fontWeight: FontWeight.w400,
                fontFamily: "Roboto"),
          ),
        ),
        Container(
          color: Colors.blue,
          width: 100.0,
          height: 100.0,
          child: Text(
            "3rd View",
            style: TextStyle(
                fontSize: 30.0,
                fontWeight: FontWeight.w400,
                fontFamily: "Roboto"),
          ),
        ),
      ],
    )
  ];
}

main.dart

import 'package:flutter/material.dart';
import './stackview.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  final title = 'Stackのサンプルコード';
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      debugShowCheckedModeBanner: true,
      title: 'Sample App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
        primaryColor: const Color(0xff2196f3),
        accentColor: const Color(0xff2196f3),
        canvasColor: const Color(0xfffafafa),
      ),
      home: new StackView(),
    );
  }
}

これをビルドすると次のスクリーンショットのように表示されます。

f:id:qed805:20200121001759p:plain
Stackのサンプル

iOSでいえば普通のUIViewのような印象を受けますが、複数のウィジェットを重ねるときにStackを使うと覚えればうまくレイアウトの配置ができると思いました。 次はGridViewを予定していますが、GridViewのGridView.countとGridView.extendの違いがまだ理解していませんので 丁寧に学習を進めたいと思っています。

Column と Row ウィジェットで複数のウィジェットを配置する

今日はColumn とRow について学習しました。

Flutter のColumn とRow の概念はiOSのUIKitにはないようなUIです。 一つのレイアウトに複数のウィジェットを配置したい場合に使います。

Androidに近い概念があります。 そのためAndroidのレイアウトと比較してみます。

Flutter Android
Column LinearLayout(Vertical)
Row LinearLayout(Horizontal)

AndroidのLinearLayoutは縦か横にUI を並べますがまさにそんな感じのウィジェットというイメージです。 LinearLayoutで思い出したのですが、そういえばiOSにも似た概念にStackViewがあったことを忘れていました。

つまり、Flutter・AndroidiOSのそれぞれで比較すると

Flutter Android Android
Column LinearLayout(Vertical) StackView(Vertical)
Row LinearLayout(Horizontal) StackView(Horizontal)

こういうことになりますね。 ただColumnとRowはそれぞれ逆にもウィジェットを並べることができるので一概に縦・横に一列に並べるという表現は違いました。 それはおいおい説明すると思います。

Columnのプロパティについて

Column にはいくつかのプロパティが用意されています。

プロパティ 役割
Main Axis Alignment Columnウィジェット「自体」の配置場所を指定する
Cross Axis Alignment Columnに「入れた」ウィジェットの配置場所を指定する
Main Axis Size ウィジェット自体のサイズを指定する

それでは試しにサンプルのソースコードを書いていきます。

stackview.dart

import 'package:flutter/material.dart';

class StackView extends StatefulWidget {
  @override
  _StackViewState createState() => _StackViewState();
}

class _StackViewState extends State<StackView> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Column Sample'),
        ),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          mainAxisSize: MainAxisSize.max,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              '1st View',
              style: TextStyle(
                  backgroundColor: Colors.blue,
                  fontSize: 30.0,
                  color: Colors.white,
                  fontWeight: FontWeight.w400,
                  fontFamily: "Roboto"),
            ),
            Text(
              '2nd View',
              style: TextStyle(
                  backgroundColor: Colors.green,
                  fontSize: 30.0,
                  color: Colors.white,
                  fontWeight: FontWeight.w400,
                  fontFamily: "Roboto"),
            ),
            Text(
              '3rd View',
              style: TextStyle(
                  backgroundColor: Colors.red,
                  fontSize: 30.0,
                  color: Colors.white,
                  fontWeight: FontWeight.w400,
                  fontFamily: "Roboto"),
            ),
          ],
        ));
  }
}

main.dart

import 'package:flutter/material.dart';
import './stackview.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  final title = 'FloatingActionButtonのサンプルコード';
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      debugShowCheckedModeBanner: true,
      title: 'Sample App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
        primaryColor: const Color(0xff2196f3),
        accentColor: const Color(0xff2196f3),
        canvasColor: const Color(0xfffafafa),
      ),
      home: new StackView(),
    );
  }
}

これをビルドします。今回はAndroidエミュレータを使います。

f:id:qed805:20200119234116p:plain
Column を使ったサンプル

このように見えます。

試しにここからMain Axis Alignmentを操作していきます。

Main Axis Alignment が start

f:id:qed805:20200119234248p:plain
Main Axis Alignment が start

Main Axis Alignment が center

f:id:qed805:20200119234313p:plain
Main Axis Alignment が center

Main Axis Alignment が end

f:id:qed805:20200119234357p:plain
Main Axis Alignment が end

このように見えることがわかりました。 次にCross Axis Alignmentを操作していきます。

Cross Axis Alignment が start

f:id:qed805:20200119234248p:plain
Cross Axis Alignment が start

Cross Axis Alignment が center

f:id:qed805:20200119234629p:plain
Cross Axis Alignment が center

Cross Axis Alignment が end

f:id:qed805:20200119234707p:plain
Cross Axis Alignment が end

わかりにくいのでTextの文字を変更してwidth の調整をしてみます。

Cross Axis Alignment が center

f:id:qed805:20200119235723p:plain
Cross Axis Alignment が center

Cross Axis Alignment が end

f:id:qed805:20200119235751p:plain
Cross Axis Alignment が end

これでだいぶ見やすくなりました。

このように Column はウィジェットを複数個を配置して縦に見せるレイアウトでした。

Rowのプロパティについて

Colum は縦にウィジェットを並べるものでした。それに対してRow はウィジェットを横に並べるレイアウトになります。 では、試しに先程のサンプルコードをColumnからRow に変更してみましょう。

stackview.dart

import 'package:flutter/material.dart';

class StackView extends StatefulWidget {
  @override
  _StackViewState createState() => _StackViewState();
}

class _StackViewState extends State<StackView> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Column Sample'),
        ),
        body: Row(
          mainAxisAlignment: MainAxisAlignment.start,
          mainAxisSize: MainAxisSize.max,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              '1st View',
              style: TextStyle(
                  backgroundColor: Colors.blue,
                  fontSize: 30.0,
                  color: Colors.white,
                  fontWeight: FontWeight.w400,
                  fontFamily: "Roboto"),
            ),
            Text(
              '2nd View',
              style: TextStyle(
                  backgroundColor: Colors.green,
                  fontSize: 30.0,
                  color: Colors.white,
                  fontWeight: FontWeight.w400,
                  fontFamily: "Roboto"),
            ),
            Text(
              '3rd View',
              style: TextStyle(
                  backgroundColor: Colors.red,
                  fontSize: 30.0,
                  color: Colors.white,
                  fontWeight: FontWeight.w400,
                  fontFamily: "Roboto"),
            ),
          ],
        ));
  }
}

ColumnをRow に変更しただけです。 これでウィジェットを横に並べることができます。

f:id:qed805:20200120000154p:plain
Row で並べたウィジェットの見え方

そして、ColumnとRow の使い分けと覚え方についてです。 iOSエンジニアをやっていると UITableView を使うことが多いと思います。 UITableView に indexPath.row というのがあってこのrowが列を表していて縦にUIが並んでいました。 なのでiOSエンジニアだとRow が縦向きのイメージを持ちますが、「Flutterでは逆にRowは横に並べる」と覚えてしまえばいいかと思います。 結構雑な覚え方になりますが私はこれで「Row=横」のイメージに切り替えられました。

以上で Column と Row の具体的な使い方がわかりました。

レイアウトウィジェットについて

今回はFlutter のレイアウトで使うレイアウトの一部について学習します。

学習するウィジェットはText, Center, Container クラスです。

Text ウィジェット について

まずはサンプルコードを書きます。

      Text(
        'Hello Flutter',
        style: TextStyle(fontSize: 32.0,
            color: const Color(0xff000000),
            fontWeight: FontWeight.w700,
            fontFamily: "Roboto"),
      ),

Android Studio で Text にフォーカスさせて Text クラスの定義にジャンプしてみます。

  /// The [data] parameter must not be null.
  const Text(
    this.data, {
    Key key,
    this.style,
    this.strutStyle,
    this.textAlign,
    this.textDirection,
    this.locale,
    this.softWrap,
    this.overflow,
    this.textScaleFactor,
    this.maxLines,
    this.semanticsLabel,
    this.textWidthBasis,
  }) 

最初の引数 data が必須ということがわかります。

f:id:qed805:20200116224831p:plain
Text の定義についてジャンプする

style プロパティについて

次の引数である style について調べてみます。 この値はTextStyleというクラスのインスタンスを入れたらいいらしいです。

fontSize フォントのサイズ。double型
fontWeight フォントの太さ。FontWeight というクラスを入れる。
fontFamily フォントファミリー。
fontStyle フォントのスタイル。normal や italic という定数を指定する
color テキストの色。Colorクラスで指定する

Color プロパティについて

Color のインスタンスを生成する際にはいくつかのパターンが存在する

/// 16進数指定
const Color(0xff000000)

/// A RGB 指定 (A = alpha)
const Color.fromARGB(255, 255, 0, 0)

/// RGB O 指定 (O = opacity)
Color.fromRGBO(38, 38, 38, 0.4)

この3パターンの生成方法がありました。 これらを把握すればText を生成して画面に表示できるようになると思います。

Center によるレイアウトの位置調整

次にText を画面のどこに配置するかを指定したいことがよくあります。 iOSアプリではstoryboard や xib でごちゃごちゃとUIKit を置いていきますが、 Flutter はそんなことはしないみたいです。

ここではレイアウトを調整できるクラスを数個紹介します。

  • Center クラス (画面中央揃え)
  • Container クラス (細かな配置を設定できる)

Center クラスの基本形

     Center(
        child: ウィジェット,
      )

Center クラスに関する簡単なサンプルコードを書きます。

sample_page.dart

import 'package:flutter/material.dart';

// ウィジェット
class SamplePage extends StatefulWidget {
  final title;

  SamplePage({this.title}): super();
  _SamplePageState createState() => new _SamplePageState();
}

// 状態を持つクラス
class _SamplePageState extends State<SamplePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('App Name'),
        ),
      body:
      Center(
        child: Text(
          "Text クラス",
          style: TextStyle(
            fontSize: 32.0,
            color: Color(0xff000000),
            fontWeight: FontWeight.w700,
            fontFamily: "Roboto"
          ),
        ),
      )
    );
  }
}

これでビルドすると「Text クラス」という文字が画面中央に表示されます。

f:id:qed805:20200116230729p:plain
Center クラスのサンプル例

Container クラスの基本形

ウィジェットの配置を制御するレイアウトでもう一つContainer クラスというものがあります。 Center クラスはレイアウトを中央に寄せるだけですが、Container クラスは左右上下という風にある程度配置を制御できます。

      Container(
        child: ,
        padding: EdgeInsets,
        alignment: Alignment,
      )

Alignment については9パターンが用意されています。

topLeft 左上
topCenter 左中央
topRight 右上
centerLeft センター左
center 中央
centerRight センター右
bottomLeft 左下
bottomCenter 中央した
bottomRight 右下

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

  /// The top left corner.
  static const Alignment topLeft = Alignment(-1.0, -1.0);

  /// The center point along the top edge.
  static const Alignment topCenter = Alignment(0.0, -1.0);

  /// The top right corner.
  static const Alignment topRight = Alignment(1.0, -1.0);

  /// The center point along the left edge.
  static const Alignment centerLeft = Alignment(-1.0, 0.0);

  /// The center point, both horizontally and vertically.
  static const Alignment center = Alignment(0.0, 0.0);

  /// The center point along the right edge.
  static const Alignment centerRight = Alignment(1.0, 0.0);

  /// The bottom left corner.
  static const Alignment bottomLeft = Alignment(-1.0, 1.0);

  /// The center point along the bottom edge.
  static const Alignment bottomCenter = Alignment(0.0, 1.0);

  /// The bottom right corner.
  static const Alignment bottomRight = Alignment(1.0, 1.0);

Container クラスに関する簡単なサンプルコードを書きます。

sample_page.dart

import 'package:flutter/material.dart';

// ウィジェット
class SamplePage extends StatefulWidget {
  final title;

  SamplePage({this.title}): super();
  _SamplePageState createState() => new _SamplePageState();
}

// 状態を持つクラス
class _SamplePageState extends State<SamplePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('App Name'),
        ),
      body:
      Container(
        child: Text(
          "Container",
          style: TextStyle(
            fontSize: 32.0,
            color: Color(0xff000000),
            fontWeight: FontWeight.w700,
            fontFamily: "Roboto"
          ),
        ),
        padding: EdgeInsets.all(10.0),
        alignment: Alignment.bottomRight,
      )
    );
  }
}

これでビルドすると「Container クラスのサンプル例」という文字が画面下側の右に表示されます。

f:id:qed805:20200116232257p:plain
Container レイアウトのサンプル例

ということでウィジェットの配置の方法について学習しました

Flutter におけるボタンウィジェットについて

今回はFlutter で使えるボタンのウィジェットについて学習します。

ButtonWidget の種類について

これまでのバックグラウンドがiOSエンジニアでしたのでiOSと比べてみます。

Flutter iOS
FlatButton UIButtonと同じ
RaisedButton UIButtonが立体的に表示される
IconButton Image付きのUIButtonと同じ
FloatingActionButton AndroidのFABのように見えるが普通のボタンとしても使える
RawMaterialButton ドロップシャドウの付いたUIButton?
FloatingActionButton AndroidのFABのように見えるが普通のボタンとしても使える

FlatButton の基本形について

FlatButton のプロパティの例として、下記のサンプルコードを書いてみます。

// 状態を持つクラス
class _AWidgetState extends State<AWidget> {
  @override
  Widget build(BuildContext context) {
    FlatButton(
      // よく分かりません
      key: null,
      onPressed: _buttonPressed,
      // 背景色
      color: Colors.black12,
      // View
      child: Padding(
        padding: EdgeInsets.all(5.0),
        child: Icon(
          Icons.android,
          size: 50.0,
        ),
      ),
    );
  }
  
  void _buttonPressed() {
    setState(() {
      // ボタンを押したときの処理
    });
  }
}

ビルドはしていませんが、これでFlatButtonが表示されてタップしたときの処理が書けます。 iOSのSwift では UIButton 表示まで色々な処理を書きますがFlutterではこれだけでボタンを表示・タップができます。

setState メソッドについて

_buttonPressed() メソッドの中でsetStateというメソッドを実行しています。 このsetStateはStateの更新をStateクラスに知らせる機能を持っています。

試しにFloatingActionButton をタップしてメッセージを更新させるサンプルコードを書いてみます。

main.dart

import 'package:flutter/material.dart';
import 'package:practice_app/sample_page.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  final title = 'FloatingActionButtonのサンプルコード';
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter App',
      home: new SamplePage(
        title: this.title,
      ),
    );
  }
}

sample_page.dart

import 'package:flutter/material.dart';

// ウィジェット
class SamplePage extends StatefulWidget {
  final title;

  SamplePage({this.title}): super();
  _SamplePageState createState() => new _SamplePageState();
}

// 状態を持つクラス
class _SamplePageState extends State<SamplePage> {
  String _message;
  int _count;

  @override
  void initState() {
    super.initState();
    _message = 'Hello';
    _count = 0;
  }

  void _incrementCount() {
    setState(() {
      _count ++;
      _message = 'ボタンを$_count回タップしました';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Text(
        _message,
        style: TextStyle(fontSize: 16.0),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCount,
        tooltip: 'set Message',
        child: Icon(Icons.android),
      ),
    );
  }
}

f:id:qed805:20200116002440p:plain
ボタンをタップしてcountをふやしてメッセージを更新

これでソースコードをビルドするとボタンをタップするたびにcountが増えていき、 メッセージに「ボタンをcount回タップしました」というメッセージが表示されます。

ということでボタンの表示とタップ処理ができるようになりました。

Flutter のState クラスについて勉強する

今回はFlutter の状態を扱う機能であるState クラスについて勉強します。

これまで書いてきた StateクラスはStatelessWidget(静的クラス)でした。 Flutter のState は2種類存在します。

  • StatelessWidget (静的なState)
  • StatefulWidget (動的なState)

よくFlutter の基本的なソースコードである

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  String title = 'Flutter のテストアプリ';
  String message = 'Hello World, Flutter';
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      debugShowCheckedModeBanner: false,
    );
  }
}

で使われるときがStatelessWidget で考えるような感じです。 それに対して変数によって値を変えたいときに使うウィジェットの場合にはStatefulなウィジェットを継承させてクラスを作成します。

StatefulWidget の基本形な次のとおりです。

// ウィジェット
class AWidget extends StatefulWidget {
  _AWidgetState createState() => new _AWidgetState();
}

// 状態を持つクラス
class _AWidgetState extends State<AWidget> {
  @override
  Widget build(BuildContext context) {
  }
}

と2つのクラスを作成しなければいけない感じです。 この基本形は丸暗記すればよさそう。

このStatefulWidgetを使ったサンプルコードを下に記載します。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  String title = 'Flutter のテストアプリ';
  String message = 'Hello World, Flutter';
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter App',
      home: new SamplePage(
        title: this.title,
        message: this.message,
      ),
    );
  }
}

// ウィジェット
class SamplePage extends StatefulWidget {
  String title;
  String message;

  SamplePage({this.title, this.message}): super();
  _SamplePageState createState() => new _SamplePageState();
}

// 状態を持つクラス
class _SamplePageState extends State<SamplePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.message),
      ),
      body: Text(widget.message,
      style: TextStyle(fontSize: 32.0),
      ),
    );
  }
}

ぱっと見ではネストが深いイメージがあります。 これが最小単位でビルドすると下のスクリーンショットの画像が表示されます。

f:id:qed805:20200115224336p:plain
StatefulWidgetを使った最小コード

Widgetクラスで別ファイルに切り分ける

本当はこのまま下にStateクラスを作成して入れ子に組み込んでいけばレイアウトができてくるのですが、 私の場合まだ慣れないのでここでSamplePageを別ファイルに分離して切り分けたいと思います。

Flutter の場合、新しいDartファイルを作成してそこに移動させたらいいみたいです。

f:id:qed805:20200115224847p:plain

このようにlibディレクトリの中に新しいdartクラスsample_widgetファイルを作成しました。

このsample_widget のファイルに下のソースコードを書きます。

import 'package:flutter/material.dart';

// ウィジェット
class SamplePage extends StatefulWidget {
  String title;
  String message;

  SamplePage({this.title, this.message}): super();
  _SamplePageState createState() => new _SamplePageState();
}

// 状態を持つクラス
class _SamplePageState extends State<SamplePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.message),
      ),
      body: Text(widget.message,
        style: TextStyle(fontSize: 32.0),
      ),
    );
  }
}

f:id:qed805:20200115225026p:plain

そして、main.dart のファイルを次のように編集します。 といっても、sample_widgetファイルを読み込むためにimport しただけです。

import 'package:flutter/material.dart';
import 'package:practice_app/sample_page.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  String title = 'Flutter のテストアプリ';
  String message = 'Hello World, Flutter';
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter App',
      home: new SamplePage(
        title: this.title,
        message: this.message,
      ),
    );
  }
}

これで作成したStatefulWidget を別ファイルからimportして使えるようになりました。 まだFlutte 触りだして1週間ですのでクラス名も分離の基準もこれで正しいか分かりません。

ですが、これでUIと画面を分離できたかなと思います。

Stateのプロパティにアクセスするwidgetプロパティ

ここで説明を省略しましたが、Stateクラスで使われているwidgetというものがあります。 widget.titleとかwidget.messageと書かれている箇所です。 widgetとはStateクラスに用意されているプロパティで_SamplePageStateそのものを指します。

import 'package:flutter/material.dart';

// 状態を持つクラス
class _SamplePageState extends State<SamplePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.message),
      ),
      body: Text(widget.message,
        style: TextStyle(fontSize: 32.0),
      ),
    );
  }
}

簡単に言えば、Stateを継承しているのでこのStateを継承することで widget プロパティ(つまり、this)にアクセスできる仕様なんだと思います。

とりあえず納得するしかありません。

これでだいたいStateについて学べたかと思います。