カテゴリー: iOSAndroid

Flutter開発のはじめかた 開発環境セットアップ〜Widtgetの解説

はじめに

Flutterとは、マルチプラットフォームで動作するUIフレームワークです。単一のソースコードからiOS/Android/Web/デスクトップ用にコンパイルすることが可能です。様々な「ウィジェット」と呼ばれるUIパーツが初めから用意されており、それを組み合わせることで、簡単に美しいUIを実装することができます。
プログラミング言語には「Dart」を使用します。Dartは、元々JavaScriptを置き換える目的で開発された言語ですが、今ではFlutterで使用できることで有名です。今回は、Dartの説明はあまり深掘りしませんが、JavaScriptやJavaをやったことのある開発者であれば、すんなり理解できると思います。

セットアップ

Flutterのインストール

Flutterはこちらからインストールできます。インストール方法は各OSにより異なりますが、ここではmacOSでのインストール方法を説明します。
https://flutter.dev/docs/get-started/install

まず、Flutterのリポジトリをクローンします

$ mkdir ~/development
$ cd ~/development
$ git clone https://github.com/flutter/flutter.git -b stable

次に、パスを通します。bashの場合は ~/.bash_profile、zshの場合は ~/.zshenvに記述します。

$ export PATH="$PATH:~/development/flutter/bin"

sourceコマンドでパスを読み込むか、ターミナルを再起動して、dockerのコマンドを試してみましょう。

$ flutter doctor

このコマンドは開発環境をチェックし、必要なセットアップを表示してくれます。それでは、進めていきましょう。
(Dart SDKはFlutterに含まれているため、セットアップは不要です。)

Android環境のセットアップ

Android Studioに含まれているAndroid SDKを使用します。以下からAndroid Studioをインストールしてください。
https://developer.android.com/studio/?hl=ja
インストール後、Android Studioを起動し、セットアップウィザードに従い、Android SDKをインストールしてください。
(Android Studioは、後ほどIDEとして使用します。)

Xcodeのインストール

iOS ビルドにはXcodeをインストールします。(Mac AppStoreからインストールしてください。)
インストール後、コマンドラインツールの設定を行います。

$ sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
$ sudo xcodebuild -runFirstLaunch

ライセンスに同意し、セットアップを進めてください。

IDEのセットアップ(Android Studio)

Flutterの開発環境として、Android StudioもしくはVisual Studio Codeが推奨されています。いずれも、Flutterプラグインを導入することで、Flutterのデバッグ・ホットリロードができるようになります。
今回は、Android Studioでのセットアップ方法を紹介します。

  1. Android Studioを起動し、Preferences画面を開きます。(Welcome画面 > Configure > Preferences)
  2. Editor > Pluginsを開き、検索ボックスで flutterと検索します。
  3. Flutterプラグインの Installボタンでインストールします。
  4. 途中、Dart Pluginのインストールを尋ねるプロンプトが表示された場合は、「Yes」ボタンでインストールします。
  5. 指示に従ってAndroid Studioを再起動します。

以上で、IDEのセットアップは完了です。これで、Android Studio上でFlutter開発を一通り行う準備ができました。

Flutterプロジェクトの新規作成

  1. Android StudioのWelcome画面で「Start a new Flutter Project」を選択します。
  2. ベースとなるプロジェクトを以下の中から選択します。
    • Flutter Application
      ユーザ向けのアプリを開発する際に選択。
    • Flutter Plugin
      AndroidやiOSのネイティブAPIをFlutterから使用できるようにするプラグインを開発
    • Flutter Package
      Dartコンポーネントを作成するために使用します。例えば、自作のWidgetを、複数のプロジェクトで共有する場合に使用できます。
    • Flutter Module
      既存のAndroid/iOSアプリにFlutterコンポーネントを組み込むモジュールを開発する場合に使用します。
  3. プロジェクト設定を行います。
    Project name、Project locationを入力して進みます。Project nameはスネークケースで入力しましょう。
  4. パッケージネームの設定をします。
    Company domainは、パッケージ名に使用する企業名を指定します。Androidでのパッケージ名、iOSでのBundle Identifierで同じ値が使用されます。
    Android X、Kotlin support、Swift supportにチェックを入れます。

iOS実機ビルド

iOS実機ビルドを行うため、署名設定を行います。

  1. Flutterプロジェクト内の ios/Runner.xcworkspaceをXcodeで開きます。
  2. プロジェクト設定の “Signing & Capabilities” を開き、チーム設定等の署名設定を行います。ここは、通常のiOSアプリ開発と同じです。

この後、Android Studioで接続された実機を選択し、Runをクリックすると、実機でFlutterアプリが起動します。

Android実機ビルド

Androidの実機を接続し、iOSと同じように実機を選択し、Runすると起動します。

ホットリロード・ホットリスタート

Flutterの開発中の便利機能として、ホットリロードがあります。これは、コードをSaveすると、デバッグ中のアプリがランタイムで更新され、最新のコードが適用される機能です。
一方で、ホットリスタートは、一般的なAndroid/iOSアプリ開発と同じように、アプリをRunし直すことで、最新のコードを適用する方法です。
レイアウト変更などの、軽微な変更はホットリロードが効きますが、変更内容によってはホットリロードが効かない場合もあります。そのような場合は、ホットリスタートを行いましょう。(Android Studioであれば、 Ctrl + rでホットリスタートです)

Flutterプロジェクトの構成

Flutterプロジェクトでは、主に以下のディレクトリ・ファイルを開発に使用します。

  • lib/
    Dartコードを記載するディレクトリです。
  • test/
    Dartのテストコードを配置するディレクトリです。
  • ios/
    Xcodeプロジェクトが配置されるディレクトリです。
  • android/
    Androidプロジェクトが配置されるディレクトリです。
  • pubspec.yaml
    外部ライブラリを導入したり、アセットを登録する際に使用します。

Widgetツリー構造

Flutterでは、UIをWidgetという単位で構築し、アプリのルートから全てツリー構造となっています。
以下に、プロジェクト作成直後のWidgetツリーを図にしました。

ルートは MaterialAppWidgetとなり、 homeに渡すWidgetが、初期画面となります。ここでは、 MyHomePageを設定し、さらにその下に画面に表示するUIがツリーとなっています。

MyHomePage直下のScaffoldは、1画面を構成する土台です。 AppBar,Body,FloatingActionButtonなどの枠にWidgetを当てはめることで、簡単にアプリのUIを構築できます。大抵の場合は、 Scaffoldを使用することをお勧めします。

1画面に必要なクラス

それでは、このツリー構造をクラスで表すと、どのようになるでしょうか。
まず、画面が「動的に変化するかどうか」で大きく分かれます。動的に変化する画面の場合、 StatefulWidgetを継承したクラスを作成します。逆に、静的な画面であれば、 StatelessWidgetを継承したクラスを使用します。

StatefulWidgetを使用した画面の作り方

StatefulWidgetを使用する場合は、次の2つのクラスが必要です。

  • StatefulWidgetを継承したクラス
  • State<T>を継承したクラス

プロジェクト作成直後のコードを例に、解説します。まずは、 StatefulWidgetを継承した MyHomePageクラスを見てみましょう。
ccreateState()をオーバーライドし、後述する State<T>のインスタンスをセットします。

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

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

次に、State<T>を継承した _MyHomePageStateクラスを見ていきましょう。
まず、状態を保持しておくフィールド変数を定義します。 build()メソッドで、フィールド変数を使用して、Widgetを構築していきます。
フィールド変数を更新するときは、 setState()のクロージャ内で変数を更新すると、 Widget build(BuildContext context)がコールされWidgetが再構築されることで、UIが更新されます。

class _MyHomePageState extends State; {
  int _counter = 0;

  void _incrementCounter() {
    // フィールド変数をこのように更新すると、Widgetが再構築される
    setState(() {
      _counter++;
    });
  }

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

 

StatelessWidgetを使用した画面の作り方

StatelessWidgetを継承したクラスを作成します。 StatelessWidgetは状態を変化させることができません。そのため、 State<T>クラスは不要で、直接 build()メソッドを実装します。

class HelloWorldPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(appBar: AppBar(
      title: Text('Hello World Page'),
    ),
      body: Text('Hello Flutter!'),
    );
  }
}

 

画面遷移

画面遷移を行うには、 Navigatorクラスを使用します。

Push/Pop

画面遷移するには、 Navigator.push()メソッドを使用します。第2引数で MaterialPageRouteクラスを使い、次の画面のインスタンスを生成して渡します。

Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => SecondPage()
));

前の画面に戻るには Navigator.pop(context)と実装すればOKです。また、Push遷移すると、左上に戻るボタンが表示されるため、この機能で十分であれば、Pop遷移の実装は必要ありません。

Modal

モーダル遷移の場合も Navigator.push()を使用します。 fullscreenDialog引数を trueに設定すると、モーダル遷移になります。

Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => SecondPage(),
        fullscreenDialog: true
));

値の渡し方

次の画面に値を渡すには、遷移先でプロパティを定義し、コンストラクタで値を渡します。
まず、遷移先のコードです。

import 'package:flutter/material.dart';

class SecondPage extends StatelessWidget {
  // プロパティ定義
  final String name;

  // コンストラクタ
  SecondPage(this.name);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('SecondPage')
      ),
      body: Text('こんにちは$nameさん') // 受け取った値を表示する
    );
  }
}

nameをコンストラクタで受けとり、 Widget build(BuildContext context)内でテキスト表示しています。
プロパティを finalとしているため、コンストラクタの実装が必須となります。(ただし、後からプロパティの値を変更する場合は、 finalを付ける必要はありません)
次に、遷移元です。

class _MyHomePageState extends State {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // ボタンをタップして遷移用メソッドを呼び出す
            RaisedButton(
              onPressed: _openSecondPage,
              child: Text('次の画面へ')
            )
          ],
        ),
      ),
    );
  }

  // ボタンタップ時に呼ばれるメソッド
  void _openSecondPage() {
    Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => SecondPage('山田'))  // コンストラクタでの値渡し
    );
  }
}

プロジェクトテンプレートのHomePageを変更し、次の画面に値を渡すようにしました。
値を渡す際は、 MaterialPageRotue()で次の画面を生成する際、コンストラクタに値を渡すことで、実現できます。

値の戻し方

値を戻す際は、 Navigator.pop()メソッドに値を渡します。

RaisedButton(
  child: Text('戻る'),
  onPressed: () {
    Navigator.pop(context, 'もう一度'); // 第二引数に、戻したい値をセット
  }
)

次に、戻した値を受け取る実装です。

class _MyHomePageState extends State {
  String _buttonText = '次の画面へ'; // ボタンテキストを変更するためのプロパティ

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('1画面目'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            RaisedButton(
              onPressed: () async {
         // awaitを使用してpopしたときに返り値を受け取ることができるようにする
                String message = await Navigator.push(
                    context,
                    MaterialPageRoute(builder: (context) => SecondPage('山田'))
                );
          // 受け取った値を使用して、ステート更新(ボタンのテキストが変わります)
                setState(() {
                  _buttonText = message;
                });
              },
              child: Text(_buttonText)
            )
          ],
        ),
      ),
    );
  }
}

このサンプルでは、戻ってきた値を使用し、ボタンのテキストを変化させるようにしました。
戻り値の受取は非同期となるため、 awaitを使用します。また、 awaitを使用する場合は、関数を asyncとします。

Widget

Widgetは、FlutterのUIを定義するオブジェクトです。Widgetの特徴としては、UIのほぼ全てをWidgetやそのツリーで構成されていることが挙げられます。
また、ユーザ操作に対するレスポンスが優れているUI/UXが、Widgetとして数多く存在しています。また、その多くはカスタマイズ可能であり、もちろん自作のWidgetを作成することも可能です。
ここでは、使用頻度の高いWidgetを紹介します

レイアウト

Scaffold

画面の骨組みとなるレイアウトです。AppBar, FAB, BottomNavigationといった、基本的なUIの骨組みを提供します。基本的に、各画面のルートWidgetは、Scaffoldを使用することをお勧めします。

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('SecondPage')
      ),
      body: Column(
        children: [
          Text('こんにちは'),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.search),
        tooltip: 'Search',
        onPressed: () {},
      ),
    );
  }

画面内のコンテンツは bodyに実装します。直接、UIパーツを置くと、1つしか配置できないため、後ほど説明する Columnなどを使用して、複数配置できるようにすることがほとんどです。

Container

様々な用途で使用できる、便利なWidgetです。色付きのボックス、サークルを作成したり、それを回転したりすることができ、柔軟にUIを表現することができます。

Container(
  child: Text('Hello!'),
  color: Colors.green,
  padding: EdgeInsets.all(20), // paddingやmarginを使用したレイアウトも可能
  margin: EdgeInsets.all(20),
),

円形にするには、このようにします。

Container(
  child: Text('Hello!'),
  decoration: BoxDecoration(
    shape: BoxShape.circle,
    color: Colors.green
  ),
  padding: EdgeInsets.all(20),
  margin: EdgeInsets.all(20),
),

実行結果は次のようになります。

BoxDecorationクラスを使用して、円形を指定します。このとき、 Containercolor引数は渡さず、 BoxDecorationcolorを渡す必要があるため、注意してください。

Center

子Widgetをセンタリングして表示したいときに使用します。

Center(
    child: Text('こんにちは'),
)

childColumnRowといった、複数のWidgetを表示できるWidgetを渡すことで、複数パーツをセンタリングすることも可能です。

Column

UIパーツを縦に並べたいときに使用します。使い方はシンプルです。

Column(
  children: [
    Text('こんにちは'),
    Text('こんばんは'),
  ],
),

Columnでは、子Widgetを複数持つことができるため、引数名が childrenとなり、Widget配列を渡します。

Row

UIパーツを横に並べたいときに使用します。

Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 均等にスペースを取る設定
  children: [
    RaisedButton(
      child: Text('YES'),
      onPressed: () {},
    ),
    RaisedButton(
        child: Text('NO'),
      onPressed: () {},
    ),
  ],
);

mainAxisAlignment: MainAxisAlignment.spaceEvenlyとすることで、均等にスペースを取るようにしています。

UIパーツ

Text

テキストを表示するために使用します。

Text(
  'Hello Flutter',
  style: TextStyle(
    fontSize: 40,
    fontWeight: FontWeight.bold,
    color: Colors.green
  ),
),

ほとんどは、第一引数のテキスト指定のみで済みますが、フォントスタイルを設定したい場合は、 style引数に TextStyleを渡します。
TextStyleでは、フォントサイズ、太字・イタリック、カラーなどを設定できます。

Image

画像を表示するWidgetです。
まずは、アプリ埋め込みのアセットを表示する方法です。アセットを表示するには、画像を配置した後、 pubspec.yamlで登録する必要があります。

flutter:
  # To add assets to your application, add an assets section, like this:
  # ↓コメントアウトし、画像のパスを記載
  assets:
   - images/italy.jpg

登録したアセットを表示するには、以下にようにします。

Image.asset('images/italy.jpg')

引数に、 pubspec.yamlで記載した画像のパスを渡します。

また、URLを指定して、画像をダウンロードして読み込むこともできます。

Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg'),

Flutterサンプルでお馴染みのフクロウが表示されます。

RaisedButton

立体的なボタンを表示します。テキストボタンの他に、 icon()ファクトリメソッドを使用することで、アイコン付きのボタンを表示することができます。

Column(
  children: [
    RaisedButton(
      child: Text('ボタン'),
      color: Colors.blue,
      onPressed: () => {
        // ここにボタンが押された時の処理を記述
      },

    ),
    RaisedButton.icon( // このファクトリメソッドを使用すると、アイコン付きのボタンを表示できます。
        onPressed: () => {
          // ここにボタンが押された時の処理を記述
        },
        color: Colors.orange,
        icon: Icon(Icons.home),
        label: Text('ホーム')
        )
  ],
)

ボタンが押された時の処理は onPressedに、クロージャとして記載します。以下のように、別メソッドを作成して、呼び出すことも可能です。

class HelloPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: Text('SecondPage')
        ),
        body: Column(
          children: [
            RaisedButton(
              child: Text('ボタン'),
              color: Colors.blue,
              onPressed: _buttonTapped, // ボタンが押された時に呼び出すメソッドを指定
            ),
        ),
    );
  }

  void _buttonTapped() {
    // ここにボタンが押された時の処理を記述
  }

なお、 StatelessWIdgetでは、 Widget build(BuildContext context)以外でBuildContextを取得することはできないため、ボタンタップ時にBuildContextが必要な場合(画面遷移など)は、クロージャで書くようにしてください。

TextField

TextFieldはテキスト入力UIを提供するWidgetです。

  Widget build(BuildContext context) {
    return Scaffold(appBar: AppBar(
      title: Text('Hello World Page'),
    ),
      body: TextField(
        decoration: InputDecoration(
          labelText: '名前を入力してください' // プレースホルダーの設定
        ),
        onSubmitted: (String value) => {
          print(value) // エンターキーや、テキストフィールドのフォーカスが外れたときに呼ばれる
        },
      ),
    );
  }

onSubmittedを実装し、入力した値を受け取ります。
また、 decoration引数に InputDecorationクラスを設定することで、TextFieldの見た目を設定できます。 このサンプルでは、labelTextではプレースホルダーを設定します。その他にも、 border: OutlineInputBorder()とすることで、枠付きの見た目にすることも可能です。

AppBar

AppBarは、画面上部に表示されるヘッダーです。

  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('SecondPage'),
          actions: <Widget>[
            IconButton(
              icon: Icon(Icons.settings),
              onPressed: () =>
              {
                 // ここにアクションボタンをタップした時の処理を記述]
              },
            )
          ],
        ),
    );
  }

