ソースコード source code

下記アプリの主要なソースコードを公開しています。アプリ開発の参考になれば幸いです。

画像等が別途必要ですので下記情報のみでアプリが完成するものではありません。 アプリは少しずつ機能拡張していますのでストア公開されているアプリと内容が異なる場合があります。 コードはコピーして自由にお使いいただけます。ただし著作権は放棄しておりませんので全部の再掲載はご遠慮ください。部分的に再掲載したり、改変して再掲載するのは構いません。 自身のアプリ作成の参考として個人使用・商用問わず自由にお使いいただけます。 コード記述のお手本を示すものではありません。ミニアプリですので変数名などさほど気遣いしていない部分も有りますし間違いも有るかと思いますので参考程度にお考え下さい。 他の賢者の皆様が公開されているコードを参考にした箇所も含まれます。Flutter開発の熟練者が書いたコードではありません。 エンジニア向け技術情報共有サービスではありませんので説明は省いています。 GitHubなどへの公開は予定しておりません。

下記コードの最終ビルド日: 2025-02-20

pubspec.yaml

name: lunarage
description: "Lunar Age"
# 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

# 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.5+6

environment:
  sdk: ^3.6.0-198.0.dev

# 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

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.8
  webview_flutter: ^4.10.0
  webview_flutter_android: any
  webview_flutter_wkwebview: any
  google_mobile_ads: ^5.3.1
  package_info_plus: ^8.2.1
  path_provider: ^2.0.11

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_launcher_icons: ^0.14.3    #flutter pub run flutter_launcher_icons
  flutter_native_splash: ^2.4.4     #flutter pub run flutter_native_splash:create

  # 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.
  flutter_lints: ^5.0.0

flutter_launcher_icons:
  android: "launcher_icon"
  ios: true
  image_path: "assets/icon/icon.png"
  adaptive_icon_background: "assets/icon/icon_back.png"
  adaptive_icon_foreground: "assets/icon/icon_fore.png"

flutter_native_splash:
  color: '#000000'
  image: 'assets/image/splash.png'
  color_dark: '#000000'
  image_dark: 'assets/image/splash.png'
  fullscreen: true
  android_12:
    icon_background_color: '#000000'
    image: 'assets/image/splash.png'
    icon_background_color_dark: '#000000'
    image_dark: 'assets/image/splash.png'

# 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:

  # 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.
  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
  assets:
    - assets/icon/
    - assets/image/
    - assets/httpdocs/
    - assets/httpdocs/common/css/
    - assets/httpdocs/image/
    - assets/httpdocs/js/

  # 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:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - 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

lib/ad_mob.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-02-06
///
library;

import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io';

class AdMob {
  late BannerAd _adMobBanner;
  bool _isAdMob = false;
  AdMob() { //constructor
    String adBannerUnitId = '';
    if (!kIsWeb && Platform.isAndroid) {
      //adBannerUnitId = 'ca-app-pub-3940256099942544/6300978111';  //test
      adBannerUnitId = 'ca-app-pub-0000000000000000/0000000000';
      _isAdMob = true;
    } else if (!kIsWeb && Platform.isIOS) {
      //adBannerUnitId = 'ca-app-pub-3940256099942544/6300978111';  //test
      adBannerUnitId = 'ca-app-pub-0000000000000000/0000000000';
      _isAdMob = true;
    }
    if (_isAdMob) {
      _adMobBanner = BannerAd(
        adUnitId: adBannerUnitId,
        size: AdSize.banner,
        request: const AdRequest(),
        listener: const BannerAdListener(),
      );
    }
  }
  void load() {
    if (_isAdMob) {
      _adMobBanner.load();
    }
  }
  void dispose() {
    if (_isAdMob) {
      _adMobBanner.dispose();
    }
  }
  Widget getAdBannerWidget() {
    if (_isAdMob) {
      return Container(
        alignment: Alignment.center,
        width: _adMobBanner.size.width.toDouble(),
        height: _adMobBanner.size.height.toDouble(),
        child: AdWidget(ad: _adMobBanner),
      );
    } else {
      return Container();
    }
  }
  double getAdBannerHeight() {
    if (_isAdMob) {
      return _adMobBanner.size.height.toDouble();
    } else {
      return 150;  //for web
    }
  }
}

lib/const_value.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;

