Loading... Search articles

Search for articles

Sorry, but we couldn't find any matches...

But perhaps we can interest you in one of our more popular articles?
Flutterウェブ-レスポンシブデザインをはじめよう

Flutterウェブ-レスポンシブデザインをはじめよう

Jan 20, 2022

Flutterウェブはかなり以前から存在していましたが、Flutter 2の導入により、ついに安定版に到達しました。いくつかの改良を受け、Flutterウェブのプラグインサポートも急速に向上しています。まだお試しになられていない方は、ぜひこの機会にお試しください。

本記事では、Flutterウェブプロジェクトを始めて、レスポンシブ対応にする方法をご紹介いたします。

本記事はSouvik Biswasが執筆し、2021年3月に更新されたものです。

Use M1 Mac mini VMs by default with Codemagic🚀 Build faster

レスポンシブウェブデザインとは?

簡単に言えば、レスポンシブウェブデザイン(RWD)とは、開発チームが作成するものすべてが、ユーザー端末に関係なく、同じように美しく仕上がることを意味します。

これは、ウェブデザインとウェブページのユーザーインターフェイスが、ユーザー端末の画面サイズに反応していることを意味します。レスポンシブなウェブサイトとは、画面サイズに応じて簡単かつ流れるように調整できる柔軟性のあるウェブサイトのことです。

過去10年間におけるモバイル端末の使用と普及の増加により、ウェブ上でプレゼンテーションを設計・構築する人々にとって、ウェブデザインを閲覧している人が皆、あなたが提供できる限りの最高のプレゼンテーションを見ることができるようにするために、レスポンシブユーザーインターフェイスに特別な注意を払うことが非常に重要となっています。

また、開発者は多くのフレームワークでレスポンシブウェブデザインをうまく作成できますが、*Flutterは、取り組むプロジェクトのあらゆるタイプに対応するクロスプラットフォームサポートを提供している点で特別です。

レスポンシブデザインが重要な理由

レスポンシブウェブデザインはもはやトレンドではなく、必需品です。RWDにより、さらに簡単に顧客にアプローチできるようになります。一貫したユーザーエクスペリエンスを実現することで、リードジェネレーション(見込み顧客を獲得するための活動)、セールス、コンバージョンの測定を容易にします。また、解析やレポートの信頼性も格段に向上します。

また、Googleは、特にコンテンツマーケティングにおいて、レスポンシブなウェブサイトを強く支持することが証明されています。ウェブサイトをレスポンシブにすることで、ほとんどの検索エンジンにおいて視認性が向上し、ターゲットにしている特定のキーワードでの順位が向上します。

レスポンシブデザインの現状は、Flutterの登場が大きく影響しています。Flutterは、クロスプラットフォームへの対応を継続的に拡大し、ウェブだけでなくLinux、Windows、macOSにも対応するようになり、ソフトウェアとウェブページの両方を開発する手法に変化をもたらしています。

技術の進歩に伴い、利用可能な端末数、存在する画面サイズや種類の数は常に増加しています。だからこそ、どの端末でも美しく見えるデザインを作成できることが非常に重要なのです。

プラットフォームを問わず、Flutterは、レスポンシブなウェブページを簡単かつ効果的に設計・実装するために必要な機能とツールを備えています。

Flutterウェブアーキテクチャ

プロジェクトを始める前に、Flutterウェブのアーキテクチャを見てみましょう。

モバイルアプリで使用されるFlutterのアーキテクチャに慣れていない方のために、簡単に概要をご説明いたします:

モバイルアプリ向けFlutterのアーキテクチャは、主に以下の3つのレイヤーで構成されています:

  1. フレームワーク: このレイヤーは純粋にDartで書かれており、Flutterのコアビルディングブロックで構成されています。

  2. エンジン: このレイヤーはフレームワークの下にあり、主にC/C++で書かれています。これは、GoogleのグラフィックライブラリSkiaを使用した低レベルのレンダリングサポートを提供します。

  3. エンベッダー: このレイヤーは基本的に、プラットフォーム固有のすべての依存関係から構成されています。

では、Flutterウェブのアーキテクチャと、これとの違いを見ていきましょう。

flutter.dev/web

Flutterウェブアーキテクチャは、わずか2つのレイヤーで構成されています:

  1. **フレームワーク:**は、純粋なDartのコードで構成されています。

  2. **ブラウザ:**は、C++とJavaScriptのコードで構成されています。

ご覧の通り、一番上のレイヤー(フレームワーク)には、通常のFlutterアーキテクチャとほぼ同じ種類のコンポーネントが含まれています。

主な違いは、ブラウザレイヤーです。Flutterウェブでは、モバイルアーキテクチャに存在する下の2つの別々のレイヤーが、たった1つのレイヤーに置き換えられます。Skia Graphics Engine(ブラウザで対応していない)の代わりに、JavaScriptエンジンを使用しています。Flutterウェブでは、モバイルアプリに使用されるARMマシンコードの代わりに、DartをJavaScriptにコンパイルしています。DOM、Canvas、CSSを組み合わせて、ブラウザでFlutterコンポーネントをレンダリングします。

はじめに

このプロジェクトではFlutter 2.0を使用しており、デフォルトでウェブ上でアプリを実行するように構成されています。

Flutterのウェブアプリケーションの実行とデバッグには、Chromeが必要です。

以下のコマンドでFlutterプロジェクトを新規に作成します:

flutter create explore

ここで、exploreは、これから作成するFlutterのウェブアプリ名です。

お好みのIDEでプロジェクトを開いてください。VS Codeで開くには、このコマンドを使用します:

code explore

ここで、ディレクトリ構造を見てみると、webというフォルダがあることにお気づきになると思います。

これは、プロジェクトがブラウザで動作するように適切に設定されていることを意味します。また、Flutterアプリを実行するための端末の一つとして、Chromeが利用できることを確認できます。

また、開始プロジェクトと同じカウンターアプリを入手できます。VS Codeから実行するには、F5を使用するか、ターミナルでこのコマンドを使用します:

flutter run -d chrome

現状はこのような感じです:

スタータープロジェクトのセットアップが完了したので、目的のウェブアプリの構築を開始しましょう。

ウェブインターフェイスデザイン

今回作成するウェブインターフェイスは、DribbbleにあるTubikの例にヒントを得ています。

lib > main.dart に移動し、すべてのコードを次のように置き換えます:

// main.dart

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Explore',
      theme: ThemeData(
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    var screenSize = MediaQuery.of(context).size;

    return Scaffold();
  }
}

ここで、アプリのUIを含むHomePageウィジェットを定義する必要があります。なぜステートフルウィジェットとして定義したかは、すぐにお分かりになると思います。

画面サイズを取得するためにMediaQueryを使っていることにお気づきでしょうか。これは、画面の大きさに合わせてウィジェットのサイズを調整するためです。これによりウィジェットはレスポンシブになり、ブラウザウィンドウのサイズ変更時にオーバーフローする問題をほとんど防ぐことができます。

では、次のようなトップバーを追加してみましょう:

そのためには、以下のコードが必要です:

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    var screenSize = MediaQuery.of(context).size;

    return Scaffold(
      appBar: PreferredSize(
        preferredSize: Size(screenSize.width, 1000),
        child: Container(
          color: Colors.blue,
          child: Padding(
            padding: EdgeInsets.all(20),
            child: Row(
              children: [
                Text('EXPLORE'),
                Expanded(
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      InkWell(
                        onTap: () {},
                        child: Text(
                          'Discover',
                          style: TextStyle(color: Colors.black),
                        ),
                      ),
                      SizedBox(width: screenSize.width / 20),
                      InkWell(
                        onTap: () {},
                        child: Text(
                          'Contact Us',
                          style: TextStyle(color: Colors.black),
                        ),
                      ),
                    ],
                  ),
                ),
                InkWell(
                  onTap: () {},
                  child: Text(
                    'Sign Up',
                    style: TextStyle(color: Colors.black),
                  ),
                ),
                SizedBox(
                  width: screenSize.width / 50,
                ),
                InkWell(
                  onTap: () {},
                  child: Text(
                    'Login',
                    style: TextStyle(color: Colors.black),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
      body: Container(),
    );
  }
}

「何かがおかしい、ウェブUIらしくない」とすぐに気がつくはずです。ホバーエフェクトはどこでしょうか? 

そうです、Flutterコンポーネントにはデフォルトではホバーエフェクトがありません。ですが、一番簡単に実現できる方法をご紹介いたします。念のためですが、ホバーエフェクトを追加した後は、このようになります:

InkWell() ウィジェットにはonHoverというプロパティがあります。これを使って、マウスポインタがコンポーネントの境界に入ったときや離れたときに追跡できます。

このエフェクトを得るには、以下の手順に従います:

-ホバーを追跡するために使用されるブーリアンのリストを追加します(ブーリアンの数は、ホバーエフェクトを適用したいコンポーネントの数です)。

List _isHovering = [false, false, false, false];

-コンポーネントに対応するブーリアン値を更新し、テキストの色を設定します(または、そのブーリアン値に応じてホバー時に表示したいその他の変更も行います)。

InkWell(
  onHover: (value) {
    setState(() {
      _isHovering[0] = value;
    });
  },
  child: Column(
    mainAxisSize: MainAxisSize.min,
    children: [
      Text(
        'Discover',
        style: TextStyle(
          color: _isHovering[0]
              ? Colors.blue.shade200
              : Colors.white,
        ),
      ),
      SizedBox(height: 5),
      // For showing an underline on hover
      Visibility(
        maintainAnimation: true,
        maintainState: true,
        maintainSize: true,
        visible: _isHovering[0],
        child: Container(
          height: 2,
          width: 20,
          color: Colors.white,
        ),
      )
    ],
  ),
),

画像をScaffoldbodyの中に追加すると、トップバーの下から始まるようになります。ですが、デザインによれば、画像はトップバーの下に流れるはずです。

このデザインの実現は、実に簡単です。extendBodyBehindAppBarというScaffoldの一つのプロパティを定義し、それをtrueに設定するだけです。

Scaffold(
  extendBodyBehindAppBar: true,
  appBar: PreferredSize(
    // ...
  ),

  // ...
);

また、画像の上部、中央下の位置にクイックアクセスバーを追加していることにお気づきでしょうか。

このようなUIを得るには、Scaffoldbody内でStackウィジェットを使用します。

Scaffold(
  // ...
  body: Stack(
    children: [
      Container( // image below the top bar
        child: SizedBox(
          height: screenSize.height * 0.45,
          width: screenSize.width,
          child: Image.asset(
            'assets/images/cover.jpg',
            fit: BoxFit.cover,
          ),
        ),
      ),
      Center(
        heightFactor: 1,
        child: Padding(
          padding: EdgeInsets.only(
            top: screenSize.height * 0.40,
            left: screenSize.width / 5,
            right: screenSize.width / 5,
          ),
          child: Card( // floating quick access bar
              // ...
          ),
        ),
      )
    ],
  ),
);

UIの構造をご説明するために必要なコードを入れているだけです。この部分のUIコード一式は、こちらで公開しています。本記事の最後に、プロジェクト全体へのリンクがあります。

次に、ウェブページにUI要素をもういくつか追加していきます。まず、SingleChildScrollViewウィジェットでScaffoldbody全体を ラップして、スクロールできるようにします。

このウェブサイトはシンプルにしています。では、あと3つだけセクションを追加しましょう:

  1. 注目
  2. 目的地
  3. 下部の情報

注目セクション

このセクションには、見出しと、3つの画像とそのラベルを含むが含まれます。このように表示されます:

見出しとその説明のUIコードは次のようになります:

Row(
  mainAxisSize: MainAxisSize.max,
  mainAxisAlignment: MainAxisAlignment.start,
  children: [
    Text(
      'Featured',
      style: GoogleFonts.montserrat(
        fontSize: 40,
        fontWeight: FontWeight.w500,
      ),
    ),
    Expanded(
      child: Text(
        'Unique wildlife tours & destinations',
        textAlign: TextAlign.end,
      ),
    ),
  ],
),

各画像(それに対応するラベルを含む)のコードは以下の通りです:

Column(
  children: [
    SizedBox(
      height: screenSize.width / 6,
      width: screenSize.width / 3.8,
      child: ClipRRect(
        borderRadius: BorderRadius.circular(5.0),
        child: Image.asset(
          'assets/images/trekking.jpg',
          fit: BoxFit.cover,
        ),
      ),
    ),
    Padding(
      padding: EdgeInsets.only(
        top: screenSize.height / 70,
      ),
      child: Text(
        'Trekking',
        style: GoogleFonts.montserrat(
          fontSize: 16,
          fontWeight: FontWeight.w500,
        ),
      ),
    ),
  ],
),

ここで、ブラウザのウィンドウサイズを変更してみると、すでにかなりレスポンスが良くなっていることがお分かりになると思います。

目的地セクション

このセクションでは、“目的地の多様性(Destinations diversity)“という潜在的な目的地のフローティングセレクタを持つ画像カルーセル(スワイプやクリックで複数の画像をスライド表示させるデザイン)を追加する予定です。このように表示されます:

カルーセルには、carousel_sliderというFlutterのパッケージを使用できます。

以下の手順でカルーセルをビルドします:

-画像とそのラベルのリストを定義します。

final List<String> images = [
  'assets/images/asia.jpg',
  'assets/images/africa.jpg',
  'assets/images/europe.jpg',
  'assets/images/south_america.jpg',
  'assets/images/australia.jpg',
  'assets/images/antarctica.jpg',
];

final List<String> places = [
  'ASIA',
  'AFRICA',
  'EUROPE',
  'SOUTH AMERICA',
  'AUSTRALIA',
  'ANTARCTICA',
];
  • Generate a list of Widgets to show in the carousel:

    List<Widget> generateImageTiles(screenSize) {
      return images
          .map(
            (element) => ClipRRect(
              borderRadius: BorderRadius.circular(8.0),
              child: Image.asset(
                element,
                fit: BoxFit.cover,
              ),
            ),
          )
          .toList();
    }
    

buildメソッドの内部で、ウィジェットのリストを保存し、それぞれのラベルを付けてカルーセル内に表示します:

@override
Widget build(BuildContext context) {
  var screenSize = MediaQuery.of(context).size;
  var imageSliders = generateImageTiles(screenSize);

  return Stack(
    children: [
      CarouselSlider(
        items: imageSliders,
        options: CarouselOptions(
            enlargeCenterPage: true,
            aspectRatio: 18 / 8,
            autoPlay: true,
            onPageChanged: (index, reason) {
              setState(() {
                _current = index;
              });
            }),
        carouselController: _controller,
      ),
      AspectRatio(
        aspectRatio: 18 / 8,
        child: Center(
          child: Text(
            places[_current],
            style: GoogleFonts.electrolize(
              letterSpacing: 8,
              fontSize: screenSize.width / 25,
              color: Colors.white,
            ),
          ),
        ),
      ),
    ],
  );
}

アプリを起動すると、カルーセルは次のようになります:

フローティングセレクタを追加するには、以下の手順で行います:

-2つのブーリアンリストを追加:

List _isHovering = [false, false, false, false, false, false, false];
List _isSelected = [true, false, false, false, false, false, false];

CarouselOptions ウィジェットの onPageChanged プロパティを変更します。

CarouselOptions(
  // ...
  onPageChanged: (index, reason) {
    setState(() {
      _current = index;

      // add the following
      for (int i = 0; i < imageSliders.length; i++) {
        if (i == index) {
          _isSelected[i] = true;
        } else {
          _isSelected[i] = false;
        }
      }

    });
  },
)

-テキストを含むCard内にウィジェットの行を表示し、選択されているオプションを強調するためにアンダーラインを表示します。ハイライターは次のように作成できます:

Visibility(
  maintainSize: true,
  maintainAnimation: true,
  maintainState: true,
  visible: _isSelected[i],
  child: Container(
    height: 5,
    decoration: BoxDecoration(
      color: Colors.blueGrey,
      borderRadius: BorderRadius.all(
        Radius.circular(10),
      ),
    ),
    width: screenSize.width / 10,
  ),
)

フローティングセレクタはこのように表示されます:

下部情報セクション

これは、単に次のようなシンプルな情報セクションになります:

この部分のUIコードはこちらで公開しています。

レスポンスの向上

このウェブアプリはかなりレスポンスが良いのですが、それでもオーバーフローが発生することがあります。また、モバイル端末の小さな画面では、UIデザインが不便です。これを解決するために、レスポンシブレイアウトを使用して、端末の画面サイズに合わせてウィジェットを構築し、サイズを変更することにします。

smallScreenmediumScreenlargeScreenブレークポイントをいくつか設定し、それぞれのブレークポイントに到達したときに新しいレイアウトでウィジェットを再構築します。これは以下のコードで実現できます:

import 'package:flutter/material.dart';

class ResponsiveWidget extends StatelessWidget {
  final Widget largeScreen;
  final Widget? mediumScreen;
  final Widget? smallScreen;

  const ResponsiveWidget({
    Key? key,
    required this.largeScreen,
    this.mediumScreen,
    this.smallScreen,
  }) : super(key: key);

  static bool isSmallScreen(BuildContext context) {
    return MediaQuery.of(context).size.width < 800;
  }

  static bool isLargeScreen(BuildContext context) {
    return MediaQuery.of(context).size.width > 1200;
  }

  static bool isMediumScreen(BuildContext context) {
    return MediaQuery.of(context).size.width >= 800 &&
        MediaQuery.of(context).size.width <= 1200;
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth > 1200) {
          return largeScreen;
        } else if (constraints.maxWidth <= 1200 &&
            constraints.maxWidth >= 800) {
          return mediumScreen ?? largeScreen;
        } else {
          return smallScreen ?? largeScreen;
        }
      },
    );
  }
}