Scaffoldの apppBar引数に渡します。 actionsIconButtonを渡すと 、AppBar右側にボタンを追加することができます。

BottomNavigationBar

画面下部にタブのようなUIを表示することができます。

import 'package:basic_sample_app/main.dart';
import 'package:flutter/material.dart';

class TabSamplePage extends StatefulWidget {
  @override
  _TabSamplePageState createState() => _TabSamplePageState();
}

class _TabSamplePageState extends State {
  // 現在選択されているBottomNavigationのタブを保持する
  int _currentIndex = 0;

  // BottomNavigationで表示する画面のインスタンス
  final List<Widget> _children = [
    MyHomePage(title: 'Flutter App'),
    HelloWorldPage()
  ];


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _children[_currentIndex],
      bottomNavigationBar: BottomNavigationBar( // BottomNavigationの表示内容の設定
        currentIndex: _currentIndex,
        items: [
          new BottomNavigationBarItem(
              icon: new Icon(Icons.home),
              title: Text("ホーム")
          ),
          new BottomNavigationBarItem(
              icon: new Icon(Icons.message),
              title: Text("メッセージ")
          ),
        ],
        onTap: _onBottomNavigationBarTap

      ),
    );
  }

  void _onBottomNavigationBarTap(int index) {
    // indexをsetStateで更新し、タブを切り替える
    setState(() {
      _currentIndex = index;
    });
  }

}

BottomNavigationを実装する際は、画面を切り替える処理は自分で実装する必要がある点に気をつけてください。今回の場合、画面のインスタンスを _children配列に保持し、Scaffoldで body: _children[_currentIndex],と渡すようにしています。
そして、BottomNavigationBarがタップされたときに呼ばれる _onBottomNavigationBarTap(int index)で、 _currentIndexを変更することで、画面を更新し、タブが切り替わったように見せています。そのため、BottomNavigationを使用する画面では、StatefulWidgetとなります。

自作Widgetの作り方

同じレイアウトを使いまわしたいときに、自作Widgetを作成するには、StatelessWiidgetもしくはStatefulWidgetを継承したクラスを作成します。

class MyCustomButton extends StatelessWidget {
  // コンストラクタ
  MyCustomButton({Key key, this.onPressed, this.child}) : super(key: key);

  VoidCallback onPressed;
  Widget child;

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: onPressed,
      color: Colors.orange,
      child: child
    );
  }
}

class HelloWorldPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
      return Scaffold(appBar: AppBar(
        title: Text('Hello World Page'),
      ),
      body: MyCustomButton(
        child: Text('カスタムボタン'),
        onPressed: () {
           print('Custom Button!!')
        },
      ),
    );
  }
}

onPressedとchildは外部から設定できるよう、プロパティとコンストラクタを実装しています。そのほかは、これまで紹介したWidgetと同じように、 Widget build(BuildContext context)でWidgetを返却するようにします。

おまけ(PlantUML)

冒頭で示したWidgetツリー図は、PlantUMLで作成しました。記事画像用なので特にコードの整理はしていませんが、参考になればと思い、PlantUMLのコードを公開します。
ちなみに、冒頭で skinparam shadowing falseとすることで、デフォルトで描画される影を消去しています。

@startuml
skinparam shadowing false

rectangle MaterialApp
rectangle MyHomePage
rectangle Scaffold
rectangle AppBar
rectangle Body
rectangle Center
rectangle Column
rectangle "Text('You have pushed the button this many times:')" as TextDescription
rectangle "Text(カウンター)" as TextCounter
rectangle "Title" as TitleWidget
rectangle FloatingActionButton
rectangle "Text('Flutter Demo Home Page')" as TitleText

MaterialApp -down-> MyHomePage: home
MyHomePage -down-> Scaffold
Scaffold -down-> AppBar
AppBar -down-> TitleWidget
TitleWidget -down-> TitleText
Scaffold -down-> Body
Body -down-> Center
Center -down-> Column
Column -down-> TextDescription
Column -down-> TextCounter
Scaffold -down-> FloatingActionButton

note right of MaterialApp : MyAppクラス
note right of MyHomePage : _MyHomePageStateクラス

@enduml

 

さいごに

Flutterは様々なWigetが用意されているされており、Wigetを組み合わせることでスピーディにクロスプラットフォームアプリが実現出来ることが分かりました。今後活用していきたいも思います!

おすすめ書籍

カイザー

シェア
執筆者:
カイザー

最近の投稿

Goの抽象構文木でコードを解析する

はじめに Goでアプリケーショ…

4日 前

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

1か月 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

1か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前