import 'package:flutter/material.dart';

class ConstValue {
  //color
  static const Color colorHeader = Color.fromRGBO(0,0,0,0.1);
  static const Color colorText = Color.fromRGBO(255,255,255,1);
  static const Color colorBack = Color.fromRGBO(0,0,0,1);
  static const Color colorSettingHeader = Color.fromRGBO(69, 99, 202, 1.0);
  static const Color colorUiActiveColor = Color.fromRGBO(69, 99, 202, 1.0);
  static const Color colorUiInactiveColor = Colors.black26;

}

lib/empty.dart

lib/local_server.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2025-02-20
///
library;

import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';

class LocalServer {
  late HttpServer _server;
  String _serverUrl = '';
  LocalServer() { //constructor
  }
  Future<void> start() async {
    //Completerのインスタンスを作成し、将来的に完了を示す非同期操作を制御。これにより、非同期処理の完了を手動で通知することができる。
    final Completer<void> completer = Completer<void>();
    //
    _server = await HttpServer.bind(InternetAddress.loopbackIPv4, 8080);
    _serverUrl = 'http://${_server.address.host}:${_server.port}/';
    _server.listen((HttpRequest request) async {
      //リクエストされたパスを処理(クエリパラメーターを無視)
      final pathSegments = request.uri.pathSegments;
      final String rawPath = pathSegments.join('/');
      final String pathWithoutQuery = rawPath.split('?')[0];
      final String path = pathWithoutQuery.isEmpty ? 'assets/httpdocs/index.html' : 'assets/httpdocs/$pathWithoutQuery';
      try {
        ContentType contentType;
        if (path.endsWith('.html')) {
          contentType = ContentType.html;
        } else if (path.endsWith('.css')) {
          contentType = ContentType('text', 'css');
        } else if (path.endsWith('.js')) {
          contentType = ContentType('application', 'javascript');
        } else if (path.endsWith('.webp')) {
          contentType = ContentType('image', 'webp');
        } else if (path.endsWith('.ico')) {
          contentType = ContentType('image', 'icon');
        } else {
          contentType = ContentType('application', 'octet-stream');
        }
        request.response.headers.contentType = contentType;
        //キャッシュ制御ヘッダーを追加してキャッシュを無効化
        //request.response.headers.set(HttpHeaders.cacheControlHeader, 'no-cache, no-store, must-revalidate');
        //request.response.headers.set(HttpHeaders.pragmaHeader, 'no-cache');
        //request.response.headers.set(HttpHeaders.expiresHeader, '0');
        //
        final bytes = await rootBundle.load(path);
        request.response.add(bytes.buffer.asUint8List());
        await request.response.close();
      } catch (e) {
        //print('Error: $path ${e}');
        request.response.statusCode = HttpStatus.notFound;
        request.response.write('404 Not Found');
        await request.response.close();
      }
    });
    //Completerを完了状態にして、非同期処理の終了を通知します。
    completer.complete();
    //非同期処理の完了を示すFutureを返します。
    //呼び出し元はこのFutureをawaitすることで、処理の完了を待つことができます。
    return completer.future;
  }
  void close() {
    _server.close();
  }
  String url() {
    return _serverUrl;
  }
}

lib/main.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2025-02-17
///
library;

import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:webview_flutter/webview_flutter.dart';

//自身で作成したclassを読み込む
import 'package:lunarage/const_value.dart';
import 'package:lunarage/version_state.dart';
import 'package:lunarage/setting.dart';
import 'package:lunarage/ad_mob.dart';
import 'package:lunarage/my_web_view_controller.dart';
import 'package:lunarage/local_server.dart';

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MainHomePage(),
    );
  }
}

class MainHomePage extends StatefulWidget {
  const MainHomePage({super.key});
  @override
  State<MainHomePage> createState() => _MainHomePageState();
}

class _MainHomePageState extends State<MainHomePage> {
  final AdMob _adMob = AdMob(); //広告表示
  final MyWebViewController _myWebViewController = MyWebViewController(); //WebViewController
  final LocalServer _localServer = LocalServer(); //ローカルサーバー
  late final WebViewController _webViewController;
  late List<int> _yearList;
  final List<int> _monthList = List<int>.generate(12, (index) => index + 1);
  int _selectedYear = DateTime.now().year;
  int _selectedMonth = DateTime.now().month;

