Skip to content

DNET_FlutterFirstStep2

nishi_74322014 edited this page Sep 11, 2026 · 1 revision

Flutterのファースト・ステップ2

概要

2026年、リブートはFlutterデスクトップ(Windows)で実施

  • この選択肢は、単純にAndroid/iOSの準備が難しいケースが多いため。
  • (コレは、一先ず、WebAPIを経由した簡単なCRUD処理の実装と、OAuth系認証・認可処理を実装できれば良いため)
  • Windowsでは、Android/iOSほど「カスタムURLスキームからの移行圧力」は強くなく、myapp:// 方式が今でも主流

詳細

システム要件

  • Windows 10/11(64bit)
  • ストレージ:10GB以上の空き
  • RAM:8GB以上推奨

インストール

Flutter SDK

  • flutter.dev から Flutter SDK をダウンロード

  • %USERPROFILE%\develop\flutter\ などのパスに解凍

  • 環境変数 PATH に %USERPROFILE%\develop\flutter\bin を追加

  • CMD / PowerShellで確認

    flutter --version
    dart --version
    

Visual Studio

  • Visual Studio のインストールで「C++によるデスクトップ開発」 ワークロードを選択
  • 「MSVC v143(または最新)」「Windows SDK」コンポーネントが含まれていることを確認

VS Code

  • VS Codeのインストール
  • 拡張機能 Flutter(Dart含む)をインストール

設定

CMD / PowerShellで

有効化

Windowsデスクトップ対応を有効化

flutter config --enable-windows-desktop

確認

依存関係の確認

flutter doctor

開発

CMD / PowerShellで

作成

flutter create my_app

編集

cd my_app

実行

flutter run -d windows

デバッグ

VS Code上から、

  • 以下から、デバイスを Windows (desktop) に切り替え。

    • 右下のステータスバー(No Device)
    • 若しくはコマンドパレット(Ctrl+Shift+P)→ Flutter: Select Device
  • .dart ファイルにブレークポイントを設定

  • 「デバッグと実行」(Ctrl+Shift+D / Cmd+Shift+D) から「▶」再生ボタン または F5 →「Dart&Flutter」で、デバッグ接続

導入

ツール類の導入手順

ココまでの導入手順もAIに生成させている。

レイアウト変更

以下はプロンプト

  • 以下で生成したコードを修正して3分割のヘッダ、左メニュー、メインエリアの画面を作成したい。
    なお、左メニューのリンクを押下するとメインエリアが画面遷移するものとする。
    
    flutter create my_app
    
    修正内容を教えてください。
    

  • 上記の出力が、全て lib/main.dart にまとめられていたため

    ページのファイルは分割したい。
    

以下は、出力から修正ポイントを抜粋したもの。

フォルダ構成

lib/
├── main.dart
├── layout/
│     └── main_layout.dart
└── pages/
      ├── page_home.dart
      ├── page_about.dart
      └── page_settings.dart

main・layout

  • main.dart

    import 'package:flutter/material.dart';
    import 'layout/main_layout.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'My App',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
            useMaterial3: true,
          ),
          home: const MainLayout(),
        );
      }
    }
  • layout/main_layout.dart

    import 'package:flutter/material.dart';
    import '../pages/page_home.dart';
    import '../pages/page_about.dart';
    import '../pages/page_settings.dart';
    import '../pages/page_contact.dart';
    
    class MainLayout extends StatefulWidget {
      const MainLayout({super.key});
    
      @override
      State<MainLayout> createState() => _MainLayoutState();
    }
    
    class _MainLayoutState extends State<MainLayout> {
      int _selectedIndex = 0;
    
      final List<Map<String, dynamic>> _menuItems = [
        {'icon': Icons.home, 'label': 'ホーム'},
        {'icon': Icons.info, 'label': '概要'},
        {'icon': Icons.settings, 'label': '設定'},
      ];
    
      final List<Widget> _pages = const [
        PageHome(),
        PageAbout(),
        PageSettings(),
      ];
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.primary,
            title: const Text(
              'My Application',
              style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
            ),
            actions: [
              IconButton(
                icon: const Icon(Icons.notifications, color: Colors.white),
                onPressed: () {},
              ),
              IconButton(
                icon: const Icon(Icons.account_circle, color: Colors.white),
                onPressed: () {},
              ),
            ],
          ),
          body: Row(
            children: [
              NavigationRail(
                selectedIndex: _selectedIndex,
                onDestinationSelected: (int index) {
                  setState(() {
                    _selectedIndex = index;
                  });
                },
                labelType: NavigationRailLabelType.all,
                destinations: _menuItems.map((item) {
                  return NavigationRailDestination(
                    icon: Icon(item['icon'] as IconData),
                    label: Text(item['label'] as String),
                  );
                }).toList(),
              ),
              const VerticalDivider(thickness: 1, width: 1),
              Expanded(
                child: _pages[_selectedIndex],
              ),
            ],
          ),
        );
      }
    }

