Flutter

【Flutter】Android系統の「Material」とiOS系統の「Cupertino」をそれぞれ比較してみる

Flutter

こんにちは。

今回はFlutterアプリケーションでAndroidのデザイン系統である「Material」とiOSのデザイン系統である「Cupertino」をサンプルアプリケーションを用いて、どのような違いがあるか見てみようと思います。

Widgetの種類

FlutterをWidgetをデザインで区別する場合、Androidのデザイン系統である「Material」とiOSのデザイン系統である「Cupertino」に2分することができます。

しかし、「Material」系統のWidgetを使ったからiOSでは動かない、「Cupertino」系統のWidgetを使ったからAndroidでは動かないといったことはなく、どちらのデバイスでも正常に動作します。(マルチデバイスに対応するFlutterならではですね。)

Material系統

まずはMaterial系統でサンプルアプリケーションを作った場合を見ていきます。

サンプルアプリケーションは以下の記事で作成したカウンターアプリになります。

Flutter
【2024年版】MacでのFlutterの環境構築手順(VSCodeでの開発想定)今回はMacでのFlutterの環境構築手順についてまとめています。今後Flutter開発していくエディタはVSCodeを想定しています。...

全体コード

// 「Material」はAndroid系統のWidget
import 'package:flutter/material.dart';

// void関数は「戻り値がない」ことを示す。
// もし関数が値を返す場合には、その値の型を指定する
// 整数を返す場合はint、文字列を返す場合はStringなど
// main関数はアプリケーションのエントリーポイント
// この関数が呼び出されると、アプリケーションが起動する
// runApp関数は、引数に渡されたWidgetを画面に表示する
// この場合、MyAppクラスのインスタンスを引数に渡している
// constを使ってMyAppインスタンスを生成している
// MyAppが定数インスタンスとして生成されるため、再レンダリングが不要な場合に効率的に処理される
void main() {
  runApp(const MyApp());
}

// MyAppクラスはStatelessWidgetクラスを継承している
// 「状態を持たない」Widgetを作成するために使用される
// ユーザーの操作によって変化するデータを持たない場合に使用される
class MyApp extends StatelessWidget {
  // constを使ってMyAppインスタンスを生成している
  // {super.key}は、親クラス(StatelessWidget)のkeyパラメータを受け取る
  // superは「親クラス」を指し、keyはStatelessWidgetが持つパラメータの一つ
  // keyはWidgetの一意性を管理するためのIDとして使われ、ツリー構造のWidgetを識別する
  // const MyApp({super.key});は、定数コンストラクタであるMyAppクラスのコンストラクタで、
  // 親クラスのkeyを直接受け取り、MyAppインスタンスを一意に管理するための役割を持つ
  const MyApp({super.key});

  // StatelessWidgetクラスから継承されたbuildメソッドをオーバーライドしている
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo', // アプリのタイトル
      theme: ThemeData(
        // アプリのカラースキーム
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blueAccent),
        // Material Design3を有効化
        useMaterial3: true,
      ),
      // homeプロパティでアプリのホーム画面を設定
      // MyHomePageクラスを指定し、タイトルとして"Flutter Demo Home Page"を渡している
      // MyHomePageはこのアプリが起動した際に最初に表示される画面
      home: const MyHomePage(title: 'Flutterヘッダー'),
    );
  }
}

// MyHomePageクラスはStatefulWidgetクラスを継承している
// 「状態を持つ」Widgetを作成するために使用される
// ユーザーの操作によって変化するデータを持つ場合に使用される
class MyHomePage extends StatefulWidget {
  // {super.key, required this.title}は、keyとtitleの2つの引数を受け取る
  // super.keyは、親クラスStatefulWidgetのkeyに値を渡すために使われ、
  // FlutterのWidgetツリー内でWidgetを一意に識別するために利用される
  // required this.titleは、必須の引数titleを設定する
  const MyHomePage({super.key, required this.title});

  // titleプロパティをfinalで宣言する
  // このプロパティは変更できない固定の値で、コンストラクタで一度設定されるとその後変更されない
  final String title;

  // State<MyHomePage>を作成するメソッドであるcreateStateをオーバーライドする
  @override
  // createStateメソッドはStatefulWidgetに必須のメソッド
  // 対応する状態クラスである_MyHomePageStateクラス(Stateオブジェクト)を返す
  // アンダーバーをつけることでそのメソッドはプライベートになる
  State<MyHomePage> createState() => _MyHomePageState();
}

// State<MyHomePage>を継承することで、このクラスはMyHomePage Widgetと連動する
// StatefulWidgetであるMyHomePageの状態を保持し、必要に応じて再描画を行う
class _MyHomePageState extends State<MyHomePage> {
  // _counterはint型の整数で、初期値は0に設定されている
  // アンダーバーが付いているのでプライベート変数として定義されている
  int _counter = 0;

