diff --git a/.gitignore b/.gitignore index 5046a79..fed9019 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,10 @@ .classpath .project .settings/ -.vscode/ +.vscode/* +!.vscode/settings.json +!.vscode/launch.json +!.vscode/extensions.json # Flutter repo-specific /bin/cache/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..313fed3 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "Dart-Code.flutter", // Flutter support and debugger for Visual Studio Code. + "Dart-Code.dart-code", // Dart + "robert-brunhage.flutter-riverpod-snippets", // riverpod + // "alexisvt.flutter-snippets", // flutter widget snippets + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..91f6776 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,56 @@ +{ + // IntelliSense を使用して利用可能な属性を学べます。 + // 既存の属性の説明をホバーして表示します。 + // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + // launch.json作成時のテンプレート - START + // "configurations": [ + // { + // "name": "flutter_github_search", + // "request": "launch", + // "type": "dart" + // }, + // { + // "name": "flutter_github_search (profile mode)", + // "request": "launch", + // "type": "dart", + // "flutterMode": "profile" + // }, + // { + // "name": "flutter_github_search (release mode)", + // "request": "launch", + // "type": "dart", + // "flutterMode": "release" + // } + // ] + // launch.json作成時のテンプレート - END + "configurations": [ + { + "name": "Debug dev", + "request": "launch", + "type": "dart", + "flutterMode": "debug", + "args": [ + "--dart-define-from-file=dart_define/development.json" + ] + }, + { + "name": "Debug stg", + "request": "launch", + "type": "dart", + "flutterMode": "debug", + "args": [ + "--dart-define-from-file=dart_define/staging.json" + ] + }, + { + "name": "Debug prod", + "request": "launch", + "type": "dart", + "flutterMode": "debug", + "args": [ + "--dart-define-from-file=dart_define/production.json" + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..85c4bc6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,49 @@ +{ + // Flutterプロジェクトのバージョン管理のため、fvmを指定 -START + // https://fvm.app/docs/getting_started/configuration#option-1---automatic-switching-recommended + "dart.flutterSdkPath": ".fvm/flutter_sdk", + // Remove .fvm files from search + "search.exclude": { + "**/.fvm": true + }, + // Remove from file watching + "files.watcherExclude": { + "**/.fvm": true + }, + // Flutterプロジェクトのバージョン管理のため、fvmを指定 -END + "[dart]": { + // Automatically format code on save and during typing of certain characters + // (like `;` and `}`). + "editor.formatOnSave": true, + "editor.formatOnType": true, + // Draw a guide line at 80 characters, where Dart's formatting will wrap code. + "editor.rulers": [ + 80 + ], + // Disables built-in highlighting of words that match your selection. Without + // this, all instances of the selected text will be highlighted, interfering + // with Dart's ability to highlight only exact references to the selected variable. + "editor.selectionHighlight": false, + // By default, VS Code prevents code completion from popping open when in + // "snippet mode" (editing placeholders in inserted code). Setting this option + // to `false` stops that and allows completion to open as normal, as if you + // weren't in a snippet placeholder. + "editor.suggest.snippetsPreventQuickSuggestions": false, + // By default, VS Code will pre-select the most recently used item from code + // completion. This is usually not the most relevant item. + // + // "first" will always select top item + // "recentlyUsedByPrefix" will filter the recently used items based on the + // text immediately preceding where completion was invoked. + "editor.suggestSelection": "first", + // Allows pressing to complete snippets such as `for` even when the + // completion list is not visible. + "editor.tabCompletion": "onlySnippets", + // By default, VS Code will populate code completion with words found in the + // current file when a language service does not provide its own completions. + // This results in code completion suggesting words when editing comments and + // strings. This setting will prevent that. + "editor.wordBasedSuggestions": false, + }, + "dart.openDevTools": "flutter", +} \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index b606bbe..96e59b8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -51,6 +51,11 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + // applicationIdSuffix に {環境}.json で定義した flutterApplicationIdSuffix を指定 + // 注意点は、gradleの変数名(applicationIdSuffix)と {環境}.json で定義した変数名が同じだと設定が反映されないので注意! + applicationIdSuffix flutterApplicationIdSuffix + // string/app_nameを作成し、{環境}.json で定義した flutterAppName を指定 + resValue "string", "app_name", flutterAppName } buildTypes { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0b67c92..197bb42 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - flutter_github_search + $(flutterAppName) + CFBundleDisplayName + $(flutterAppName) CFBundlePackageType APPL CFBundleShortVersionString diff --git a/lib/data/api/github/github_api.dart b/lib/data/api/github/github_api.dart index 8990ffc..9bff27e 100644 --- a/lib/data/api/github/github_api.dart +++ b/lib/data/api/github/github_api.dart @@ -14,7 +14,7 @@ final Provider githubApiProvider = Provider((_) { }); class GithubApi { - static const _authority = 'api.github.com'; + static const _authority = String.fromEnvironment('githubApiDomain'); static const perPage = 20; final client = GithubApiHttpClient(); Future searchRepositories( diff --git a/lib/data/api/github/github_api_http_client.dart b/lib/data/api/github/github_api_http_client.dart index f854832..97c1321 100644 --- a/lib/data/api/github/github_api_http_client.dart +++ b/lib/data/api/github/github_api_http_client.dart @@ -10,10 +10,11 @@ import 'package:flutter_github_search/data/api/http_handler.dart'; import 'package:flutter_github_search/domain/exception/api_exceptions.dart'; class GithubApiHttpClient extends http.BaseClient { - // static const String _accessToken = "INPUT HERE TOKEN"; + static const String _accessToken = + String.fromEnvironment('githubAccessToken'); final Map _headers = { 'Accept': 'application/vnd.github.v3+json', - // 'Authorization': 'Bearer $_accessToken', + 'Authorization': 'Bearer $_accessToken', 'X-GitHub-Api-Version': '2022-11-28', }; final http.Client _client = http.Client(); diff --git a/lib/ui/environment_variable/environment_variable_screen.dart b/lib/ui/environment_variable/environment_variable_screen.dart new file mode 100644 index 0000000..0f398a9 --- /dev/null +++ b/lib/ui/environment_variable/environment_variable_screen.dart @@ -0,0 +1,47 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +class EnvironmentVariableScreen extends StatelessWidget { + const EnvironmentVariableScreen({super.key}); + @override + Widget build(BuildContext context) { + const flavor = String.fromEnvironment('flavor'); + const appName = String.fromEnvironment('flutterAppName'); + const applicationIdSuffix = + String.fromEnvironment('flutterApplicationIdSuffix'); + const githubAccessToken = String.fromEnvironment('githubAccessToken'); + const githubApiDomain = String.fromEnvironment('githubApiDomain'); + return Scaffold( + appBar: AppBar( + title: const Text('Env Variable'), + ), + body: SafeArea( + child: SingleChildScrollView( + child: Column( + children: const [ + ListTile( + leading: Text('flavor'), + title: Text(flavor), + ), + ListTile( + leading: Text('appName'), + title: Text(appName), + ), + ListTile( + leading: Text('applicationIdSuffix'), + title: Text(applicationIdSuffix), + ), + ListTile( + leading: Text('githubAccessToken'), + title: Text(githubAccessToken), + ), + ListTile( + leading: Text('githubApiDomain'), + title: Text(githubApiDomain), + ), + ], + ), + )), + ); + } +} diff --git a/lib/ui/home/home_screen.dart b/lib/ui/home/home_screen.dart index 11b1db6..f2383b6 100644 --- a/lib/ui/home/home_screen.dart +++ b/lib/ui/home/home_screen.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; // Project imports: +import 'package:flutter_github_search/ui/package_info/package_info_screen.dart'; import 'package:flutter_github_search/ui/search/pagination/search_paging_screen.dart'; import 'package:flutter_github_search/ui/search/search_screen.dart'; +import '../environment_variable/environment_variable_screen.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key, required this.title}); @@ -25,6 +27,26 @@ class HomeScreen extends StatelessWidget { child: Text( 'Paging compatible / non-compatible versions are available.'), ), + ListTile( + leading: const Icon(Icons.computer), + title: const Text("Environment Variable"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + const EnvironmentVariableScreen())); + }), + ListTile( + leading: const Icon(Icons.info), + title: const Text("Package Info"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + const PackageInfoScreen())); + }), ListTile( leading: const Icon(Icons.flutter_dash_outlined), title: const Text("Paging compatible"), diff --git a/lib/ui/package_info/package_info_screen.dart b/lib/ui/package_info/package_info_screen.dart new file mode 100644 index 0000000..f380aa5 --- /dev/null +++ b/lib/ui/package_info/package_info_screen.dart @@ -0,0 +1,67 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:package_info_plus/package_info_plus.dart'; + +/// see: https://pub.dev/packages/package_info_plus/example +class PackageInfoScreen extends StatefulWidget { + const PackageInfoScreen({Key? key}) : super(key: key); + + @override + State createState() => _PackageInfoScreenState(); +} + +class _PackageInfoScreenState extends State { + PackageInfo _packageInfo = PackageInfo( + appName: 'Unknown', + packageName: 'Unknown', + version: 'Unknown', + buildNumber: 'Unknown', + buildSignature: 'Unknown', + installerStore: 'Unknown', + ); + + @override + void initState() { + super.initState(); + _initPackageInfo(); + } + + Future _initPackageInfo() async { + final info = await PackageInfo.fromPlatform(); + setState(() { + _packageInfo = info; + }); + } + + Widget _infoTile(String title, String subtitle) { + return ListTile( + title: Text(title), + subtitle: Text(subtitle.isEmpty ? 'Not set' : subtitle), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Package Info'), + ), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _infoTile('App name', _packageInfo.appName), + _infoTile('Package name', _packageInfo.packageName), + _infoTile('App version', _packageInfo.version), + _infoTile('Build number', _packageInfo.buildNumber), + _infoTile('Build signature', _packageInfo.buildSignature), + _infoTile( + 'Installer store', + _packageInfo.installerStore ?? 'not available', + ), + ], + ), + ); + } +} diff --git a/lib/ui/search/pagination/search_paging_notifier.dart b/lib/ui/search/pagination/search_paging_notifier.dart index 813c8c2..b068f18 100644 --- a/lib/ui/search/pagination/search_paging_notifier.dart +++ b/lib/ui/search/pagination/search_paging_notifier.dart @@ -25,7 +25,7 @@ class SearchPagingStateNotifier extends StateNotifier { final GithubRepoRepository _repository; void searchRepositories( - {required String query, required int page, bool refresh = false}) async { + {required String query, int page = 1, bool refresh = false}) async { Logger().d('ando searchRepositories page: $page, refresh: $refresh'); final List repositories = (refresh) ? [] : state.repositories; diff --git a/lib/ui/search/pagination/search_paging_screen.dart b/lib/ui/search/pagination/search_paging_screen.dart index 772046d..0392b99 100644 --- a/lib/ui/search/pagination/search_paging_screen.dart +++ b/lib/ui/search/pagination/search_paging_screen.dart @@ -66,8 +66,7 @@ class _SearchPagingScreenState extends ConsumerState { onFieldSubmitted: (String value) { ref .read(searchPagingStateNotifierProvider.notifier) - .searchRepositories( - query: value, page: nextPageNo, refresh: true); + .searchRepositories(query: value, refresh: true); }, uiState: uiState), data: (_, nextPageNo) => _buildMainWidget( @@ -88,8 +87,7 @@ class _SearchPagingScreenState extends ConsumerState { onFieldSubmitted: (String value) { ref .read(searchPagingStateNotifierProvider.notifier) - .searchRepositories( - query: value, page: nextPageNo ?? 1, refresh: true); + .searchRepositories(query: value, refresh: true); }, uiState: uiState, ), @@ -113,8 +111,7 @@ class _SearchPagingScreenState extends ConsumerState { onFieldSubmitted: (String value) { ref .read(searchPagingStateNotifierProvider.notifier) - .searchRepositories( - query: value, page: nextPageNo ?? 1, refresh: true); + .searchRepositories(query: value, refresh: true); }, uiState: uiState, errorMessage: e.message, @@ -199,12 +196,41 @@ class _SearchPagingScreenState extends ConsumerState { }), ), onNotification: (notification) { + // WORKAROUND! + // ios/macosで動作 - START + // ios/macosだとOverscrollNotificationが呼ばれないので、notification.metrics.outOfRangeを使用 + final outOfRange = notification.metrics.outOfRange; + final extentAfter = notification.metrics.extentAfter; + final extentBefore = notification.metrics.extentBefore; + Logger().d('ando outOfRange $outOfRange'); + Logger().d('ando extentAfter $extentAfter'); + Logger().d('ando extentBefore $extentBefore'); + if (outOfRange == true && extentAfter <= 0) { + Logger().d('ando 下端のオーバースクロールの場合'); + if (uiState is Loading) { + } else { + if (uiState.nextPageNo != null) { + // 次ページがある場合のみ検索APIを叩く. + Logger().d('ando nextPageNo: ${uiState.nextPageNo}'); + ref + .read(searchPagingStateNotifierProvider.notifier) + .searchRepositories( + query: _textEditingController.text, + page: uiState.nextPageNo ?? 1); + } + } + } else if (outOfRange == true && extentBefore <= 0) { + Logger().d('ando 上端のオーバースクロールの場合'); + } + // ios/macosで動作 - END + // Androidで動作 - START if (notification is OverscrollNotification) { if (uiState is Loading) { } else { if (notification.overscroll > 0 && uiState.nextPageNo != null) { // 下端のオーバースクロール && 次ページがある場合のみ検索APIを叩く. + Logger().d('ando nextPageNo: ${uiState.nextPageNo}'); ref .read(searchPagingStateNotifierProvider.notifier) .searchRepositories( @@ -213,6 +239,7 @@ class _SearchPagingScreenState extends ConsumerState { } } } + // Androidで動作 - END return false; }, ); diff --git a/pubspec.yaml b/pubspec.yaml index 7d5935c..8071f25 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: json_annotation: ^4.8.0 logger: ^1.1.0 cached_network_image: ^3.2.3 + package_info_plus: ^3.0.3 dev_dependencies: flutter_test: