diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..9ddf6b28 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cmake.ignoreCMakeListsMissing": true +} \ No newline at end of file diff --git a/flutter_newokanelab/lib/core/api_config.dart b/flutter_newokanelab/lib/core/api_config.dart new file mode 100644 index 00000000..ad076a71 --- /dev/null +++ b/flutter_newokanelab/lib/core/api_config.dart @@ -0,0 +1,26 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; + +class ApiConfig { + // Androidエミュレータからは '10.0.2.2'、iOSシミュレータやWebからは 'localhost' または '127.0.0.1' + // 実機の場合はPCのローカルIPアドレスを指定する必要があります + static String get baseUrl { + if (kReleaseMode) { + // 本番環境のURL(今回は未使用) + return 'http://your-production-server.com'; + } + + // Androidエミュレータの場合 + if (!kIsWeb && Platform.isAndroid) { + return 'http://10.0.2.2:8080'; + } + + // iOSシミュレータ、Web、デスクトップの場合 + return 'http://127.0.0.1:8080'; + } + + // エンドポイント + static String get healthEndpoint => '$baseUrl/api/health'; + static String get companiesEndpoint => '$baseUrl/api/companies'; + static String get simulateEndpoint => '$baseUrl/api/simulate'; +} \ No newline at end of file diff --git a/flutter_newokanelab/lib/core/constants.dart b/flutter_newokanelab/lib/core/constants.dart index 88a852b1..41005b25 100644 --- a/flutter_newokanelab/lib/core/constants.dart +++ b/flutter_newokanelab/lib/core/constants.dart @@ -1 +1,6 @@ -//定数を書くファイル +import 'package:flutter/material.dart'; + +// App Colors +const Color kMainColor = Color(0xFFF7F6F5); +const Color kCardColor = Color(0xFFFFF9F5); +const Color kGreenColor = Color(0xFF79C06E); \ No newline at end of file diff --git a/flutter_newokanelab/lib/main.dart b/flutter_newokanelab/lib/main.dart index 7b7f5b6f..3e243dbb 100644 --- a/flutter_newokanelab/lib/main.dart +++ b/flutter_newokanelab/lib/main.dart @@ -1,122 +1,73 @@ import 'package:flutter/material.dart'; +import 'core/constants.dart'; +import 'screens/home_screen.dart'; // Import the new home screen +/// アプリケーションのエントリーポイント(開始点) void main() { - runApp(const MyApp()); + // InvestmentSimulatorApp ウィジェットをアプリケーションのルートとして実行 + runApp(const InvestmentSimulatorApp()); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +/// アプリケーション全体のテーマと基本的な設定を定義するルートウィジェット +class InvestmentSimulatorApp extends StatelessWidget { + // constコンストラクタ + const InvestmentSimulatorApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { + // MaterialAppウィジェットは、アプリケーションの基本的な構造やナビゲーションを提供 return MaterialApp( - title: 'Flutter Demo', + // アプリケーションのタイトル + title: '投資シミュレーター', + // アプリケーション全体のテーマ設定 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: 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 createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - 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: MainAxisAlignment.center, - children: [ - const Text('You have pushed the button this many times:'), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], + brightness: Brightness.light, + primaryColor: kGreenColor, + scaffoldBackgroundColor: kMainColor, + cardColor: kCardColor, + colorScheme: ColorScheme.fromSeed( + seedColor: kGreenColor, + brightness: Brightness.light, + primary: kGreenColor, + background: kMainColor, + surface: kCardColor, + onSurface: Colors.black, + ), + textTheme: const TextTheme( + bodyMedium: TextStyle(color: Colors.black), + headlineSmall: TextStyle(fontWeight: FontWeight.bold), + titleLarge: TextStyle(fontWeight: FontWeight.bold, color: kGreenColor), + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide(color: Colors.grey.shade400), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: const BorderSide(color: kGreenColor, width: 2), + ), + fillColor: kCardColor, + filled: true, + ), + sliderTheme: SliderThemeData( + activeTrackColor: kGreenColor, + inactiveTrackColor: Colors.grey[300], + thumbColor: kGreenColor, + overlayColor: kGreenColor.withAlpha(100), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: kGreenColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), ), ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + // アプリケーションのホームページ + home: const HomePage(), ); } } diff --git a/flutter_newokanelab/lib/models/investment_models.dart b/flutter_newokanelab/lib/models/investment_models.dart new file mode 100644 index 00000000..1a1f71b1 --- /dev/null +++ b/flutter_newokanelab/lib/models/investment_models.dart @@ -0,0 +1,179 @@ +import 'package:fl_chart/fl_chart.dart'; + +// --- UI用モデル --- + +/// 投資ファンドのデータ(UIでの選択肢) +class Fund { + final String id; // ファンドID + final String label; // 表示名 + final double annualReturn; // (バックエンド利用時は参考値、またはロジック側で使用) + + const Fund({required this.id, required this.label, required this.annualReturn}); +} + +/// ライフイベントのデータ(UIでの選択肢) +class LifeEvent { + final String id; + final String label; + + const LifeEvent({required this.id, required this.label}); +} + +// --- API連携用データモデル --- + +/// 企業情報 (/api/companies レスポンス用) +class Company { + final String tickerCode; + final String companyName; + + Company({required this.tickerCode, required this.companyName}); + + factory Company.fromJson(Map json) { + return Company( + tickerCode: json['ticker_code'] as String, + companyName: json['company_name'] as String, + ); + } +} + +/// シミュレーションAPIのレスポンス全体 +class SimulationResponse { + final List graphData; + final List events; + final SimulationSummary summary; + + SimulationResponse({ + required this.graphData, + required this.events, + required this.summary, + }); + + factory SimulationResponse.fromJson(Map json) { + return SimulationResponse( + graphData: (json['graph_data'] as List) + .map((e) => GraphData.fromJson(e)) + .toList(), + events: (json['events'] as List) + .map((e) => SimulationEvent.fromJson(e)) + .toList(), + summary: SimulationSummary.fromJson(json['summary']), + ); + } + + /// グラフ描画用に fl_chart の FlSpot リストに変換するヘルパーメソッド + /// X軸: シミュレーション開始からの経過年数 (0, 1, 2...) ※簡易的な変換 + /// 日付ベースでX軸を描画する場合は別途調整が必要ですが、 + /// 現在のUIロジックに合わせて「年単位」または「インデックス」でマッピングします。 + List toFlSpots() { + // データが多すぎる場合は間引くなどの処理が必要かもしれませんが、 + // ここでは単純に最初の日付からの経過年数(概算)をX軸にします。 + if (graphData.isEmpty) return []; + + final startDate = DateTime.parse(graphData.first.date); + return graphData.map((data) { + final currentDate = DateTime.parse(data.date); + final diffDays = currentDate.difference(startDate).inDays; + final years = diffDays / 365.25; // 年換算 + return FlSpot(years, data.totalAssets.toDouble()); + }).toList(); + } +} + +/// 日次の資産データ +class GraphData { + final String date; + final num totalAssets; + final num investedAmount; + + GraphData({ + required this.date, + required this.totalAssets, + required this.investedAmount, + }); + + factory GraphData.fromJson(Map json) { + return GraphData( + date: json['date'] as String, + totalAssets: json['total_assets'] as num, + investedAmount: json['invested_amount'] as num, + ); + } +} + +/// イベント(ニュース)データ +class SimulationEvent { + final String date; + final String ticker; + final String title; + final String description; + final num sentiment; + + SimulationEvent({ + required this.date, + required this.ticker, + required this.title, + required this.description, + required this.sentiment, + }); + + factory SimulationEvent.fromJson(Map json) { + return SimulationEvent( + date: json['date'] as String, + ticker: json['ticker'] as String, + title: json['title'] as String, + description: json['description'] as String, + sentiment: json['sentiment'] as num, + ); + } +} + +/// サマリーデータ +class SimulationSummary { + final num finalAssets; + final num totalInvested; + final num returnRate; + + SimulationSummary({ + required this.finalAssets, + required this.totalInvested, + required this.returnRate, + }); + + factory SimulationSummary.fromJson(Map json) { + return SimulationSummary( + finalAssets: json['final_assets'] as num, + totalInvested: json['total_invested'] as num, + returnRate: json['return_rate'] as num, + ); + } +} + +// --- アプリ内で使用する統合結果オブジェクト (ResultsSectionへ渡すもの) --- +class SimulationResult { + final List projectionData; + final Map summaryData; + final List events; // イベント表示用に追加 + + SimulationResult({ + required this.projectionData, + required this.summaryData, + this.events = const [], + }); + + /// APIレスポンスからUI用Resultを作成するファクトリ + factory SimulationResult.fromResponse(SimulationResponse response) { + final spots = response.toFlSpots(); + final profit = response.summary.finalAssets - response.summary.totalInvested; + + return SimulationResult( + projectionData: spots, + summaryData: { + "totalAmount": response.summary.finalAssets.toDouble(), + "totalInvested": response.summary.totalInvested.toDouble(), + "profit": profit.toDouble(), + "profitRate": response.summary.returnRate.toDouble(), + }, + events: response.events, + ); + } +} diff --git a/flutter_newokanelab/lib/screens/home_screen.dart b/flutter_newokanelab/lib/screens/home_screen.dart new file mode 100644 index 00000000..fe52ab9b --- /dev/null +++ b/flutter_newokanelab/lib/screens/home_screen.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; + +import '../models/investment_models.dart'; +import '../services/simulation_service.dart'; +import '../widgets/input_section.dart'; +import '../widgets/results_section.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + final _scrollController = ScrollController(); + final _resultsKey = GlobalKey(); + + // --- State Variables --- + double _initialInvestment = 300000; + double _simulationYears = 20; + bool _isMonthlyMode = false; + double _fixedMonthlyInvestment = 20000; + late List _monthlyInvestments; + String _selectedFundId = "7203.T"; + final Set _selectedEventIds = {}; + + bool _isLoading = false; + SimulationResult? _simulationResult; + + // --- Static Data --- + final List _funds = const [ + Fund(id: "7203.T", label: "トヨタ自動車 (7203.T)", annualReturn: 0.04), + Fund(id: "^N225", label: "日経平均株価 (^N225)", annualReturn: 0.05), + Fund(id: "6758.T", label: "ソニーグループ (6758.T)", annualReturn: 0.05), + Fund(id: "8306.T", label: "三菱UFJフィナンシャルG (8306.T)", annualReturn: 0.03), + Fund(id: "7974.T", label: "任天堂 (7974.T)", annualReturn: 0.06), + Fund(id: "8035.T", label: "東京エレクトロン (8035.T)", annualReturn: 0.07), + Fund(id: "9984.T", label: "ソフトバンクグループ (9984.T)", annualReturn: 0.08), + Fund(id: "6861.T", label: "キーエンス (6861.T)", annualReturn: 0.06), + Fund(id: "4063.T", label: "信越化学工業 (4063.T)", annualReturn: 0.05), + Fund(id: "9432.T", label: "日本電信電話 (9432.T)", annualReturn: 0.03), + ]; + final List _events = const [ + LifeEvent(id: "china_shock_2015", label: "チャイナショック懸念 (2015)"), + LifeEvent(id: "corona_shock_2020", label: "コロナショック (2020)"), + LifeEvent(id: "yen_depreciation_2022", label: "円安・物価高 (2022)"), + ]; + final List _months = const [ + "1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月" + ]; + + @override + void initState() { + super.initState(); + _monthlyInvestments = List.filled(12, 20000); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + double get _averageMonthlyInvestment { + if (!_isMonthlyMode) { + return _fixedMonthlyInvestment; + } + return _monthlyInvestments.reduce((a, b) => a + b) / 12; + } + + void _runSimulation() async { + setState(() { + _isLoading = true; + _simulationResult = null; + }); + + final result = await SimulationService.calculate( + initialInvestment: _initialInvestment, + averageMonthlyInvestment: _averageMonthlyInvestment, + selectedFundId: _selectedFundId, + funds: _funds, + simulationYears: _simulationYears.round(), + ); + + setState(() { + _simulationResult = result; + _isLoading = false; + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final context = _resultsKey.currentContext; + if (context != null) { + Scrollable.ensureVisible(context, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('投資シミュレーター'), + backgroundColor: Theme.of(context).cardColor, + elevation: 1, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(20.0), + child: Padding( + padding: const EdgeInsets.only(left: 16.0, bottom: 8.0), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + '将来の資産形成をシミュレーションします', + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ), + ), + ), + body: SingleChildScrollView( + controller: _scrollController, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + InputSection( + initialInvestment: _initialInvestment, + onInitialInvestmentChanged: (value) => setState(() => _initialInvestment = value), + simulationYears: _simulationYears, + onSimulationYearsChanged: (value) => setState(() => _simulationYears = value), + isMonthlyMode: _isMonthlyMode, + onMonthlyModeChanged: (value) => setState(() => _isMonthlyMode = value), + fixedMonthlyInvestment: _fixedMonthlyInvestment, + onFixedMonthlyInvestmentChanged: (value) => setState(() => _fixedMonthlyInvestment = value), + monthlyInvestments: _monthlyInvestments, + onMonthlyInvestmentChanged: (index, value) => setState(() => _monthlyInvestments[index] = value), + selectedFundId: _selectedFundId, + onFundIdChanged: (value) { + if (value != null) { + setState(() => _selectedFundId = value); + } + }, + selectedEventIds: _selectedEventIds, + onEventIdToggled: (id, value) { + setState(() { + if (value == true) { + _selectedEventIds.add(id); + } else { + _selectedEventIds.remove(id); + } + }); + }, + funds: _funds, + events: _events, + months: _months, + averageMonthlyInvestment: _averageMonthlyInvestment, + onSimulatePressed: _runSimulation, + ), + const SizedBox(height: 16), + ResultsSection( + isLoading: _isLoading, + simulationResult: _simulationResult, + resultsKey: _resultsKey, + initialInvestment: _initialInvestment, + averageMonthlyInvestment: _averageMonthlyInvestment, + simulationYears: _simulationYears, + ), + ], + ), + ), + ), + ); + } +} diff --git a/flutter_newokanelab/lib/services/api_service.dart b/flutter_newokanelab/lib/services/api_service.dart new file mode 100644 index 00000000..5f1be59a --- /dev/null +++ b/flutter_newokanelab/lib/services/api_service.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:flutter_newokanelab/core/api_config.dart'; +import 'package:flutter_newokanelab/models/investment_models.dart'; + +class ApiService { + + // 会社一覧を取得する + Future> getCompanies() async { + try { + final response = await http.get(Uri.parse(ApiConfig.companiesEndpoint)); + + if (response.statusCode == 200) { + // 日本語が含まれる可能性があるため、UTF-8でデコード + final List jsonResponse = json.decode(utf8.decode(response.bodyBytes)); + return jsonResponse.map((company) => Company.fromJson(company)).toList(); + } else { + throw Exception('Failed to load companies: ${response.statusCode}'); + } + } catch (e) { + // 開発中などサーバーに繋がらない場合のエラーハンドリング + print("Error fetching companies: $e"); + rethrow; + } + } + + // シミュレーションを実行する + Future simulate({ + required double initialInvestment, + required double monthlyInvestment, + required List tickers, + required String startDate, + required String endDate, + }) async { + final Map requestBody = { + "initial_investment": initialInvestment, + "monthly_investment": monthlyInvestment, + "tickers": tickers, + "start_date": startDate, + "end_date": endDate, + }; + + try { + final response = await http.post( + Uri.parse(ApiConfig.simulateEndpoint), + headers: {"Content-Type": "application/json"}, + body: json.encode(requestBody), + ); + + if (response.statusCode == 200) { + final Map jsonResponse = json.decode(utf8.decode(response.bodyBytes)); + return SimulationResponse.fromJson(jsonResponse); + } else { + throw Exception('Failed to run simulation: ${response.statusCode} ${response.body}'); + } + } catch (e) { + print("Error running simulation: $e"); + rethrow; + } + } +} diff --git a/flutter_newokanelab/lib/services/simulation_service.dart b/flutter_newokanelab/lib/services/simulation_service.dart new file mode 100644 index 00000000..bb3cdb4f --- /dev/null +++ b/flutter_newokanelab/lib/services/simulation_service.dart @@ -0,0 +1,76 @@ +import 'package:flutter_newokanelab/models/investment_models.dart'; +import 'package:flutter_newokanelab/services/api_service.dart'; + +class SimulationService { + static final ApiService _apiService = ApiService(); + + /// シミュレーションを実行し、結果を返す + static Future calculate({ + required double initialInvestment, + required double averageMonthlyInvestment, + required String selectedFundId, + required List funds, + required int simulationYears, + }) async { + + // 1. ファンドIDに基づいて投資銘柄(Tickers)を決定する + List tickers; + switch (selectedFundId) { + case 'nikkei225': + // 日経平均株価連動 + tickers = ["^N225"]; + break; + case 'japan_core': + // 国内主力大型株 (トヨタ, ソニーG, 三菱UFJ) + tickers = ["7203.T", "6758.T", "8306.T"]; + break; + case 'us_tech': + // 米国テック大手 (Apple, Microsoft, Google) + tickers = ["AAPL", "MSFT", "GOOGL"]; + break; + case 'semi_growth': + // 半導体・グロース (東京エレクトロン, キーエンス, NVIDIA) + tickers = ["8035.T", "6861.T", "NVDA"]; + break; + case 'high_dividend': + // 高配当・バリュー (三菱商事, 武田薬品, NTT) + tickers = ["8058.T", "4502.T", "9432.T"]; + break; + default: + // デフォルト + tickers = ["^N225"]; + } + + // 2. シミュレーション期間の設定 + // バックエンドのデータが 2015-01-01 からあるため、そこを開始点とします。 + // 終了日は開始日から simulationYears 後、またはデータの終わり(2024年末)まで + const startYear = 2015; + final endYear = startYear + simulationYears; + + // データが存在する範囲にクリップする(バックエンドのデータが2024年までと仮定) + final effectiveEndYear = endYear > 2024 ? 2024 : endYear; + + final startDate = "$startYear-01-01"; + final endDate = "$effectiveEndYear-12-31"; + + try { + // 3. API呼び出し + final response = await _apiService.simulate( + initialInvestment: initialInvestment, + monthlyInvestment: averageMonthlyInvestment, + tickers: tickers, + startDate: startDate, + endDate: endDate, + ); + + // 4. 結果の変換 + return SimulationResult.fromResponse(response); + + } catch (e) { + print("Simulation failed: $e"); + // エラー時は空の結果などを返すか、再スローしてUI側でハンドリングさせます + // ここでは簡易的にエラーを再スローします + rethrow; + } + } +} diff --git a/flutter_newokanelab/lib/widgets/input_section.dart b/flutter_newokanelab/lib/widgets/input_section.dart new file mode 100644 index 00000000..4a8208ba --- /dev/null +++ b/flutter_newokanelab/lib/widgets/input_section.dart @@ -0,0 +1,423 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../models/investment_models.dart'; + +/// ユーザー入力を受け付けるフォームセクション全体を構築するStatelessWidget +class InputSection extends StatelessWidget { + // --- 現在の入力値 --- + final double initialInvestment; + final double? simulationYears; + final bool isMonthlyMode; + final double fixedMonthlyInvestment; + final List monthlyInvestments; + final String selectedFundId; + final Set selectedEventIds; + + // --- データ --- + final List funds; + final List events; + final List months; + final double averageMonthlyInvestment; + + // --- コールバック関数 --- + final ValueChanged onInitialInvestmentChanged; + final ValueChanged onSimulationYearsChanged; + final ValueChanged onMonthlyModeChanged; + final ValueChanged onFixedMonthlyInvestmentChanged; + final void Function(int, double) onMonthlyInvestmentChanged; + final ValueChanged onFundIdChanged; + final void Function(String, bool?) onEventIdToggled; + final VoidCallback onSimulatePressed; + + const InputSection({ + super.key, + required this.initialInvestment, + this.simulationYears, + required this.isMonthlyMode, + required this.fixedMonthlyInvestment, + required this.monthlyInvestments, + required this.selectedFundId, + required this.selectedEventIds, + required this.funds, + required this.events, + required this.months, + required this.averageMonthlyInvestment, + required this.onInitialInvestmentChanged, + required this.onSimulationYearsChanged, + required this.onMonthlyModeChanged, + required this.onFixedMonthlyInvestmentChanged, + required this.onMonthlyInvestmentChanged, + required this.onFundIdChanged, + required this.onEventIdToggled, + required this.onSimulatePressed, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + children: [ + _buildInitialInvestmentCard(context), + const SizedBox(height: 16), + _buildYearsSelectorCard(context), + ], + ), + ), + const SizedBox(width: 16), + Expanded(child: _buildFundSelectorCard(context)), + ], + ), + const SizedBox(height: 16), + _buildMonthlyInvestmentCard(context), + const SizedBox(height: 16), + _buildEventsCard(context), + const SizedBox(height: 24), + ElevatedButton( + onPressed: onSimulatePressed, + child: const Text('シミュレーションを実行'), + ), + ], + ); + } + + /// 初期投資金額設定カード + Widget _buildInitialInvestmentCard(BuildContext context) { + final formatter = NumberFormat.currency(locale: 'ja_JP', symbol: '¥'); + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("初期投資金額", style: Theme.of(context).textTheme.bodySmall), + InkWell( + onTap: () { + _showNumberInputDialog( + context: context, + title: '初期投資金額', + initialValue: initialInvestment.toStringAsFixed(0), + onSave: (value) { + final newValue = double.tryParse(value); + if (newValue != null) { + final clampedValue = newValue.clamp(10000.0, 1000000.0); + onInitialInvestmentChanged(clampedValue); + } + }, + ); + }, + child: Text( + formatter.format(initialInvestment), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Theme.of(context).colorScheme.tertiary, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Slider( + value: initialInvestment, + min: 10000, + max: 1000000, + divisions: 99, + onChanged: onInitialInvestmentChanged, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("¥10,000", style: Theme.of(context).textTheme.bodySmall), + Text("¥1,000,000", style: Theme.of(context).textTheme.bodySmall), + ], + ), + ], + ), + ), + ); + } + + /// シミュレーション年数設定カード + Widget _buildYearsSelectorCard(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("シミュレーション年数", style: Theme.of(context).textTheme.bodySmall), + InkWell( + onTap: () { + _showNumberInputDialog( + context: context, + title: 'シミュレーション年数', + initialValue: (simulationYears ?? 20).round().toString(), + onSave: (value) { + final newValue = double.tryParse(value); + if (newValue != null) { + final clampedValue = newValue.clamp(1.0, 50.0); + onSimulationYearsChanged(clampedValue); + } + }, + ); + }, + child: Text( + '${(simulationYears ?? 20).round()} 年', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Theme.of(context).colorScheme.tertiary, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Slider( + value: simulationYears ?? 20, + min: 1, + max: 50, + divisions: 49, + onChanged: onSimulationYearsChanged, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("1年", style: Theme.of(context).textTheme.bodySmall), + Text("50年", style: Theme.of(context).textTheme.bodySmall), + ], + ), + ], + ), + ), + ); + } + + /// ファンド選択カード + Widget _buildFundSelectorCard(BuildContext context) { + final selectedFund = funds.firstWhere((f) => f.id == selectedFundId); + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("ファンドを選択", style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + DropdownButtonFormField( + value: selectedFundId, + items: funds.map((fund) { + return DropdownMenuItem( + value: fund.id, + child: Text('${fund.label} (リターン: ${(fund.annualReturn * 100).toStringAsFixed(1)}%)'), + ); + }).toList(), + onChanged: onFundIdChanged, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12), + ), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + const Text("選択中のファンド: "), + Text(selectedFund.label, style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.tertiary)), + ], + ), + ), + ], + ), + ), + ); + } + + /// 月額投資設定カード + Widget _buildMonthlyInvestmentCard(BuildContext context) { + final formatter = NumberFormat.currency(locale: 'ja_JP', symbol: '¥'); + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text("月ごとの投資金額設定", style: TextStyle(fontWeight: FontWeight.bold)), + Row( + children: [ + Text("定額", style: TextStyle(color: !isMonthlyMode ? Theme.of(context).colorScheme.tertiary : Colors.grey)), + Switch( + value: isMonthlyMode, + onChanged: onMonthlyModeChanged, + activeColor: Theme.of(context).colorScheme.tertiary, + ), + Text("月ごと", style: TextStyle(color: isMonthlyMode ? Theme.of(context).colorScheme.tertiary : Colors.grey)), + ], + ), + ], + ), + const SizedBox(height: 16), + !isMonthlyMode + ? _buildFixedInvestmentInput(context) + : _buildMonthlyInvestmentInputs(context), + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: Text( + isMonthlyMode + ? '平均月額: ${formatter.format(averageMonthlyInvestment)}' + : '年間投資額: ${formatter.format(fixedMonthlyInvestment * 12)}', + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ], + ), + ), + ); + } + + /// 定額投資入力フィールド + Widget _buildFixedInvestmentInput(BuildContext context) { + return TextFormField( + initialValue: fixedMonthlyInvestment.toStringAsFixed(0), + decoration: const InputDecoration( + labelText: '月額投資金額', + prefixText: '¥ ', + ), + keyboardType: TextInputType.number, + onChanged: (value) => onFixedMonthlyInvestmentChanged(double.tryParse(value) ?? fixedMonthlyInvestment), + ); + } + + /// 月ごと投資入力フィールド + Widget _buildMonthlyInvestmentInputs(BuildContext context) { + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 3.0, + crossAxisSpacing: 10, + mainAxisSpacing: 5, + ), + itemCount: 12, + itemBuilder: (context, index) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(months[index], style: Theme.of(context).textTheme.bodySmall), + Expanded( + child: TextFormField( + initialValue: monthlyInvestments[index].toStringAsFixed(0), + keyboardType: TextInputType.number, + decoration: const InputDecoration( + prefixText: '¥ ', + contentPadding: EdgeInsets.symmetric(horizontal: 8), + ), + onChanged: (value) => onMonthlyInvestmentChanged(index, double.tryParse(value) ?? monthlyInvestments[index]), + ), + ), + ], + ); + }, + ); + } + + /// ライフイベント選択カード + Widget _buildEventsCard(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("重要なイベント", style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 3, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + ), + itemCount: events.length, + itemBuilder: (context, index) { + final event = events[index]; + return CheckboxListTile( + title: Text(event.label, style: Theme.of(context).textTheme.bodySmall), + value: selectedEventIds.contains(event.id), + onChanged: (value) => onEventIdToggled(event.id, value), + controlAffinity: ListTileControlAffinity.leading, + activeColor: Theme.of(context).colorScheme.tertiary, + contentPadding: EdgeInsets.zero, + ); + }, + ), + ], + ), + ), + ); + } + + // Helper method to show a dialog for number input + Future _showNumberInputDialog({ + required BuildContext context, + required String title, + required String initialValue, + required void Function(String) onSave, + }) async { + final controller = TextEditingController(text: initialValue); + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(title), + content: TextField( + controller: controller, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + autofocus: true, + decoration: const InputDecoration( + hintText: '数値を入力してください', + ), + ), + actions: [ + TextButton( + child: const Text('キャンセル'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('保存'), + onPressed: () { + onSave(controller.text); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } +} diff --git a/flutter_newokanelab/lib/widgets/results_section.dart b/flutter_newokanelab/lib/widgets/results_section.dart new file mode 100644 index 00000000..c8cd5b7c --- /dev/null +++ b/flutter_newokanelab/lib/widgets/results_section.dart @@ -0,0 +1,286 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import '../models/investment_models.dart'; + +/// シミュレーション結果の表示セクション全体を構築するStatelessWidget +class ResultsSection extends StatelessWidget { + final bool isLoading; + final SimulationResult? simulationResult; + final GlobalKey resultsKey; + final double initialInvestment; + final double averageMonthlyInvestment; + final double? simulationYears; + + const ResultsSection({ + super.key, + required this.isLoading, + this.simulationResult, + required this.resultsKey, + required this.initialInvestment, + required this.averageMonthlyInvestment, + this.simulationYears, + }); + + @override + Widget build(BuildContext context) { + // 数値を通貨形式(円)にフォーマットするためのフォーマッター + final formatter = NumberFormat.currency(locale: 'ja_JP', symbol: '¥'); + + Widget content; + + if (isLoading) { + // ローディング中の場合、インジケーターを表示 + content = const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 80.0), + child: CircularProgressIndicator(), + )); + } else if (simulationResult == null) { + // まだ結果がない場合、初期メッセージを表示 + content = Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 80.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.insights, size: 80, color: Colors.grey[700]), + const SizedBox(height: 20), + Text( + 'シミュレーションを実行して\n結果を表示します', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey[600], fontSize: 16), + ), + ], + ), + ), + ); + } else { + // 結果がある場合、グラフとサマリーを表示 + final projectionData = simulationResult!.projectionData; + final summaryData = simulationResult!.summaryData; + // LayoutBuilderを使って、画面幅に応じたレイアウト切り替えを行う + content = LayoutBuilder( + builder: (context, constraints) { + // 画面幅が800pxより大きい場合 (PCレイアウト) + if (constraints.maxWidth > 800) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildChartCard(context, projectionData), + const SizedBox(height: 16), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildInvestmentSummaryCard(context, summaryData, formatter)), + const SizedBox(width: 16), + Expanded(child: _buildExpectedResultsCard(context, summaryData, formatter)), + ], + ), + ], + ); + } else { // 画面幅が800px以下の場合 (モバイルレイアウト) + return Column( + children: [ + _buildChartCard(context, projectionData), + const SizedBox(height: 16), + _buildSummaryCards(context, summaryData, formatter), + ], + ); + } + }, + ); + } + + // 結果セクション全体のウィジェット + return Column( + key: resultsKey, // スクロール位置の特定に使用 + children: [ + Padding( + padding: const EdgeInsets.only(top: 24, bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("シミュレーション結果", style: Theme.of(context).textTheme.headlineSmall), + ], + ), + ), + const SizedBox(height: 8), + content, + ], + ); + } + + /// 資産成長グラフのカードを構築 + Widget _buildChartCard(BuildContext context, List data) { + return Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("${(simulationYears ?? 20).round()}年間の資産成長", style: Theme.of(context).textTheme.bodySmall), + const SizedBox(height: 24), + SizedBox( + height: 500, + child: LineChart( + LineChartData( + gridData: FlGridData( + show: true, + drawVerticalLine: true, + getDrawingHorizontalLine: (value) => FlLine(color: Colors.white.withOpacity(0.1), strokeWidth: 1), + getDrawingVerticalLine: (value) => FlLine(color: Colors.white.withOpacity(0.1), strokeWidth: 1), + ), + titlesData: FlTitlesData( + leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: 60, getTitlesWidget: _leftTitleWidgets)), + bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: 30, getTitlesWidget: _bottomTitleWidgets)), + topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + ), + borderData: FlBorderData(show: true, border: Border.all(color: Colors.white.withOpacity(0.1))), + lineBarsData: [ + LineChartBarData( + spots: data, + isCurved: true, + color: Theme.of(context).colorScheme.tertiary, + barWidth: 4, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + color: Theme.of(context).colorScheme.tertiary.withOpacity(0.3), + ), + ), + ], + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipItems: (touchedBarSpots) { + return touchedBarSpots.map((barSpot) { + final year = barSpot.x.toInt(); + final amount = barSpot.y; + final formatter = NumberFormat.currency(locale: 'ja_JP', symbol: '¥'); + return LineTooltipItem( + '${year}年目\n${formatter.format(amount)}', + TextStyle(color: Theme.of(context).colorScheme.onPrimary), + ); + }).toList(); + }, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + + /// グラフのX軸(下側)のラベルを構築 + Widget _bottomTitleWidgets(double value, TitleMeta meta) { + const style = TextStyle(fontSize: 10); + Widget text; + if (value.toInt() % 5 == 0) { + text = Text('${value.toInt()}年', style: style); + } else { + text = const Text('', style: style); + } + return SideTitleWidget(axisSide: meta.axisSide, space: 8.0, child: text); + } + + /// グラフのY軸(左側)のラベルを構築 + Widget _leftTitleWidgets(double value, TitleMeta meta) { + final style = TextStyle(fontSize: 10, color: Colors.grey[400]); + if (value % 1000000 == 0 && value != 0) { + return Text('${(value / 1000000).toInt()}M', style: style, textAlign: TextAlign.right); + } + return const SizedBox.shrink(); + } + + /// サマリーカード群を構築(モバイルレイアウト用) + Widget _buildSummaryCards(BuildContext context, Map summaryData, NumberFormat formatter) { + if (summaryData['totalInvested'] == null || summaryData['totalAmount'] == null || summaryData['profit'] == null || summaryData['profitRate'] == null) { + return const SizedBox.shrink(); + } + return Column( + children: [ + _buildInvestmentSummaryCard(context, summaryData, formatter), + const SizedBox(height: 16), + _buildExpectedResultsCard(context, summaryData, formatter), + ], + ); + } + + /// 投資額サマリーカードを構築 + Widget _buildInvestmentSummaryCard(BuildContext context, Map summary, NumberFormat formatter) { + final years = (simulationYears ?? 20).round(); + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("投資額サマリー", style: Theme.of(context).textTheme.bodySmall), + const SizedBox(height: 12), + _buildSummaryRow(context, "初期投資", formatter.format(initialInvestment)), + _buildSummaryRow(context, "月額投資 × ${years * 12}ヶ月", formatter.format(averageMonthlyInvestment * 12 * years)), + const Divider(height: 24), + _buildSummaryRow(context, "総投資額", formatter.format(summary['totalInvested']), isTotal: true), + ], + ), + ), + ); + } + + /// 予想成果カードを構築 + Widget _buildExpectedResultsCard(BuildContext context, Map summary, NumberFormat formatter) { + final years = (simulationYears ?? 20).round(); + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("予想成果(${years}年後)", style: Theme.of(context).textTheme.bodySmall), + const SizedBox(height: 12), + Text("予想資産額", style: Theme.of(context).textTheme.bodySmall), + Text( + formatter.format(summary['totalAmount']), + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.tertiary, + ), + ), + const Divider(height: 24), + Text("利益", style: Theme.of(context).textTheme.bodySmall), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(formatter.format(summary['profit']), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + Text('+${summary['profitRate']?.toStringAsFixed(1)}%', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Theme.of(context).colorScheme.tertiary)), + ], + ) + ], + ), + ), + ); + } + + /// サマリーカード内の各行を構築 + Widget _buildSummaryRow(BuildContext context, String label, String value, {bool isTotal = false}) { + final valueStyle = isTotal + ? TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.tertiary, fontSize: 16) + : const TextStyle(fontWeight: FontWeight.bold, fontSize: 16); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: Theme.of(context).textTheme.bodySmall), + Text(value, style: valueStyle), + ], + ), + ); + } +} diff --git a/flutter_newokanelab/pubspec.lock b/flutter_newokanelab/pubspec.lock index d074b124..92c2c5ce 100644 --- a/flutter_newokanelab/pubspec.lock +++ b/flutter_newokanelab/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -57,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: d0f0d49112f2f4b192481c16d05b6418bd7820e021e265a3c22db98acf7ed7fb + url: "https://pub.dev" + source: hosted + version: "0.68.0" flutter: dependency: "direct main" description: flutter @@ -75,6 +91,30 @@ packages: description: flutter source: sdk version: "0.0.0" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" leak_tracker: dependency: transitive description: @@ -192,6 +232,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.6" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: @@ -208,6 +256,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" sdks: dart: ">=3.9.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/flutter_newokanelab/pubspec.yaml b/flutter_newokanelab/pubspec.yaml index db66e012..7d8b7662 100644 --- a/flutter_newokanelab/pubspec.yaml +++ b/flutter_newokanelab/pubspec.yaml @@ -1,78 +1,41 @@ -name: flutter_newokanelab -description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +name: flutter_newokanelab # パッケージ名 +description: "A new Flutter project." # パッケージの説明 +publish_to: 'none' # pub.devへの公開を防ぐ -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +version: 1.0.0+1 # アプリケーションのバージョン番号 environment: - sdk: ^3.9.0 + sdk: '>=3.9.0 <4.0.0' # Dart SDKのバージョン制約 -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter + fl_chart: ^0.68.0 # グラフ描画ライブラリ + intl: ^0.19.0 # 国際化ライブラリ - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. + # iOSスタイルのアイコンフォント cupertino_icons: ^1.0.8 + http: ^1.6.0 dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. + # コーディング規約をチェックするための推奨Linter flutter_lints: ^5.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. +# Flutter固有の設定 flutter: - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. + # Material Iconsフォントを有効にする uses-material-design: true - # To add assets to your application, add an assets section, like this: + # アセット(画像など)を追加するには、次のように記述します # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: + # カスタムフォントを追加するには、次のように記述します # fonts: # - family: Schyler # fonts: @@ -84,6 +47,3 @@ flutter: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package