  //アプリのバージョン取得
  void _getVersion() async {
    PackageInfo packageInfo = await PackageInfo.fromPlatform();
    setState(() {
      VersionState.versionSave(packageInfo.version);
    });
  }

  void _updateWebView() async {
    String serverUrl = _localServer.url();
    await _webViewController.loadRequest(Uri.parse('${serverUrl}index.html?year=$_selectedYear&month=$_selectedMonth'));
  }

  List<int> _generateYearList() {
    int currentYear = DateTime.now().year;
    return List<int>.generate(161, (index) => currentYear - 80 + index);
  }
  void _incrementYear() {
    setState(() {
      if (_selectedYear < _yearList.last) {
        _selectedYear += 1;
      }
      _updateWebView();
    });
  }
  void _decrementYear() {
    setState(() {
      if (_selectedYear > _yearList.first) {
        _selectedYear -= 1;
      }
      _updateWebView();
    });
  }
  void _incrementMonth() {
    setState(() {
      if (_selectedMonth < 12) {
        _selectedMonth += 1;
      } else {
        if (_selectedYear < _yearList.last) {
          _selectedMonth = 1;
          _selectedYear += 1;
        }
      }
      _updateWebView();
    });
  }
  void _decrementMonth() {
    setState(() {
      if (_selectedMonth > 1) {
        _selectedMonth -= 1;
      } else {
        if (_selectedYear > _yearList.first) {
          _selectedMonth = 12;
          _selectedYear -= 1;
        }
      }
      _updateWebView();
    });
  }

  //ページ起動開始時に一度だけ呼ばれる
  @override
  void initState() {
    super.initState();
    _yearList = _generateYearList();
    _getVersion();
    _adMob.load();
    _webViewController = _myWebViewController.controller();
    Future<void>(() async {
      await _localServer.start();
      Future.delayed(Duration(seconds: 1), () {
        _updateWebView();
      });
      Future.delayed(Duration(seconds: 2), () { //出ない時が有るので再度
        _updateWebView();
      });
    });
  }
  //ページ終了時に一度だけ呼ばれる
  @override
  void dispose() {
    _adMob.dispose();
    _localServer.close();
    super.dispose();
  }
  //画面全体
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.transparent,
      appBar: AppBar(
        backgroundColor: ConstValue.colorHeader,
        //タイトル表示
        title: const Text('Lunar age',
          style: TextStyle(
            color: Colors.white,
            fontSize: 15.0,
          )
        ),
        //設定ボタン
        actions: <Widget>[
          TextButton(
            onPressed: () async {
              bool? ret = await Navigator.of(context).push(
                MaterialPageRoute<bool>(builder:(context) => const SettingPage()),
              );
              //awaitで呼び出しているので、settingから戻ったら以下が実行される。
              if (ret!) { //設定で適用だった場合
                setState(() {});
              }
            },
            child: Text('Information',
              style: const TextStyle(
                color: Colors.white,
              )
            )
          )
        ]
      ),
      body: SafeArea(
        child: Column(children:[
          Row(children:[
            Expanded(child: _selectYear()),
            Expanded(child: _selectMonth()),
          ]),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.only(top: 10, left: 10, right: 10, bottom: 0),
              child: Center(
                child: WebViewWidget(controller: _webViewController),
              ),
            )
          ),
          //広告
          Padding(
            padding: const EdgeInsets.only(top: 10, left: 0, right: 0, bottom: 0),
            child: SizedBox(
              width: double.infinity,
              child: _adMob.getAdBannerWidget(),
            )
          )
        ])
      )
    );
  }

  Widget _selectYear() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        IconButton(
          icon: Icon(Icons.chevron_left),
          color: Colors.white,
          onPressed: _decrementYear,
        ),
        DropdownButton<int>(
          value: _selectedYear,
          onChanged: (int? newValue) {
            setState(() {
              _selectedYear = newValue!;
              _updateWebView();
            });
          },
          dropdownColor: Colors.grey[850],
          items: _generateYearList().map<DropdownMenuItem<int>>((int year) {
            return DropdownMenuItem<int>(
              value: year,
              child: Text(year.toString(),
                  style: const TextStyle(
                    color: ConstValue.colorText,
                  )
              ),
            );
          }).toList(),
        ),
        IconButton(
          icon: Icon(Icons.chevron_right),
          color: Colors.white,
          onPressed: _incrementYear,
        ),
      ],
    );
  }

  Widget _selectMonth() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        IconButton(
          icon: Icon(Icons.chevron_left),
          color: Colors.white,
          onPressed: _decrementMonth,
        ),
        DropdownButton<int>(
          value: _selectedMonth,
          onChanged: (int? newValue) {
            setState(() {
              _selectedMonth = newValue!;
              _updateWebView();
            });
          },
          dropdownColor: Colors.grey[850],  // ドロップダウンの背景色を設定
          items: _monthList.map<DropdownMenuItem<int>>((int month) {
            return DropdownMenuItem<int>(
              value: month,
              child: Text(
                month.toString(),
                style: const TextStyle(color: Colors.white), // テキストの色を設定
              ),
            );
          }).toList(),
        ),
        IconButton(
          icon: Icon(Icons.chevron_right),
          color: Colors.white,
          onPressed: _incrementMonth,
        ),
      ],
    );
  }
}

