「[[.NET 開発基盤部会 Wiki>http://dotnetdevelopmentinfrastructure.osscons.jp]]」は、「[[Open棟梁Project>https://github.com/OpenTouryoProject/]]」,「[[OSSコンソーシアム .NET開発基盤部会>https://www.osscons.jp/dotNetDevelopmentInfrastructure/]]」によって運営されています。
-[[戻る>Flutter]]
--...[[Flutterの5thステップ]]
--Flutterのファースト・ステップ2
--[[Flutterのセカンド・ステップ2]]
--[[Flutterのサード・ステップ2]]
*目次 [#d0705168]
#contents
*概要 [#x79dd04e]
2026年、リブートはFlutterデスクトップ(Windows)で実施
-この選択肢は、単純にAndroid/iOSの準備が難しいケースが多いため。
-(コレは、一先ず、WebAPIを経由した簡単なCRUD処理の実装と、OAuth系認証・認可処理を実装できれば良いため)
-Windowsでは、Android/iOSほど「カスタムURLスキームからの移行圧力」は強くなく、myapp:// 方式が今でも主流
*詳細 [#b3a57dbc]
**システム要件 [#e740e7e8]
-Windows 10/11(64bit)
-ストレージ:10GB以上の空き
-RAM:8GB以上推奨
**インストール [#gd3d7a56]
***Flutter SDK [#x75e9ac9]
-flutter.dev から Flutter SDK をダウンロード
-%USERPROFILE%\develop\flutter\ などのパスに解凍
-環境変数 PATH に %USERPROFILE%\develop\flutter\bin を追加
-CMD / PowerShellで確認
flutter --version
dart --version
***Visual Studio [#l16d83af]
-Visual Studio のインストールで「C++によるデスクトップ開発」 ワークロードを選択
-「MSVC v143(または最新)」「Windows SDK」コンポーネントが含まれていることを確認
***VS Code [#b5c05e19]
-VS Codeのインストール
-拡張機能 Flutter(Dart含む)をインストール
**設定 [#r9e5e8d7]
CMD / PowerShellで
***有効化 [#qa8b8e13]
Windowsデスクトップ対応を有効化
flutter config --enable-windows-desktop
***確認 [#e693fbe3]
依存関係の確認
flutter doctor
**開発 [#afe00c72]
CMD / PowerShellで
***作成 [#t4b49c03]
flutter create my_app
***編集 [#ta991b13]
cd my_app
***実行 [#u2c7682a]
flutter run -d windows
***デバッグ [#kea768df]
VS Code上から、
-以下から、デバイスを Windows (desktop) に切り替え。
--右下のステータスバー(No Device)
--若しくはコマンドパレット(Ctrl+Shift+P)→ Flutter: Select Device
-.dart ファイルにブレークポイントを設定
-「デバッグと実行」(Ctrl+Shift+D / Cmd+Shift+D) から「▶」再生ボタン または F5 →「Dart&Flutter」で、デバッグ接続
*導入 [#s915c677]
-AI活用(https://claude.ai/chat)で、殆どWebを参照せず、作業を進めることが出来た(Sonnet4.6)。
-先ずは、[[3分割レイアウトのCounter>Reactのファースト・ステップ2]]を作成してみる(flutter create でも Counter が生成される)。
**ツール類の導入手順 [#h9f89f3f]
[[ココまでの導入手順>#b3a57dbc]]もAIに生成させている。
**レイアウト変更 [#c0f7556b]
以下はプロンプト
-1
以下で生成したコードを修正して3分割のヘッダ、左メニュー、メインエリアの画面を作成したい。
なお、左メニューのリンクを押下するとメインエリアが画面遷移するものとする。
flutter create my_app
修正内容を教えてください。
-2~
上記の出力が、全て lib/main.dart にまとめられていたため
ページのファイルは分割したい。
以下は、出力から修正ポイントを抜粋したもの。
***フォルダ構成 [#ae76d40f]
lib/
├── main.dart
├── layout/
│ └── main_layout.dart
└── pages/
├── page_home.dart
├── page_about.dart
└── page_settings.dart
***main・layout [#p1811bf1]
-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 [#xf1f3342]
ホントは、page_contact.dartもあったが割愛。
-page_home.dart
-page_about.dart
-page_settings.dart
**Counter画面 [#ze2cbf5e]
もともとあった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 [#x570320f]
main_layout.dart~
_menuItems と _pages にCounterを追記
***pages [#z65213bb]
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),
),
);
}
}
**ヘッダ、左メニューの切出 [#w247e2ee]
-プロンプト
ヘッダ、左メニューを切り出したい。
ちなみに同様の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
***補足的に [#ec05e263]
-プロンプト
「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」は付与しない方向性で修正
***ファイル修正 [#dd6e961f]
-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(),
);
}
}
*参考 [#bd14ad50]
-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
-Flutterを手動でインストールする~
https://docs.flutter.dev/install/manual
-FrontendTemplates/UI/XPlat at develop · OpenTouryoProject/FrontendTemplates~
https://github.com/OpenTouryoProject/FrontendTemplates/tree/develop/UI/XPlat/
--https://github.com/OpenTouryoProject/FrontendTemplates/tree/develop/UI/XPlat/flutter_template
--https://github.com/OpenTouryoProject/FrontendTemplates/tree/develop/UI/XPlat/old/flutter_template