-
Notifications
You must be signed in to change notification settings - Fork 0
DNET_FlutterFirstStep2
-
戻る
- ...Flutterの5thステップ
- Flutterのファースト・ステップ2(本ページ)
- Flutterのセカンド・ステップ2
- Flutterのサード・ステップ2
2026年、リブートはFlutterデスクトップ(Windows)で実施
- この選択肢は、単純にAndroid/iOSの準備が難しいケースが多いため。
- (コレは、一先ず、WebAPIを経由した簡単なCRUD処理の実装と、OAuth系認証・認可処理を実装できれば良いため)
- Windowsでは、Android/iOSほど「カスタムURLスキームからの移行圧力」は強くなく、myapp:// 方式が今でも主流
- Windows 10/11(64bit)
- ストレージ:10GB以上の空き
- RAM:8GB以上推奨
-
flutter.dev から Flutter SDK をダウンロード
-
%USERPROFILE%\develop\flutter\ などのパスに解凍
-
環境変数 PATH に %USERPROFILE%\develop\flutter\bin を追加
-
CMD / PowerShellで確認
flutter --version dart --version
- Visual Studio のインストールで「C++によるデスクトップ開発」 ワークロードを選択
- 「MSVC v143(または最新)」「Windows SDK」コンポーネントが含まれていることを確認
- 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活用(https://claude.ai/chat)で、殆どWebを参照せず、作業を進めることが出来た(Sonnet4.6)。
- 先ずは、3分割レイアウトのCounterを作成してみる(flutter create でも Counter が生成される)。
ココまでの導入手順もAIに生成させている。
以下はプロンプト
-
1
以下で生成したコードを修正して3分割のヘッダ、左メニュー、メインエリアの画面を作成したい。 なお、左メニューのリンクを押下するとメインエリアが画面遷移するものとする。 flutter create my_app 修正内容を教えてください。 -
2
上記の出力が、全て lib/main.dart にまとめられていたためページのファイルは分割したい。
以下は、出力から修正ポイントを抜粋したもの。
lib/
├── main.dart
├── layout/
│ └── main_layout.dart
└── pages/
├── page_home.dart
├── page_about.dart
└── page_settings.dart
-
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], ), ], ), ); } }
ホントは、page_contact.dartもあったが割愛。
- page_home.dart
- page_about.dart
- page_settings.dart
もともとあった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),
),
);
}
}
```
main_layout.dart
_menuItems と _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_screenlib/ ├── 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を手動でインストールする
https://docs.flutter.dev/install/manual -
Flutterをデスクトップアプリ開発におすすめしない話 - izanami
https://izanami.dev/post/47b04883-8ea4-4282-85d9-d684bec4d0a1 -
デスクトップアプリ(Windows)の開発にFlutterを使うべき? : r/FlutterDev
https://www.reddit.com/r/FlutterDev/comments/1c9gwpx/should_i_use_flutter_for_desktop_app_windows/?tl=ja -
デスクトップ開発ならFlutterとElectronの友達は何が違うんだ #Flutter - Qiita
https://qiita.com/UBinKitte/items/ce0596c18ab9fd1d03c7 -
FrontendTemplates/UI/XPlat at develop · OpenTouryoProject/FrontendTemplates
https://github.com/OpenTouryoProject/FrontendTemplates/tree/develop/UI/XPlat/
移行メモ
- 「戻る」の並びにある「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
このWikiは「Open棟梁Project」,「OSSコンソーシアム 開発基盤部会」によって運営されています。