lib/my_web_view_controller.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2025-02-20
///
library;

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';

class MyWebViewController {
  MyWebViewController() {
    //constructor
  }
  WebViewController controller() {
    late final PlatformWebViewControllerCreationParams params;
    if (WebViewPlatform.instance is WebKitWebViewPlatform) {
      params = WebKitWebViewControllerCreationParams(
        allowsInlineMediaPlayback: true,
        mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
      );
    } else {
      params = const PlatformWebViewControllerCreationParams();
    }
    final WebViewController controller = WebViewController.fromPlatformCreationParams(params);
    controller
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onUrlChange: (UrlChange change) {
            //debugPrint('url change to ${change.url}');
          },
        ),
      );
    controller.setBackgroundColor(const Color(0x00000000));
    if (controller.platform is AndroidWebViewController) {
      AndroidWebViewController.enableDebugging(true);
      (controller.platform as AndroidWebViewController).setMediaPlaybackRequiresUserGesture(false);
    }
    return controller;
  }
}

lib/setting.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;

import 'package:flutter/material.dart';

import 'package:lunarage/const_value.dart';
import 'package:lunarage/version_state.dart';
import 'package:lunarage/ad_mob.dart';

class SettingPage extends StatefulWidget {
  const SettingPage({super.key});
  @override
  State<SettingPage> createState() => _SettingPageState();
}

class _SettingPageState extends State<SettingPage> {
  final AdMob _adMob = AdMob(); //広告
  //ページ起動時に一度だけ実行される
  @override
  void initState() {
    super.initState();
    _adMob.load();
  }
  //ページ終了時に一度だけ実行される
  @override
  void dispose() {
    _adMob.dispose();
    super.dispose();
  }
  //ページ描画
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        elevation: 0,
        //設定キャンセルボタン
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: () {
            Navigator.of(context).pop(false); //falseを返す
          },
        ),
        title: Text('Information'),
        foregroundColor: const Color.fromRGBO(255,255,255,1),
        backgroundColor: ConstValue.colorSettingHeader,
        actions: [
          //設定OKボタン
          IconButton(
            icon: const Icon(Icons.check),
            onPressed: () async {
              if (!mounted) {
                return;
              }
              Navigator.of(context).pop(true);  //trueを返す
            },
          ),
        ],
      ),
      body: SafeArea(
        child: Column(children:[
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(20),
              child: Column(children: [
                Padding(
                  padding: const EdgeInsets.only(top: 24, left: 0, right: 0, bottom: 36),
                  child: SizedBox(
                    child: Text('version  ${VersionState.versionLoad()}',
                      style: const TextStyle(
                        fontSize: 10,
                      ),
                    ),
                  ),
                ),
              ]),
            ),
          ),
          Padding(
            padding: const EdgeInsets.only(top: 10, left: 0, right: 0, bottom: 0),
            child: SizedBox(
              width: double.infinity,
              child: _adMob.getAdBannerWidget(),
            ),
          ),
        ]),
      )
    );
  }
}

lib/version_state.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-01-27
///
library;

class VersionState {

  static String _version = '';

  //バージョンを記録
  static void versionSave(String str) {
    _version = str;
  }
  //バージョンを返す
  static String versionLoad() {
    return _version;
  }

}