これで、画面サイズごとに異なるウィジェットレイアウトを定義できるようになりました。小さい画面では、トップバーのオプションでdrawerを表示することが望ましいです。このために、別のAppBarウィジェットを使用します。Scaffoldのdrawerプロパティで定義すると、ハンバーガーアイコンがデフォルトでアプリバーに表示されるようになります。

Scaffold(
  appBar: ResponsiveWidget.isSmallScreen(context)
      ? AppBar( // for smaller screen sizes
          backgroundColor: Colors.transparent,
          elevation: 0,
          title: Text(
            'EXPLORE',
            style: TextStyle(
              color: Colors.blueGrey.shade100,
              fontSize: 20,
              fontFamily: 'Montserrat',
              fontWeight: FontWeight.w400,
              letterSpacing: 3,
            ),
          ),
        )
      : PreferredSize( // for larger & medium screen sizes
          preferredSize: Size(screenSize.width, 1000),
          child: TopBarContents(_opacity),
        ),
  drawer: ExploreDrawer(), // The drawer widget

  // ...

);

drawerのUIは以下のようになります:

drawerのUIコードはこちらです。

このようにして、さまざまな画面サイズに対応したレイアウトを定義できます。

最終版は、デスクトップとモバイルのブラウザでこのように表示されます:

結論

本記事がFlutterウェブの開始でお役に立てますと幸いです。次回のFlutterウェブシリーズパートでは、アニメーションやテーマを追加してこのUIを改善してみます。

本記事は、Flutterウェブを始めるのにお役に立ちましたでしょうか? 🤔 ご感想をこちらからお伝えください。

役に立つリンクと参考資料

公式Flutterウェブドキュメント

-このプロジェクトは、GitHubでこちらで公開されています。

-ウェブアプリをこちらでお試しください。

Flutterウェブに関するその他の記事

Flutterウェブ:Firebase認証とGoogleサインインFlutterウェブ:アニメーションとダイナミックなテーマ


Souvik Biswasは、情熱的なモバイルアプリ開発者です(AndroidとFlutter)。彼はこれまでの歩みの中で、数多くのモバイルアプリを手がけてまいりました。GitHubでのオープンソース貢献が大好きです。彼は現在、Indian Institute of Information Technology Kalyaniでコンピューターサイエンスとエンジニアリングの技術学士号の取得を目指しています。また、Medium - Flutter CommunityでFlutterの記事も執筆しております。

Latest articles

Show more posts