  // カウンターを増加させるメソッド
  // アンダーバーが付いているのでプライベート変数として定義されている
  void _incrementCounter() {
    // setStateメソッドは状態が変化したことを通知し、Widgetを再描画するために使用する
    // setStateを呼び出さずに_counterを更新した場合、変更が表示されない
    setState(() {
      // _counterを1ずつ増加させる
      // アンダーバーが付いているのでプライベート変数として定義されている
      _counter++;
    });
  }

  // カウンターを減少させるメソッド
  // アンダーバーが付いているのでプライベート変数として定義されている
  void _decrementCounter() {
    // setStateメソッドは状態が変化したことを通知し、Widgetを再描画するために使用する
    // setStateを呼び出さずに_counterを更新した場合、変更が表示されない
    setState(() {
      // _counterを1ずつ減少させる
      // アンダーバーが付いているのでプライベート変数として定義されている
      _counter--;
    });
  }

  @override
  Widget build(BuildContext context) {
    // Scaffoldは、Flutterアプリの基本的なレイアウト構造を提供するウィジェット
    // appBar/body/floatingActionButtonプロパティを使って、各部分を簡単に設定できる
    return Scaffold(
      // AppBarはアプリ画面の上部に表示されるバー
      // ここではアプリのタイトルや背景色が設定される
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        // title: Text(widget.title),
        title: Align(
          alignment: Alignment.centerLeft,
          child: Text(widget.title),
        ),
        actions: <Widget>[
          IconButton(
            icon: const Icon(Icons.add),
            onPressed: _incrementCounter,
          ),
        ],
      ),
      // bodyは画面の中央部分のレイアウトを構成
      // Center Widgetは、指定した子Widgetを中央に配置するためのWidget
      body: Center(
        // Column Widgetは、複数の子Widgetを縦方向に並べるためのレイアウトWidget
        child: Column(
          // MainAxisAlignment.centerにより、縦方向の中央に配置する
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // 初期表示のテキスト
            const Text(
              'Hello Flutter!',
              style: TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),
            const Text(
              'カウンターが1ずつ増加します',
            ),
            Text(
              // 文字列内に変数を埋め込むため$を使用する
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            onPressed: _decrementCounter,
            tooltip: 'Decrement',
            child: const Icon(Icons.remove),
          ),
          const SizedBox(width: 10),
          FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        ],
      ),
    );
  }
}

画面イメージ

動作チェック

Cupertino系統

続いてCupertino系統の場合はどうなるかを見ていきます。

全体コード

// 「Cupertino」はiOS系統のWidget
import 'package:flutter/cupertino.dart';

// void関数は「戻り値がない」ことを示す。
// もし関数が値を返す場合には、その値の型を指定する
// 整数を返す場合はint、文字列を返す場合はStringなど
// main関数はアプリケーションのエントリーポイント
// この関数が呼び出されると、アプリケーションが起動する
// runApp関数は、引数に渡されたWidgetを画面に表示する
// この場合、MyAppクラスのインスタンスを引数に渡している
// constを使ってMyAppインスタンスを生成している
// MyAppが定数インスタンスとして生成されるため、再レンダリングが不要な場合に効率的に処理される
void main() {
  runApp(const MyApp());
}

// MyAppクラスはStatelessWidgetクラスを継承している
// 「状態を持たない」Widgetを作成するために使用される
// ユーザーの操作によって変化するデータを持たない場合に使用される
class MyApp extends StatelessWidget {
  // constを使ってMyAppインスタンスを生成している
  // {super.key}は、親クラス(StatelessWidget)のkeyパラメータを受け取る
  // superは「親クラス」を指し、keyはStatelessWidgetが持つパラメータの一つ
  // keyはWidgetの一意性を管理するためのIDとして使われ、ツリー構造のWidgetを識別する
  // const MyApp({super.key});は、定数コンストラクタであるMyAppクラスのコンストラクタで、
  // 親クラスのkeyを直接受け取り、MyAppインスタンスを一意に管理するための役割を持つ
  const MyApp({super.key});

  // StatelessWidgetクラスから継承されたbuildメソッドをオーバーライドしている
  @override
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo', // アプリのタイトル
      theme: CupertinoThemeData(
        // アプリのカラースキーム
        primaryColor: CupertinoColors.activeBlue,
      ),
      home: MyHomePage(title: 'Flutterヘッダー'),
    );
  }
}

// MyHomePageクラスはStatefulWidgetクラスを継承している
// 「状態を持つ」Widgetを作成するために使用される
// ユーザーの操作によって変化するデータを持つ場合に使用される
class MyHomePage extends StatefulWidget {
  // {super.key, required this.title}は、keyとtitleの2つの引数を受け取る
  // super.keyは、親クラスStatefulWidgetのkeyに値を渡すために使われ、
  // FlutterのWidgetツリー内でWidgetを一意に識別するために利用される
  // required this.titleは、必須の引数titleを設定する
  const MyHomePage({super.key, required this.title});

  // titleプロパティをfinalで宣言する
  // このプロパティは変更できない固定の値で、コンストラクタで一度設定されるとその後変更されない
  final String title;

  // State<MyHomePage>を作成するメソッドであるcreateStateをオーバーライドする
  @override
  // createStateメソッドはStatefulWidgetに必須のメソッド
  // 対応する状態クラスである_MyHomePageStateクラス(Stateオブジェクト)を返す
  // アンダーバーをつけることでそのメソッドはプライベートになる
  State<MyHomePage> createState() => _MyHomePageState();
}

// State<MyHomePage>を継承することで、このクラスはMyHomePage Widgetと連動する
// StatefulWidgetであるMyHomePageの状態を保持し、必要に応じて再描画を行う
class _MyHomePageState extends State<MyHomePage> {
  // _counterはint型の整数で、初期値は0に設定されている
  // アンダーバーが付いているのでプライベート変数として定義されている
  int _counter = 0;

  // カウンターを増加させるメソッド
  // アンダーバーが付いているのでプライベート変数として定義されている
  void _incrementCounter() {
    // setStateメソッドは状態が変化したことを通知し、Widgetを再描画するために使用する
    // setStateを呼び出さずに_counterを更新した場合、変更が表示されない
    setState(() {
      // _counterを1ずつ増加させる
      // アンダーバーが付いているのでプライベート変数として定義されている
      _counter++;
    });
  }

  // カウンターを減少させるメソッド
  // アンダーバーが付いているのでプライベート変数として定義されている
  void _decrementCounter() {
    // setStateメソッドは状態が変化したことを通知し、Widgetを再描画するために使用する
    // setStateを呼び出さずに_counterを更新した場合、変更が表示されない
    setState(() {
      // _counterを1ずつ減少させる
      // アンダーバーが付いているのでプライベート変数として定義されている
      _counter--;
    });
  }

  @override
  Widget build(BuildContext context) {
    // scaffoldの代わりに、iOSスタイルのCupertinoPageScaffoldを使用
    // navigationBar/childプロパティを使って、各部分を簡単に設定できる
    return CupertinoPageScaffold(
      // CupertinoNavigationBarはiOSスタイルのナビゲーションバー
      navigationBar: CupertinoNavigationBar(
        backgroundColor: CupertinoColors.activeBlue,
        // loadingプロパティは、ナビゲーションバーの左側に表示されるWidget
        leading: Align(
          alignment: Alignment.centerLeft, // 左寄せかつ上下中央
          child: Text(
            widget.title,
            style: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
          ),
        ),
        // trailingプロパティは、ナビゲーションバーの右側に表示されるWidget
        // loadingは左寄せ、trailingは右寄せ、middleは中央寄せ
        trailing: CupertinoButton(
          // paddingプロパティは、ボタンの内側の余白を設定
          // 余白をなくし、アイコンのみ表示されるように設定
          padding: EdgeInsets.zero,
          onPressed: _incrementCounter,
          child: const Icon(
            CupertinoIcons.add,
            color: CupertinoColors.white, // アイコンを白に設定
          ),
        ),
      ),
      // StackWidgetは、子Widgetを重ねて表示するためのWidget
      child: Stack(
        // childrenプロパティには、重ねて表示する子Widgetを設定
        children: [
          // CenterWidgetは、指定した子Widgetを中央に配置するためのWidget
          Center(
            // ColumnWidgetは、複数の子Widgetを縦方向に並べるためのレイアウトWidget
            child: Column(
              // mainAxisAlignmentプロパティは、縦方向の中央に配置する
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'Hello Flutter!',
                  style: TextStyle(
                    fontSize: 24,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const Text(
                  'カウンターが1ずつ増加します',
                ),
                Text(
                  // 文字列内に変数を埋め込むため$を使用する
                  '$_counter',
                  style: const TextStyle(
                    fontSize: 32,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
          ),
          // PositionedWidgetは、子Widgetを指定した位置に配置するためのWidget
          Positioned(
            // 画面右下から50px上、30px左に配置
            bottom: 50,
            right: 30,
            // RowWidgetは、複数の子Widgetを横方向に並べるためのレイアウトWidget
            child: Row(
              // mainAxisAlignmentプロパティは、横方向の右寄せに配置する
              mainAxisSize: MainAxisSize.min,
              children: [
                CupertinoButton(
                  onPressed: _decrementCounter,
                  padding: EdgeInsets.zero,
                  child: const Icon(CupertinoIcons.minus_circle, size: 60),
                ),
                const SizedBox(width: 16),
                CupertinoButton(
                  onPressed: _incrementCounter,
                  padding: EdgeInsets.zero,
                  child: const Icon(CupertinoIcons.add_circled, size: 60),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

画面イメージ

Cupertino系統

まとめ

Material系統
カウンターアプリ
Cupetino系統
Cupertino系統

参考

 

Widgetの豊富さで言うとMaterial系統の方が勝るため、Material系統のWidgetを使った開発が多いかと思います。自分もMaterial系統のWidgetを中心に使用しますが、部分的にCupertino系統のWidgetを使用することも可能なので、ここはよしなに使っていきたいと思います。