pages

ホントは、page_contact.dartもあったが割愛。

  • page_home.dart
  • page_about.dart
  • page_settings.dart

Counter画面

もともとあったIncrement機能が消失したので、page_counter.dartとして追加。

既存のIncrement機能が消失したのでCounterとして追加して下さい。
```
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // TRY THIS: Try running your application with "flutter run". You'll see
        // the application has a purple toolbar. Then, without quitting the app,
        // try changing the seedColor in the colorScheme below to Colors.green
        // and then invoke "hot reload" (save your changes or press the "hot
        // reload" button in a Flutter-supported IDE, or press "r" if you used
        // the command line to start the app).
        //
        // Notice that the counter didn't reset back to zero; the application
        // state is not lost during the reload. To reset the state, use hot
        // restart instead.
        //
        // This works for code too, not just values: Most code changes can be
        // tested with just a hot reload.
        colorScheme: .fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  // 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;

  @override
  State<MyHomePage> 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++;
    });
  }

  @override
  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(
        // TRY THIS: Try changing the color here to a specific color (to
        // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
        // change color while the other colors stay the same.
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        // 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.
          //
          // 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).
          //
          // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
          // action in the IDE, or press "p" in the console), to see the
          // wireframe for each widget.
          mainAxisAlignment: .center,
          children: [
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

Layout(Counter画面)

main_layout.dart

_menuItems と _pages にCounterを追記

pages(Counter画面)

page_counter.dart

import 'package:flutter/material.dart';

class PageCounter extends StatefulWidget {
  const PageCounter({super.key});

  @override
  State<PageCounter> createState() => _PageCounterState();
}

class _PageCounterState extends State<PageCounter> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.touch_app, size: 64, color: Colors.teal),
            const SizedBox(height: 16),
            const Text('ボタンを押した回数:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

ヘッダ、左メニューの切出

  • プロンプト

    ヘッダ、左メニューを切り出したい。
    ちなみに同様のReactプロジェクトでは、pagesではなくcomponentに切り出されていた。
    
    import 'package:flutter/material.dart';
    import '../pages/page_home.dart';
    import '../pages/page_about.dart';
    import '../pages/page_settings.dart';
    import '../pages/page_counter.dart';
    
    class MainLayout extends StatefulWidget {
      const MainLayout({super.key});
    
      @override
      State<MainLayout> createState() => _MainLayoutState();
    }
    
    class _MainLayoutState extends State<MainLayout> {
      int _selectedIndex = 0;
    
      final List<Map<String, dynamic>> _menuItems = [
        {'icon': Icons.home, 'label': 'ホーム'},
        {'icon': Icons.info, 'label': '概要'},
        {'icon': Icons.settings, 'label': '設定'},
        {'icon': Icons.touch_app,    'label': 'カウンター'},
      ];
    
      final List<Widget> _pages = const [
        PageHome(),
        PageAbout(),
        PageSettings(),
        PageCounter(),
      ];
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.primary,
            title: const Text(
              'My Application',
              style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
            ),
            actions: [
              IconButton(
                icon: const Icon(Icons.notifications, color: Colors.white),
                onPressed: () {},
              ),
              IconButton(
                icon: const Icon(Icons.account_circle, color: Colors.white),
                onPressed: () {},
              ),
            ],
          ),
          body: Row(
            children: [
              NavigationRail(
                selectedIndex: _selectedIndex,
                onDestinationSelected: (int index) {
                  setState(() {
                    _selectedIndex = index;
                  });
                },
                labelType: NavigationRailLabelType.all,
                destinations: _menuItems.map((item) {
                  return NavigationRailDestination(
                    icon: Icon(item['icon'] as IconData),
                    label: Text(item['label'] as String),
                  );
                }).toList(),
              ),
              const VerticalDivider(thickness: 1, width: 1),
              Expanded(
                child: _pages[_selectedIndex],
              ),
            ],
          ),
        );
      }
    }
    
  • 出力(構成)

    lib/
    ├── components/
    │    ├── app_header.dart
    │    └── side_navigation.dart
    ├── pages/
    │    ├── page_home.dart
    │    ├── page_about.dart
    │    ├── page_settings.dart
    │    └── page_counter.dart
    └── layouts/
          └── main_layout.dart
    

補足的に

  • プロンプト

    「components」「pages」は慣例的ですか?
    
  • 出力
    pages → screens、components → widgets、page_home → home_screen

    lib/
    ├── screens/       # ← pages/ の代わり
    │    ├── home.dart
    │    ├── about.dart
    │    ├── settings.dart
    │    └── counter.dart
    ├── widgets/       # ← components/ の代わり
    │   ├── app_header.dart
    │   └── side_navigation.dart
    

※ 今回は、「_screens」は付与しない方向性で修正

ファイル修正

  • main_layout.dart

    import 'package:flutter/material.dart';
    import '../widgets/app_header.dart';
    import '../widgets/side_navigation.dart';
    import '../screens/home.dart';
    import '../screens/about.dart';
    import '../screens/settings.dart';
    import '../screens/counter.dart';
    
    class MainLayout extends StatefulWidget {
      const MainLayout({super.key});
    
      @override
      State<MainLayout> createState() => _MainLayoutState();
    }
    
    class _MainLayoutState extends State<MainLayout> {
      int _selectedIndex = 0;
    
      final List<Map<String, dynamic>> _menuItems = [
        {'icon': Icons.home, 'label': 'ホーム'},
        {'icon': Icons.info, 'label': '概要'},
        {'icon': Icons.settings, 'label': '設定'},
        {'icon': Icons.touch_app,    'label': 'カウンター'},
      ];
    
      final List<Widget> _pages = const [
        ScreenHome(),
        ScreenAbout(),
        ScreenSettings(),
        ScreenCounter(),
      ];
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: const AppHeader(),
          body: Row(
            children: [
              SideNavigation(
                selectedIndex: _selectedIndex,
                onDestinationSelected: (int index) {
                  setState(() {
                    _selectedIndex = index;
                  });
                },
                menuItems: _menuItems,
              ),
              const VerticalDivider(thickness: 1, width: 1),
              Expanded(
                child: _pages[_selectedIndex],
              ),
            ],
          ),
        );
      }
    }
  • app_header.dart

    import 'package:flutter/material.dart';
    
    class AppHeader extends StatelessWidget implements PreferredSizeWidget {
      const AppHeader({super.key});
    
      @override
      Widget build(BuildContext context) {
        return AppBar(
          backgroundColor: Theme.of(context).colorScheme.primary,
          title: const Text(
            'My Application',
            style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
          ),
          actions: [
            IconButton(
              icon: const Icon(Icons.notifications, color: Colors.white),
              onPressed: () {},
            ),
            IconButton(
              icon: const Icon(Icons.account_circle, color: Colors.white),
              onPressed: () {},
            ),
          ],
        );
      }
    
      @override
      Size get preferredSize => const Size.fromHeight(kToolbarHeight);
    }
  • side_navigation.dart

    import 'package:flutter/material.dart';
    
    class SideNavigation extends StatelessWidget {
      final int selectedIndex;
      final ValueChanged<int> onDestinationSelected;
      final List<Map<String, dynamic>> menuItems;
    
      const SideNavigation({
        super.key,
        required this.selectedIndex,
        required this.onDestinationSelected,
        required this.menuItems,
      });
    
      @override
      Widget build(BuildContext context) {
        return NavigationRail(
          selectedIndex: selectedIndex,
          onDestinationSelected: onDestinationSelected,
          labelType: NavigationRailLabelType.all,
          destinations: menuItems.map((item) {
            return NavigationRailDestination(
              icon: Icon(item['icon'] as IconData),
              label: Text(item['label'] as String),
            );
          }).toList(),
        );
      }
    }

参考

移行メモ

  • 「戻る」の並びにある「Flutterのファースト・ステップ2」は自ページのため、 その旨を補ってプレーン・テキストとした。
  • 同名の見出し(「Layout」「pages」)が重複するため、括弧で文脈を補って一意にした。
  • 元 Wiki の行頭空白によるプレフォーマット・ブロックは、 フェンス付きコードブロックにした。プロンプト中にコードブロックが 入れ子になっている箇所は、外側を 4 個のバッククォートで囲んだ。
  • 引用されている生成コードの colorScheme: .fromSeed(...)mainAxisAlignment: .center は、原文(AI の出力を貼ったもの)ママ。 Dart としては ColorScheme.fromSeed(...) / MainAxisAlignment.center が正しい。
  • リンク先が未移行のページは、移行先のファイル名を `DNET_○○.md` の形で示し TODO.md に登録した。

Tags: 移行, Flutter, Flutterデスクトップ, Windows, VS Code, NavigationRail, レイアウト, AI活用, Claude

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally