├── .fvm └── fvm_config.json ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── mk │ │ │ │ └── webfactory │ │ │ │ └── flutter_template │ │ │ │ ├── App.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── model │ │ │ │ └── TaskGroup.kt │ │ │ │ └── platform_comm │ │ │ │ ├── Platform.kt │ │ │ │ ├── PlatformComm.kt │ │ │ │ ├── ResultCallback.kt │ │ │ │ └── Subscription.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── test │ │ └── java │ │ └── mk.webfactory.flutter_template │ │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets └── l10n │ ├── app_en.arb │ └── app_mk.arb ├── diagrams ├── color_palette.png ├── dark_theme.jpg ├── high_lvl_diagram.png ├── high_lvl_diagram.vsdx ├── light_theme.png └── use_as_template.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ ├── production.xcscheme │ │ └── staging.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ ├── Contents.json │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── README.md │ │ │ ├── icons8-flutter-480.png │ │ │ ├── icons8-flutter-481.png │ │ │ └── icons8-flutter-482.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── PlatformCommunication │ │ ├── PlatformCommunication.swift │ │ └── PlatformConstants.swift │ ├── Runner-Bridging-Header.h │ └── Runner.entitlements └── build │ └── Runner.build │ └── Debug-staging-iphoneos │ └── Runner.build │ └── dgph ├── l10n.yaml ├── lib ├── app.dart ├── config │ ├── app_update_config.dart │ ├── firebase_config.dart │ ├── flavor_config.dart │ ├── logger_config.dart │ ├── network_constants.dart │ ├── post_app_config.dart │ └── pre_app_config.dart ├── data │ ├── data_not_found_exception.dart │ ├── item_converter.dart │ ├── mock │ │ ├── mock_tasks_api_service.dart │ │ ├── mock_user_api_service.dart │ │ └── tasks_dummy_data.dart │ └── repository │ │ └── tasks │ │ ├── tasks_cache_data_source.dart │ │ ├── tasks_data_source.dart │ │ ├── tasks_remote_data_source.dart │ │ └── tasks_repository.dart ├── di │ ├── service_locator.dart │ ├── user_scope.dart │ └── user_scope_hook.dart ├── feature │ ├── auth │ │ ├── login │ │ │ ├── bloc │ │ │ │ ├── login_cubit.dart │ │ │ │ └── login_state.dart │ │ │ └── ui │ │ │ │ ├── login_page.dart │ │ │ │ └── login_view.dart │ │ ├── router │ │ │ ├── auth_nav_state.dart │ │ │ ├── auth_router.dart │ │ │ └── auth_router_delegate.dart │ │ └── signup │ │ │ ├── bloc │ │ │ ├── signup_cubit.dart │ │ │ └── signup_state.dart │ │ │ └── ui │ │ │ ├── password │ │ │ ├── password_page.dart │ │ │ └── password_view.dart │ │ │ └── username │ │ │ ├── username_page.dart │ │ │ └── username_view.dart │ ├── force_update │ │ ├── force_update_alert.dart │ │ ├── force_update_exception.dart │ │ ├── force_update_handler.dart │ │ └── ui │ │ │ ├── force_update_page.dart │ │ │ └── force_update_view.dart │ ├── home │ │ ├── create_task │ │ │ ├── bloc │ │ │ │ ├── create_task_cubit.dart │ │ │ │ └── create_task_state.dart │ │ │ └── ui │ │ │ │ └── create_task_view.dart │ │ ├── router │ │ │ ├── home_nav_state.dart │ │ │ ├── home_router.dart │ │ │ └── home_router_delegate.dart │ │ ├── task_detail │ │ │ └── ui │ │ │ │ ├── task_detail_page.dart │ │ │ │ └── task_detail_view.dart │ │ └── task_list │ │ │ ├── bloc │ │ │ ├── task_list_bloc.dart │ │ │ ├── task_list_event.dart │ │ │ └── task_list_state.dart │ │ │ └── ui │ │ │ ├── task_list_page.dart │ │ │ └── task_list_view.dart │ ├── loading │ │ └── ui │ │ │ ├── circular_progress_indicator.dart │ │ │ └── loading_page.dart │ └── settings │ │ ├── preferences_helper.dart │ │ └── ui │ │ ├── settings_page.dart │ │ ├── settings_view.dart │ │ └── widget │ │ ├── settings_header_widget.dart │ │ ├── settings_language_icon_widget.dart │ │ ├── settings_language_widget.dart │ │ └── settings_theme_switch_widget.dart ├── log │ ├── abstract_logger.dart │ ├── bloc_events_logger.dart │ ├── console_logger.dart │ ├── file_logger.dart │ ├── filtered_logger.dart │ ├── firebase_logger.dart │ ├── log.dart │ ├── multi_logger.dart │ └── stub_logger.dart ├── main_dev.dart ├── main_mock.dart ├── main_production.dart ├── main_staging.dart ├── model │ ├── task │ │ ├── api │ │ │ ├── create_task.dart │ │ │ ├── create_task.g.dart │ │ │ ├── create_task_group.dart │ │ │ └── create_task_group.g.dart │ │ ├── task.dart │ │ ├── task.g.dart │ │ ├── task_event.dart │ │ ├── task_group.dart │ │ ├── task_group.g.dart │ │ └── task_status.dart │ └── user │ │ ├── credentials.dart │ │ ├── credentials.g.dart │ │ ├── refresh_token.dart │ │ ├── refresh_token.g.dart │ │ ├── user.dart │ │ ├── user.g.dart │ │ ├── user_credentials.dart │ │ └── user_credentials.g.dart ├── network │ ├── chopper │ │ ├── authenticator │ │ │ ├── authenticator_helper_jwt.dart │ │ │ └── refresh_token_authenticator.dart │ │ ├── converters │ │ │ ├── json_convert_adapter.dart │ │ │ ├── json_type_converter.dart │ │ │ ├── json_type_converter_builder.dart │ │ │ ├── json_type_converter_provider.dart │ │ │ ├── map_converter.dart │ │ │ └── response_to_type_converter.dart │ │ ├── generated │ │ │ ├── chopper_tasks_api_service.chopper.dart │ │ │ ├── chopper_tasks_api_service.dart │ │ │ ├── chopper_user_api_service.chopper.dart │ │ │ ├── chopper_user_api_service.dart │ │ │ ├── chopper_user_auth_api_service.chopper.dart │ │ │ └── chopper_user_auth_api_service.dart │ │ ├── http_api_service_provider.dart │ │ └── interceptors │ │ │ ├── auth_interceptor.dart │ │ │ ├── error_interceptor.dart │ │ │ ├── http_logger_interceptor.dart │ │ │ ├── language_interceptor.dart │ │ │ └── version_interceptor.dart │ ├── tasks_api_service.dart │ ├── user_api_service.dart │ ├── user_auth_api_service.dart │ └── util │ │ ├── http_exception_code.dart │ │ ├── http_util.dart │ │ └── network_utils.dart ├── notifications │ ├── data │ │ ├── data_notification_consumer.dart │ │ ├── data_notification_consumer_factory.dart │ │ ├── filter │ │ │ └── message_filter.dart │ │ ├── handler │ │ │ └── message_handler.dart │ │ ├── model │ │ │ ├── message.dart │ │ │ ├── message.g.dart │ │ │ ├── message_serializer.dart │ │ │ ├── message_type.dart │ │ │ └── remote_message_extension.dart │ │ ├── notification_consumer.dart │ │ └── parser │ │ │ ├── base_message_type_parser.dart │ │ │ └── message_parser.dart │ ├── fcm │ │ ├── fcm_notifications_listener.dart │ │ └── firebase_user_hook.dart │ └── local │ │ ├── android_notification_channels.dart │ │ ├── android_notification_details.dart │ │ ├── android_notification_ids.dart │ │ ├── local_notification_manager.dart │ │ ├── local_notification_manager_aw_adapter.dart │ │ └── local_notification_manager_aw_channels.dart ├── platform_comm │ ├── app_platform_methods.dart │ ├── platform_callback.dart │ └── platform_comm.dart ├── resources │ ├── colors │ │ └── color_palette.dart │ ├── localization │ │ ├── l10n.dart │ │ └── localization_notifier.dart │ ├── styles │ │ └── text_styles.dart │ └── theme │ │ ├── app_theme.dart │ │ └── theme_change_notifier.dart ├── routing │ ├── app_nav_state.dart │ ├── app_router_delegate.dart │ ├── navigation_observer.dart │ └── no_animation_transition_delegate.dart ├── user │ ├── unauthorized_user_exception.dart │ ├── unauthorized_user_handler.dart │ ├── user_event_hook.dart │ └── user_manager.dart ├── util │ ├── app_lifecycle_observer.dart │ ├── check_result.dart │ ├── collections_util.dart │ ├── date_time_util.dart │ ├── developer_util.dart │ ├── either.dart │ ├── enum_util.dart │ ├── input_buffer.dart │ ├── nullable_util.dart │ ├── screen_size_util.dart │ ├── string_util.dart │ ├── subscription.dart │ ├── text_util.dart │ ├── updates_stream.dart │ └── validations.dart └── widgets │ ├── alert_dialog.dart │ ├── basic_web_view.dart │ ├── debug_overlay.dart │ ├── flavor_banner.dart │ ├── keyboard_dismissal_container.dart │ ├── loading_overlay.dart │ ├── modal_sheet_presentation.dart │ └── transparent_appbar.dart ├── pubspec.lock ├── pubspec.yaml ├── test ├── dart_strange_runtime_type_test.dart ├── dart_type_test.dart ├── data │ └── repository │ │ └── tasks │ │ ├── tasks_cache_data_source_test.dart │ │ ├── tasks_data_source_base_test.dart │ │ ├── tasks_remote_data_source_test.dart │ │ ├── tasks_repository_test.dart │ │ └── tasks_stub_data_source.dart ├── log │ ├── console_logger_test.dart │ ├── console_logger_test.mocks.dart │ ├── file_logger_test.dart │ ├── filtered_logger_test.dart │ ├── firebase_logger_test.dart │ ├── firebase_logger_test.mocks.dart │ └── multi_logger_test.dart ├── model │ └── user │ │ └── user_credentials_extension_test.dart ├── network │ ├── chopper │ │ ├── authenticator │ │ │ ├── authenticator_helper_jwt_test.dart │ │ │ └── authenticator_helper_jwt_test.mocks.dart │ │ ├── converters │ │ │ ├── date_time_parse_test.dart │ │ │ └── user_parse_test.dart │ │ └── interceptors │ │ │ ├── language_interceptor_test.dart │ │ │ ├── version_interceptor_test.dart │ │ │ └── version_interceptor_test.mocks.dart │ ├── mock_client_handler.dart │ ├── network_test_helper.dart │ ├── tasks_api_service_test.dart │ ├── tasks_api_service_test.mocks.dart │ └── util │ │ ├── network_utils_test.dart │ │ └── network_utils_test.mocks.dart ├── platform_comm │ ├── platform_comm_test.dart │ ├── platform_comm_test.mocks.dart │ └── test_method_channel.dart ├── user │ ├── test_user_manager.dart │ ├── user_manager_test.dart │ └── user_manager_test.mocks.dart └── util │ ├── collections_util_test.dart │ ├── date_time_test.dart │ ├── either_test.dart │ ├── enum_util_test.dart │ ├── input_buffer_test.dart │ ├── nullable_util_test.dart │ ├── string_util_test.dart │ ├── subscription_test.dart │ └── updates_stream_test.dart └── test_driver ├── app.dart ├── app_test.dart ├── main_test.dart └── platform_comm_test_widget.dart /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "stable" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Google services config files for iOS and Android 2 | # todo delete for private repos 3 | **/google-services.json 4 | **/GoogleService-Info.plist 5 | 6 | # Miscellaneous 7 | *.class 8 | *.log 9 | *.pyc 10 | *.swp 11 | .DS_Store 12 | .atom/ 13 | .buildlog/ 14 | .history 15 | .svn/ 16 | 17 | # IntelliJ related 18 | *.iml 19 | *.ipr 20 | *.iws 21 | .idea/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | # Android related 37 | **/android/**/gradle-wrapper.jar 38 | **/android/.gradle 39 | **/android/captures/ 40 | **/android/gradlew 41 | **/android/gradlew.bat 42 | **/android/local.properties 43 | **/android/**/GeneratedPluginRegistrant.java 44 | 45 | # Symbolication related 46 | app.*.symbols 47 | 48 | # Obfuscation related 49 | app.*.map.json 50 | # iOS/XCode related 51 | **/ios/**/*.mode1v3 52 | **/ios/**/*.mode2v3 53 | **/ios/**/*.moved-aside 54 | **/ios/**/*.pbxuser 55 | **/ios/**/*.perspectivev3 56 | **/ios/**/*sync/ 57 | **/ios/**/.sconsign.dblite 58 | **/ios/**/.tags* 59 | **/ios/**/.vagrant/ 60 | **/ios/**/DerivedData/ 61 | **/ios/**/Icon? 62 | **/ios/**/Pods/ 63 | **/ios/**/.symlinks/ 64 | **/ios/**/profile 65 | **/ios/**/xcuserdata 66 | **/ios/.generated/ 67 | **/ios/Flutter/App.framework 68 | **/ios/Flutter/Flutter.framework 69 | **/ios/Flutter/Generated.xcconfig 70 | **/ios/Flutter/app.flx 71 | **/ios/Flutter/app.zip 72 | **/ios/Flutter/flutter_assets/ 73 | **/ios/ServiceDefinitions.json 74 | **/ios/Runner/GeneratedPluginRegistrant.* 75 | 76 | # Exceptions to above rules. 77 | !**/ios/**/default.mode1v3 78 | !**/ios/**/default.mode2v3 79 | !**/ios/**/default.pbxuser 80 | !**/ios/**/default.perspectivev3 81 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 82 | android/key 83 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: b1395592de68cc8ac4522094ae59956dd21a91db 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Web Factory LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/mk/webfactory/flutter_template/App.kt: -------------------------------------------------------------------------------- 1 | package mk.webfactory.flutter_template; 2 | 3 | import io.flutter.app.FlutterApplication 4 | 5 | class App : FlutterApplication() {} -------------------------------------------------------------------------------- /android/app/src/main/kotlin/mk/webfactory/flutter_template/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package mk.webfactory.flutter_template 2 | 3 | import android.content.Context 4 | import io.flutter.embedding.android.FlutterActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import io.flutter.embedding.engine.FlutterEngineCache 7 | import mk.webfactory.flutter_template.platform_comm.Platform 8 | import mk.webfactory.flutter_template.platform_comm.PlatformComm 9 | 10 | const val MAIN_ENGINE_CACHE_ID = "main_engine_cache" 11 | 12 | class MainActivity : FlutterActivity() { 13 | 14 | override fun configureFlutterEngine(flutterEngine: FlutterEngine) { 15 | super.configureFlutterEngine(flutterEngine) 16 | if (BuildConfig.USE_CACHED_FLUTTER_ENGINE) { 17 | FlutterEngineCache.getInstance().put(MAIN_ENGINE_CACHE_ID, flutterEngine) 18 | } 19 | PlatformComm.configure(flutterEngine) 20 | .run { Platform.log("MainActivity: configureFlutterEngine") } 21 | } 22 | 23 | override fun shouldDestroyEngineWithHost(): Boolean = !BuildConfig.USE_CACHED_FLUTTER_ENGINE 24 | 25 | override fun provideFlutterEngine(context: Context): FlutterEngine? { 26 | return if (BuildConfig.USE_CACHED_FLUTTER_ENGINE) 27 | FlutterEngineCache.getInstance().get(MAIN_ENGINE_CACHE_ID) else null 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/mk/webfactory/flutter_template/model/TaskGroup.kt: -------------------------------------------------------------------------------- 1 | package mk.webfactory.flutter_template.model 2 | 3 | data class TaskGroup( 4 | val id: String, 5 | val name: String, 6 | val taskIds: List 7 | ) 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/mk/webfactory/flutter_template/platform_comm/Platform.kt: -------------------------------------------------------------------------------- 1 | package mk.webfactory.flutter_template.platform_comm 2 | 3 | import com.google.gson.Gson 4 | import mk.webfactory.flutter_template.BuildConfig 5 | import mk.webfactory.flutter_template.model.TaskGroup 6 | 7 | const val nativeLogs = "nativeLogs" 8 | const val platformTestMethod = "platformTestMethod" 9 | const val platformTestMethod2 = "platformTestMethod2" 10 | 11 | /** 12 | * Convenience app platform methods for communication w/ the flutter code. 13 | */ 14 | object Platform { 15 | private val jsonConverter: Gson = Gson() 16 | 17 | // Each time the flutter engine is configured this instance will be renewed 18 | var platformComm: PlatformComm? = null 19 | set(value) { 20 | field = value 21 | if (value != null) { 22 | initPlatformComm(value) 23 | } 24 | } 25 | 26 | private fun initPlatformComm(platformComm: PlatformComm) { 27 | if (BuildConfig.DEBUG) { 28 | // For testing only. 29 | platformComm.listenMethod(platformTestMethod, fun(echoMessage) = echoMessage) 30 | platformComm.listenMethod( 31 | method = platformTestMethod2, 32 | callback = fun(echoObject) = jsonConverter.toJson(echoObject), 33 | deserializeParams = fun(resultJson) = 34 | jsonConverter.fromJson(resultJson as String, TaskGroup::class.java)) 35 | } 36 | } 37 | 38 | fun log(message: String) { 39 | platformComm?.invokeProcedure(nativeLogs, message) 40 | } 41 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/mk/webfactory/flutter_template/platform_comm/ResultCallback.kt: -------------------------------------------------------------------------------- 1 | package mk.webfactory.flutter_template.platform_comm 2 | 3 | import androidx.annotation.UiThread 4 | 5 | interface ResultCallback { 6 | 7 | /** Handles a successful result. */ 8 | @UiThread 9 | fun success(result: R) 10 | 11 | /** 12 | * Handles an error result. 13 | * 14 | * @param errorMessage A human-readable error message String, possibly null. 15 | * @param errorDetails Error details, possibly null. 16 | */ 17 | @UiThread 18 | fun error(errorMessage: String?, errorDetails: Any?) 19 | 20 | /** Handles a call to an unimplemented method. */ 21 | @UiThread 22 | fun notImplemented() 23 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/mk/webfactory/flutter_template/platform_comm/Subscription.kt: -------------------------------------------------------------------------------- 1 | package mk.webfactory.flutter_template.platform_comm; 2 | 3 | /** 4 | * Service subscription. Call [cancel] to unsubscribe. 5 | */ 6 | class Subscription(private var cancel: (() -> Unit)?) { 7 | 8 | /** 9 | * Cancels the subscription. 10 | * 11 | * Can be called once. Subsequent calls have no effect. 12 | */ 13 | fun cancel() { 14 | cancel?.invoke() 15 | cancel = null 16 | } 17 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/test/java/mk.webfactory.flutter_template/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package mk.webfactory.flutter_template 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.4.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:4.3.15' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /assets/l10n/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "helloWorld": "Hello World!", 3 | "@helloWorld": { 4 | "description": "Newborn programmer greeting" 5 | }, 6 | 7 | "helloPlaceholders": "Hello {name}!", 8 | "@helloPlaceholders": { 9 | "description": "Greeting with placeholders example", 10 | "placeholders": { 11 | "name": { 12 | "type": "String" 13 | } 14 | } 15 | }, 16 | 17 | "next": "Next", 18 | "submit": "Submit", 19 | "remove": "Remove", 20 | "cancel": "Cancel", 21 | "ok": "OK", 22 | "save": "Save", 23 | "retry": "Retry", 24 | "send": "Send", 25 | "start": "Start", 26 | "reset": "Reset", 27 | 28 | "logout": "Logout", 29 | 30 | "no_network": "You're offline", 31 | "timeout": "Sorry, the network request timed out. Try again.", 32 | "unauthorized": "Error: Unauthorized user", 33 | "default_error": "Ops. Something went wrong.", 34 | "session_expired": "Error: Session expired", 35 | "error_title": "Error", 36 | 37 | "update_alert_title": "App update required", 38 | "update_alert_message_ios": "Please update the app to the latest version", 39 | "update_alert_message_android": "Please update the app to the latest version", 40 | "update_cta": "Update app", 41 | 42 | "task_list_title": "Tasks", 43 | "task_list_create_new": "Create new task", 44 | "task_list_no_tasks_message": "You have no tasks. Yay! Do nothing!", 45 | "task_list_error_loading_tasks": "Error loading tasks.", 46 | "task_list_error_general": "Ops! An error has occurred.", 47 | 48 | "theme": "Theme", 49 | "dark_theme": "Dark theme", 50 | "settings": "Settings", 51 | "language": "Language" 52 | } 53 | -------------------------------------------------------------------------------- /assets/l10n/app_mk.arb: -------------------------------------------------------------------------------- 1 | { 2 | "helloWorld": "Здраво!", 3 | 4 | "helloPlaceholders": "Здраво {name}!", 5 | 6 | "next": "Следно", 7 | "submit": "Внеси", 8 | "remove": "Избриши", 9 | "cancel": "Откажи", 10 | "ok": "ОК", 11 | "save": "Зачувај", 12 | "retry": "Пробај повторно", 13 | "send": "Прати", 14 | "start": "Почни", 15 | "reset": "Ресетирај", 16 | 17 | "logout": "Одјави се", 18 | 19 | "no_network": "Немате интернет конекција", 20 | "timeout": "Извниете, времето за конекција истече. Пробај повторно.", 21 | "unauthorized": "Грешка: Не најавен корисник", 22 | "default_error": "Се случи грешка", 23 | "session_expired": "Грешка: Сесијата истече", 24 | "error_title": "Грешка", 25 | 26 | "update_alert_title": "Потребно е ажурирање на апликацијата", 27 | "update_alert_message_ios": "Ве молиме инсталирајте ја последната верзија од апликацијата", 28 | "update_alert_message_android": "Ве молиме инсталирајте ја последната верзија од апликацијата", 29 | "update_cta": "Ажурирај апликација", 30 | 31 | "task_list_title": "Задачи", 32 | "task_list_create_new": "Креирај нова задача", 33 | "task_list_no_tasks_message": "Нема задачи. Супер, не прави ништо!", 34 | "task_list_error_loading_tasks": "Грешка при вчитување задачи.", 35 | "task_list_error_general": "Се случи грешка.", 36 | 37 | "theme": "Тема", 38 | "dark_theme": "Темна тема", 39 | "settings": "Поставувања", 40 | "language": "Јазик" 41 | 42 | } 43 | -------------------------------------------------------------------------------- /diagrams/color_palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/diagrams/color_palette.png -------------------------------------------------------------------------------- /diagrams/dark_theme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/diagrams/dark_theme.jpg -------------------------------------------------------------------------------- /diagrams/high_lvl_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/diagrams/high_lvl_diagram.png -------------------------------------------------------------------------------- /diagrams/high_lvl_diagram.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/diagrams/high_lvl_diagram.vsdx -------------------------------------------------------------------------------- /diagrams/light_theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/diagrams/light_theme.png -------------------------------------------------------------------------------- /diagrams/use_as_template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/diagrams/use_as_template.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | target.build_configurations.each do |config| 41 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | import Firebase 4 | import FirebaseMessaging 5 | 6 | @UIApplicationMain 7 | @objc class AppDelegate: FlutterAppDelegate { 8 | override func application( 9 | _ application: UIApplication, 10 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 11 | ) -> Bool { 12 | FirebaseApp.configure() 13 | 14 | 15 | if #available(iOS 10.0, *) { 16 | UNUserNotificationCenter.current().delegate = self 17 | } 18 | 19 | _ = PlatformCommunication.shared 20 | 21 | GeneratedPluginRegistrant.register(with: self) 22 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 23 | } 24 | 25 | override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { 26 | 27 | Messaging.messaging().apnsToken = deviceToken 28 | super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) 29 | } 30 | 31 | override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Swift.Void) { 32 | 33 | Messaging.messaging().appDidReceiveMessage(userInfo) 34 | super.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler) 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icons8-flutter-482.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icons8-flutter-481.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "icons8-flutter-480.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/icons8-flutter-480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/LaunchImage.imageset/icons8-flutter-480.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/icons8-flutter-481.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/LaunchImage.imageset/icons8-flutter-481.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/icons8-flutter-482.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfactorymk/flutter-template/b0da529bc1b5e35c8ad67fdf7a67a4520aea73bd/ios/Runner/Assets.xcassets/LaunchImage.imageset/icons8-flutter-482.png -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/PlatformCommunication/PlatformCommunication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlatformCommunication.swift 3 | // Runner 4 | // 5 | // Created by WF | Gordana Badarovska on 11.5.21. 6 | // 7 | 8 | import Foundation 9 | 10 | class PlatformCommunication { 11 | static let shared = PlatformCommunication() 12 | final var methodChannel: FlutterMethodChannel 13 | 14 | init() { 15 | let appDelegate = UIApplication.shared.delegate as? AppDelegate 16 | let window = appDelegate?.window 17 | 18 | let controller : FlutterViewController = window?.rootViewController as! FlutterViewController 19 | 20 | methodChannel = FlutterMethodChannel(name: Constants.Channel.name, 21 | binaryMessenger: controller.binaryMessenger) 22 | methodChannel.setMethodCallHandler({ 23 | [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in 24 | 25 | switch call.method { 26 | case Constants.Method.platformTestMethod: 27 | self?.onTestMethod(call: call) 28 | case Constants.Method.platformTestMethod2: 29 | self?.onTestMethod2(call: call) 30 | default: 31 | result(FlutterMethodNotImplemented) 32 | } 33 | }) 34 | } 35 | 36 | func onTestMethod(call: FlutterMethodCall) { 37 | methodChannel.invokeMethod(Constants.Method.platformTestMethod, arguments: call.arguments) 38 | } 39 | 40 | func onTestMethod2(call: FlutterMethodCall) { 41 | methodChannel.invokeMethod(Constants.Method.platformTestMethod2, arguments: call.arguments) 42 | } 43 | 44 | func logUpdate(log: String) { 45 | methodChannel.invokeMethod(Constants.Method.nativeLogs, arguments: log) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ios/Runner/PlatformCommunication/PlatformConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlatformConstants.swift 3 | // Runner 4 | // 5 | // Created by WF | Gordana Badarovska on 11.5.21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Constants { 11 | struct Channel { 12 | static let name = "com.my-app.package-name.general" 13 | } 14 | 15 | struct Method { 16 | // For testing only 17 | static let platformTestMethod = "platformTestMethod" 18 | static let platformTestMethod2 = "platformTestMethod2" 19 | static let nativeLogs = "nativeLogs" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/build/Runner.build/Debug-staging-iphoneos/Runner.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Oct 5 202123:29:11/Userswf-dimitarzabaznoskiProjectsFlutterProjectsflutter-templateios -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: assets/l10n 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart 4 | -------------------------------------------------------------------------------- /lib/config/app_update_config.dart: -------------------------------------------------------------------------------- 1 | const APP_STORE_URL = 'https://apps.apple.com/app/exampleApp'; //todo change 2 | const PLAY_STORE_URL = 'market://details?id=example.com'; //todo change 3 | -------------------------------------------------------------------------------- /lib/config/firebase_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:firebase_core/firebase_core.dart'; 4 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 5 | import 'package:firebase_messaging/firebase_messaging.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:flutter_template/config/flavor_config.dart'; 8 | import 'package:flutter_template/log/log.dart'; 9 | 10 | const String apnsTokenKey = 'apns-device-token'; 11 | const String fcmTokenKey = 'firebase-device-token'; 12 | const String voipTokenKey = 'voip-device-token'; 13 | 14 | //todo decide when you need firebase in your project 15 | bool shouldConfigureFirebase() => 16 | FlavorConfig.isInitialized() && 17 | (FlavorConfig.isStaging() || FlavorConfig.isProduction()); 18 | 19 | Future configureFirebase() async { 20 | if (!shouldConfigureFirebase()) { 21 | return; 22 | } 23 | await Firebase.initializeApp(); 24 | await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled( 25 | kDebugMode ? false : true); //todo crashlytics in debug mode? 26 | FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError; 27 | } 28 | 29 | Future logFirebaseToken() async { 30 | final token = await FirebaseMessaging.instance.getToken(); 31 | Log.d('FirebaseMessaging - Token: $token'); 32 | } 33 | 34 | R? runZonedGuardedWithErrorHandler(R Function() body) => 35 | runZonedGuarded(body, (error, stack) { 36 | if (shouldConfigureFirebase()) { 37 | FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /lib/config/flavor_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/util/enum_util.dart'; 2 | 3 | enum Flavor { MOCK, DEV, STAGING, PRODUCTION } 4 | 5 | /// App specific flavor values. 6 | class FlavorValues { 7 | final String baseUrlApi; 8 | 9 | //todo add flavor specific values here 10 | 11 | FlavorValues({ 12 | required this.baseUrlApi, 13 | }); 14 | } 15 | 16 | /// Flavor static configuration. Use it to access flavor specific settings. 17 | abstract class FlavorConfig { 18 | static Flavor? _flavor; 19 | static String? _flavorName; 20 | static FlavorValues? _values; 21 | 22 | /// Sets the flavor configuration. 23 | /// Should be called just when the app starts before any access calls. 24 | static void set(Flavor flavor, FlavorValues values) { 25 | _flavor = flavor; 26 | _flavorName = enumToString(flavor); 27 | _values = values; 28 | } 29 | 30 | static bool isInitialized() => _flavor != null; //in tests it's not 31 | 32 | static bool isMock() => _flavor! == Flavor.MOCK; 33 | 34 | static bool isDev() => _flavor! == Flavor.DEV; 35 | 36 | static bool isStaging() => _flavor! == Flavor.STAGING; 37 | 38 | static bool isProduction() => _flavor! == Flavor.PRODUCTION; 39 | 40 | static String get flavorName => _flavorName!; 41 | 42 | static FlavorValues get values => _values!; 43 | } 44 | -------------------------------------------------------------------------------- /lib/config/logger_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter_template/config/firebase_config.dart'; 3 | import 'package:flutter_template/log/console_logger.dart'; 4 | import 'package:flutter_template/log/file_logger.dart'; 5 | import 'package:flutter_template/log/filtered_logger.dart'; 6 | import 'package:flutter_template/log/firebase_logger.dart'; 7 | import 'package:flutter_template/log/log.dart'; 8 | import 'package:flutter_template/log/multi_logger.dart'; 9 | 10 | /// App specific logging setup. 11 | /// 12 | /// This setup configuration will: 13 | /// - use console logger except in production 14 | /// - use file logger for bug reports except in production 15 | /// - use firebase logs, always, in production too 16 | /// 17 | /// todo tailor this setup to your needs 18 | /// 19 | void initLogger() { 20 | Log.logger = MultiLogger([ 21 | ConsoleLogger.create().makeFiltered(noLogsInProductionOrTests()), 22 | FileLogger.instance().makeFiltered(noLogsInProductionOrTests()), 23 | if (shouldConfigureFirebase()) 24 | FirebaseLogger.instance().makeFiltered(noLogsInTests()), 25 | ]); 26 | 27 | if (kDebugMode && shouldConfigureFirebase()) { 28 | logFirebaseToken(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/config/network_constants.dart: -------------------------------------------------------------------------------- 1 | const int TIMEOUT = 25; 2 | 3 | const String baseUrlDev = 'https://dev.example.com'; 4 | const String baseUrlStage = 'https://stage.example.com'; 5 | const String baseUrlProd = 'https://example.com'; 6 | 7 | const String apiVersion = '/v1'; 8 | const String apiPrefix = '/api'; 9 | 10 | const String webPrefix = '/web'; 11 | 12 | const String webFAQ = "/faq"; 13 | const String webPrivacyPolicy = "/privacy_policy"; 14 | const String webTermsAndConditions = "/terms_and_conditions"; 15 | 16 | -------------------------------------------------------------------------------- /lib/config/post_app_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/config/flavor_config.dart'; 2 | import 'package:flutter_template/di/service_locator.dart'; 3 | import 'package:flutter_template/log/log.dart'; 4 | import 'package:flutter_template/model/task/task_group.dart'; 5 | import 'package:flutter_template/platform_comm/platform_comm.dart'; 6 | 7 | /// Configuration that needs to be done after the Flutter app starts goes here. 8 | /// 9 | /// To minimize the app loading time keep this setup fast and simple. 10 | void postAppConfig() { 11 | _testPlatformCommunication(); 12 | } 13 | 14 | void _testPlatformCommunication(){ 15 | if (!FlavorConfig.isProduction()) { 16 | serviceLocator.get(instanceName: buildVersionKey); 17 | serviceLocator 18 | .get() 19 | .echoMessage('echo') 20 | .catchError((error) => 'Test platform method error: $error') 21 | .then((backEcho) => Log.d("Test message 'echo' - '$backEcho'")); 22 | serviceLocator 23 | .get() 24 | .echoObject(TaskGroup('TG-id', 'Test group', List.of(['1', '2']))) 25 | .then((backEcho) => Log.d("Test message TaskGroup - '$backEcho'")) 26 | .catchError((error) => Log.e('Test platform method err.: $error')); 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /lib/config/pre_app_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_template/config/firebase_config.dart'; 3 | import 'package:flutter_template/config/logger_config.dart'; 4 | import 'package:flutter_template/di/service_locator.dart'; 5 | import 'package:flutter_template/di/service_locator.dart' as serviceLocatorConf; 6 | import 'package:flutter_template/feature/settings/preferences_helper.dart'; 7 | import 'package:flutter_template/notifications/local/local_notification_manager.dart'; 8 | import 'package:flutter_template/user/user_manager.dart'; 9 | 10 | /// Configuration that needs to be done before the Flutter app starts goes here. 11 | /// 12 | /// To minimize the app loading time keep this setup fast and simple. 13 | Future preAppConfig() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | await configureFirebase(); 16 | initLogger(); 17 | await serviceLocatorConf.setupGlobalDependencies(); 18 | // await serviceLocator.get().init(); //todo uncomment for local notifications 19 | await serviceLocator.get().init(); 20 | await serviceLocator.get().init(); 21 | } 22 | -------------------------------------------------------------------------------- /lib/data/data_not_found_exception.dart: -------------------------------------------------------------------------------- 1 | class DataNotFoundException implements Exception { 2 | final String? message; 3 | 4 | DataNotFoundException([this.message]); 5 | 6 | @override 7 | String toString() { 8 | return 'DataNotFoundException{message: $message}'; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/data/item_converter.dart: -------------------------------------------------------------------------------- 1 | typedef dynamic Serialize(E item); 2 | 3 | typedef E Deserialize(dynamic data); 4 | -------------------------------------------------------------------------------- /lib/data/mock/mock_user_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/model/user/credentials.dart'; 2 | import 'package:flutter_template/model/user/refresh_token.dart'; 3 | import 'package:flutter_template/model/user/user.dart'; 4 | import 'package:flutter_template/network/user_api_service.dart'; 5 | 6 | const String mockToken = 7 | 'eyJhbGciOiJIUzI1NiJ9.eyJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJb' 8 | 'lVzZSIsImV4cCI6MTcxNDIyMDYwMSwiaWF0IjoxNjE5NTI2MjAxfQ.yjTgXqiqGH3F-ycq' 9 | '2I3Ec-v3l0mzVV8Rg_RijsR50do'; 10 | 11 | class MockUserApiService implements UserApiService { 12 | Future signUp(User user) => Future.delayed(Duration(seconds: 2)); 13 | 14 | Future login(String username, String password) => Future.delayed( 15 | Duration(seconds: 2), 16 | () => Credentials( 17 | mockToken, 18 | RefreshToken( 19 | mockToken, 20 | DateTime.now() 21 | .add(Duration(days: 1500)) 22 | .millisecondsSinceEpoch))); 23 | 24 | @override 25 | Future getUserProfile({String? authHeader}) => Future.value(User( 26 | id: "1", 27 | email: "user@email.com", 28 | firstName: "First", 29 | lastName: "Last", 30 | dateOfBirth: DateTime.now())); 31 | 32 | @override 33 | Future addNotificationsToken(String token) => Future.value(); 34 | 35 | @override 36 | Future deactivate() => Future.value(); 37 | 38 | @override 39 | Future logout() => Future.value(); 40 | 41 | @override 42 | Future resetPassword(String email) => Future.value(); 43 | 44 | @override 45 | Future updateUserProfile(User user) => Future.value(user); 46 | } 47 | -------------------------------------------------------------------------------- /lib/data/mock/tasks_dummy_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/model/task/task.dart'; 2 | import 'package:flutter_template/model/task/task_group.dart'; 3 | import 'package:flutter_template/model/task/task_status.dart'; 4 | 5 | const List DUMMY_TASK_GROUPS = [ 6 | TaskGroup('tg-1', 'Home', ['t1', 't2', 't3', 't4']), 7 | TaskGroup('tg-2', 'Work', ['t5', 't6', 't7']), 8 | TaskGroup('tg-3', 'Other', ['t8', 't9', 't10']) 9 | ]; 10 | 11 | const List DUMMY_TASKS = [ 12 | Task(id: 't1', title: 'Clean kitchen', status: DONE), 13 | Task(id: 't2', title: 'Plant flower', description: 'Any', status: NOT_DONE), 14 | Task(id: 't3', title: 'Buy milk', status: NOT_DONE), 15 | Task(id: 't4', title: 'Fix cupboard door', status: NOT_DONE), 16 | Task(id: 't5', title: 'Pretend to work', status: DONE), 17 | Task(id: 't6', title: 'Vacation', description: '2 weeks', status: DONE), 18 | Task(id: 't7', title: 'Pretend to work again', status: NOT_DONE), 19 | Task(id: 't8', title: 'Refuel car', status: DONE), 20 | Task(id: 't9', title: 'Visit dentist', status: NOT_DONE), 21 | Task(id: 't10', title: 'Pickup mail', status: DONE) 22 | ]; 23 | -------------------------------------------------------------------------------- /lib/data/repository/tasks/tasks_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter_template/model/task/task.dart'; 3 | import 'package:flutter_template/model/task/task_group.dart'; 4 | 5 | /// Main entry point for accessing and manipulating tasks data. 6 | abstract class TasksDataSource { 7 | abstract final String userId; 8 | 9 | /// Get all task groups for the logged in user. 10 | Future> getTaskGroups(); 11 | 12 | /// Get all tasks belonging to a task group. 13 | /// 14 | /// Throws [DataNotFoundException] unless ALL tasks are retrieved. 15 | Future> getTasks(String taskGroupId); 16 | 17 | /// Find a task by id. 18 | /// 19 | /// Throws [DataNotFoundException] if the tasks could not be found. 20 | Future getTask(String taskId); 21 | 22 | /// Mark a [Task] as done. 23 | /// 24 | /// Throws [DataNotFoundException] if the tasks could not be found. 25 | Future completeTask(String taskId); 26 | 27 | /// Mark a [Task] as not done. 28 | /// 29 | /// Throws [DataNotFoundException] if the tasks could not be found. 30 | Future reopenTask(String taskId); 31 | 32 | /// Gets all tasks grouped by TaskGroup. 33 | Future>> getAllTasksGrouped(); 34 | 35 | /// Creates a new [Task]. [Task.id] is overwritten by server. 36 | Future createTask(Task createTask); 37 | 38 | /// Creates a new [TaskGroup]. [TaskGroup.id] is overwritten by server. 39 | Future createTaskGroup(TaskGroup createTaskGroup); 40 | 41 | /// Updates taskIds in given task group. 42 | Future updateTaskGroup(final TaskGroup taskGroup); 43 | 44 | /// Deletes all task groups. 45 | Future deleteAllTaskGroups(); 46 | 47 | /// Deletes all user data. 48 | Future deleteAllData(); 49 | } 50 | -------------------------------------------------------------------------------- /lib/di/user_scope.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/data/repository/tasks/tasks_cache_data_source.dart'; 2 | import 'package:flutter_template/data/repository/tasks/tasks_remote_data_source.dart'; 3 | import 'package:flutter_template/data/repository/tasks/tasks_repository.dart'; 4 | import 'package:flutter_template/di/service_locator.dart'; 5 | import 'package:flutter_template/network/tasks_api_service.dart'; 6 | 7 | /// User scoped components that are created when the user logs in 8 | /// and destroyed on logout. 9 | const String userScopeName = 'userScope'; 10 | 11 | /// Use [setupUserScope] to setup components that need to be alive as 12 | /// long as there is a logged in user. Provide a dispose method when 13 | /// registering that will be invoked when this scope is torn down. 14 | Future setupUserScope(String userId) async { 15 | 16 | //todo add other user scope dependencies here, mind to provide dispose methods 17 | 18 | // Repositories 19 | final TasksApiService tasksApiService = serviceLocator.get(); 20 | final TasksRepository tasksRepository = TasksRepository( 21 | remote: TasksRemoteDataSource(userId, tasksApiService), 22 | cache: TasksCacheDataSource(userId), 23 | ); 24 | 25 | serviceLocator 26 | ..registerSingleton(tasksRepository, 27 | dispose: (instance) => instance.teardown()); 28 | } 29 | 30 | /// Use [teardownUserScope] to dispose the user scoped components if 31 | /// you haven't provided a dispose method when registering. 32 | Future teardownUserScope() async { 33 | 34 | //todo teardown user scope components registered without dispose method here 35 | } 36 | -------------------------------------------------------------------------------- /lib/feature/auth/login/bloc/login_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter_template/log/log.dart'; 3 | import 'package:flutter_template/user/user_manager.dart'; 4 | 5 | import 'login_state.dart'; 6 | 7 | export 'login_state.dart'; 8 | 9 | class LoginCubit extends Cubit { 10 | final UserManager userManager; 11 | 12 | LoginCubit(this.userManager) : super(AwaitUserInput()); 13 | 14 | Future onUserLogin(String username, String password) async { 15 | Log.d('LoginCubit - User login: username $username'); 16 | emit(LoginInProgress()); 17 | 18 | try { 19 | await userManager.login(username, password); 20 | emit(LoginSuccess()); 21 | } catch (exp) { 22 | emit(LoginFailure(error: exp)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/feature/auth/login/bloc/login_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class LoginState extends Equatable { 4 | @override 5 | List get props => []; 6 | 7 | @override 8 | String toString() { 9 | return this.runtimeType.toString(); 10 | } 11 | } 12 | 13 | class AwaitUserInput extends LoginState {} 14 | 15 | class LoginInProgress extends LoginState {} 16 | 17 | class LoginSuccess extends LoginState {} 18 | 19 | class LoginFailure extends LoginState { 20 | final dynamic error; 21 | 22 | LoginFailure({this.error}); 23 | 24 | @override 25 | String toString() { 26 | return 'LoginFailure {error: $error}'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/feature/auth/login/ui/login_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_template/di/service_locator.dart'; 5 | import 'package:flutter_template/feature/auth/login/bloc/login_cubit.dart'; 6 | import 'package:flutter_template/feature/auth/login/ui/login_view.dart'; 7 | import 'package:flutter_template/user/user_manager.dart'; 8 | 9 | class LoginPage extends Page { 10 | @override 11 | Route createRoute(BuildContext context) { 12 | return CupertinoPageRoute( 13 | settings: this, 14 | builder: (BuildContext context) => BlocProvider( 15 | create: (BuildContext context) => 16 | LoginCubit(serviceLocator.get()), 17 | child: LoginView(), 18 | ), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/feature/auth/router/auth_nav_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | 4 | @immutable 5 | abstract class AuthNavState extends Equatable { 6 | const AuthNavState._(this.prevState); 7 | 8 | const factory AuthNavState.login() = LoginNavState; 9 | 10 | const factory AuthNavState.signupUsername(AuthNavState prevState) = 11 | SignupUsernameNavState; 12 | 13 | const factory AuthNavState.signupPassword(AuthNavState prevState) = 14 | SignupPasswordNavState; 15 | 16 | final AuthNavState? prevState; 17 | 18 | @override 19 | List get props => []; 20 | 21 | @override 22 | String toString() { 23 | return this.runtimeType.toString() + '(prevState: $prevState)'; 24 | } 25 | } 26 | 27 | @immutable 28 | class LoginNavState extends AuthNavState { 29 | const LoginNavState() : super._(null); 30 | } 31 | 32 | @immutable 33 | class SignupUsernameNavState extends AuthNavState { 34 | const SignupUsernameNavState(AuthNavState prevState) : super._(prevState); 35 | } 36 | 37 | @immutable 38 | class SignupPasswordNavState extends AuthNavState { 39 | const SignupPasswordNavState(AuthNavState prevState) : super._(prevState); 40 | } 41 | -------------------------------------------------------------------------------- /lib/feature/auth/router/auth_router.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_template/di/service_locator.dart'; 4 | import 'package:flutter_template/feature/auth/router/auth_router_delegate.dart'; 5 | import 'package:flutter_template/feature/auth/signup/bloc/signup_cubit.dart'; 6 | import 'package:flutter_template/network/user_api_service.dart'; 7 | import 'package:flutter_template/user/user_manager.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | /// Nested router that hosts all auth screens and manages navigation among them. 11 | class AuthRouter extends StatelessWidget { 12 | final GlobalKey navigatorKey; 13 | 14 | const AuthRouter(this.navigatorKey, {Key? key}) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final childBackButtonDispatcher = 19 | ChildBackButtonDispatcher(Router.of(context).backButtonDispatcher!) 20 | ..takePriority(); 21 | 22 | return BlocProvider( 23 | create: (BuildContext context) => SignupCubit( 24 | serviceLocator.get(), 25 | serviceLocator.get(), 26 | ), 27 | child: ChangeNotifierProvider( 28 | create: (_) => AuthRouterDelegate(navigatorKey), 29 | child: Consumer( 30 | builder: (context, authRouterDelegate, child) => Router( 31 | routerDelegate: authRouterDelegate, 32 | backButtonDispatcher: childBackButtonDispatcher, 33 | ))), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/feature/auth/router/auth_router_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_template/feature/auth/login/ui/login_page.dart'; 4 | import 'package:flutter_template/feature/auth/router/auth_nav_state.dart'; 5 | import 'package:flutter_template/feature/auth/signup/ui/password/password_page.dart'; 6 | import 'package:flutter_template/feature/auth/signup/ui/username/username_page.dart'; 7 | 8 | class AuthRouterDelegate extends RouterDelegate 9 | with ChangeNotifier, PopNavigatorRouterDelegateMixin { 10 | final GlobalKey navigatorKey; 11 | 12 | AuthNavState _authNavState; 13 | 14 | AuthRouterDelegate(this.navigatorKey, 15 | [this._authNavState = const AuthNavState.login()]); 16 | 17 | void setLoginNavState() { 18 | _authNavState = AuthNavState.login(); 19 | notifyListeners(); 20 | } 21 | 22 | void setSignupUsernameNavState() { 23 | _authNavState = AuthNavState.signupUsername(_authNavState); 24 | notifyListeners(); 25 | } 26 | 27 | void setSignupPasswordNavState() { 28 | _authNavState = AuthNavState.signupPassword(_authNavState); 29 | notifyListeners(); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return Navigator( 35 | key: navigatorKey, 36 | pages: [ 37 | LoginPage(), 38 | if (_authNavState is SignupUsernameNavState) UsernamePage(), 39 | if (_authNavState is SignupPasswordNavState) ...[ 40 | UsernamePage(), 41 | PasswordPage() 42 | ], 43 | ], 44 | onPopPage: (route, result) { 45 | _authNavState = _authNavState.prevState ?? AuthNavState.login(); 46 | return route.didPop(result); 47 | }); 48 | } 49 | 50 | @override 51 | Future setNewRoutePath(configuration) async { 52 | /* no-op */ 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/feature/auth/signup/bloc/signup_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter_template/log/log.dart'; 3 | import 'package:flutter_template/model/user/user.dart'; 4 | import 'package:flutter_template/network/user_api_service.dart'; 5 | import 'package:flutter_template/user/user_manager.dart'; 6 | 7 | import 'signup_state.dart'; 8 | 9 | export 'signup_state.dart'; 10 | 11 | class SignupCubit extends Cubit { 12 | final UserApiService apiService; 13 | final UserManager userManager; 14 | 15 | String? _username; 16 | String? _password; 17 | 18 | SignupCubit(this.apiService, this.userManager) 19 | : super(AwaitUsernameInput('')); 20 | 21 | Future onUsernameEntered(String username) async { 22 | Log.d('SignUpCubit - User sign up: username $username'); 23 | _username = username; 24 | emit(AwaitPasswordInput(username)); 25 | } 26 | 27 | Future onPasswordEntered(String password) async { 28 | Log.d('SignUpCubit - User sign up: username $password'); 29 | _password = password; 30 | } 31 | 32 | Future onUserSignup() async { 33 | Log.d('SignUpCubit - User sign up: username $_username'); 34 | Log.d('SignUpCubit - User sign up: password $_password'); 35 | 36 | emit(SignupInProgress()); 37 | final User user = User(id: "id", email: _username!); 38 | 39 | await apiService 40 | .signUp(user) 41 | .then((_) => userManager.login(_username!, _password!)); 42 | emit(SignupSuccess()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/feature/auth/signup/bloc/signup_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class SignupState extends Equatable { 4 | @override 5 | List get props => [Object()]; 6 | 7 | @override 8 | String toString() { 9 | return this.runtimeType.toString(); 10 | } 11 | } 12 | 13 | abstract class AwaitUserInput extends SignupState { 14 | final String username; 15 | 16 | AwaitUserInput(this.username); 17 | } 18 | 19 | class AwaitUsernameInput extends AwaitUserInput { 20 | AwaitUsernameInput(String username) : super(username); 21 | } 22 | 23 | class AwaitPasswordInput extends AwaitUserInput { 24 | AwaitPasswordInput(String username) : super(username); 25 | } 26 | 27 | class SignupInProgress extends SignupState {} 28 | 29 | class SignupSuccess extends SignupState {} 30 | 31 | class SignupFailure extends SignupState { 32 | final dynamic error; 33 | 34 | SignupFailure({this.error}); 35 | 36 | @override 37 | String toString() { 38 | return 'SignUpFailure {error: $error}'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/feature/auth/signup/ui/password/password_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_template/feature/auth/signup/ui/password/password_view.dart'; 4 | 5 | class PasswordPage extends Page { 6 | @override 7 | Route createRoute(BuildContext context) { 8 | return CupertinoPageRoute( 9 | settings: this, 10 | builder: (BuildContext context) => PasswordView(), 11 | ); 12 | } 13 | } -------------------------------------------------------------------------------- /lib/feature/auth/signup/ui/username/username_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_template/feature/auth/signup/ui/username/username_view.dart'; 4 | 5 | class UsernamePage extends Page { 6 | @override 7 | Route createRoute(BuildContext context) { 8 | return CupertinoPageRoute( 9 | settings: this, 10 | builder: (BuildContext context) => UsernameView(), 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/feature/force_update/force_update_alert.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_template/config/app_update_config.dart'; 4 | import 'package:flutter_template/log/log.dart'; 5 | import 'package:flutter_template/widgets/alert_dialog.dart'; 6 | import 'package:flutter/cupertino.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:url_launcher/url_launcher.dart'; 9 | 10 | const forceUpdateDialogRouteName = '/forceUpdateDialog'; 11 | 12 | Future showForceUpdateAlert(BuildContext context) { 13 | final storeUrl = Platform.isIOS ? APP_STORE_URL : PLAY_STORE_URL; 14 | 15 | return showAlert( 16 | context, 17 | title: 'force_update_title', //todo change 18 | message: 'force_update_message', //todo change 19 | primaryButtonText: 'force_update_button_title', //todo change 20 | onPrimaryClick: () => _launchURL(storeUrl), 21 | isDismissible: false, 22 | willPopScope: false, 23 | settings: RouteSettings(name: forceUpdateDialogRouteName), 24 | ); 25 | } 26 | 27 | void _launchURL(String url) async { 28 | var encodedUrl = Uri.parse(url); 29 | if (await canLaunchUrl(encodedUrl)) { 30 | await launchUrl(encodedUrl); 31 | } else { 32 | Log.e(Exception('Could not launch $encodedUrl')); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/feature/force_update/force_update_exception.dart: -------------------------------------------------------------------------------- 1 | /// Exception thrown when we're trying to perform an action on an 2 | /// older version that must be updated. 3 | class ForceUpdateException implements Exception { 4 | final String? message; 5 | 6 | ForceUpdateException([this.message]); 7 | 8 | @override 9 | String toString() { 10 | return 'ForceUpdateException{message: $message}'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/feature/force_update/force_update_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/app.dart'; 2 | import 'package:flutter_template/log/log.dart'; 3 | import 'package:flutter_template/routing/app_router_delegate.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | /// Handles [ForceUpdateException]. 7 | class ForceUpdateHandler { 8 | ForceUpdateHandler(); 9 | 10 | void onForceUpdateEvent() { 11 | final rootContext = rootNavigatorKey.currentContext; 12 | if (rootContext != null) { 13 | rootContext.read().setForceUpdateNavState(); 14 | } else { 15 | Log.e(Exception('Force update dialog not shown: rootContext == null')); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/feature/force_update/ui/force_update_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/feature/force_update/ui/force_update_view.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class ForceUpdatePage extends Page { 6 | ForceUpdatePage(); 7 | 8 | @override 9 | Route createRoute(BuildContext context) { 10 | return PageRouteBuilder( 11 | settings: this, 12 | transitionDuration: Duration.zero, 13 | pageBuilder: (BuildContext context, _, __) => 14 | ForceUpdateView(), 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/feature/force_update/ui/force_update_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/feature/force_update/force_update_alert.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ForceUpdateView extends StatefulWidget { 5 | const ForceUpdateView({Key? key}) : super(key: key); 6 | 7 | @override 8 | State createState() => _ForceUpdateViewState(); 9 | } 10 | 11 | class _ForceUpdateViewState extends State { 12 | @override 13 | void initState() { 14 | super.initState(); 15 | 16 | WidgetsBinding.instance.addPostFrameCallback((_) async { 17 | showForceUpdateAlert(context); 18 | }); 19 | } 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Builder(builder: (context) { 24 | return Container( 25 | color: Colors.white, 26 | ); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/feature/home/create_task/bloc/create_task_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter_template/data/repository/tasks/tasks_repository.dart'; 3 | import 'package:flutter_template/log/log.dart'; 4 | 5 | import 'create_task_state.dart'; 6 | 7 | class CreateTaskCubit extends Cubit { 8 | final TasksRepository _tasksRepository; 9 | 10 | CreateTaskCubit(this._tasksRepository) : super(AwaitUserInput()); 11 | 12 | Future onCreateTask(Task task) async { 13 | Log.d('CreateTaskBloc - Create Task'); 14 | emit(CreateTaskInProgress()); 15 | try { 16 | await _tasksRepository.createTask(task); 17 | emit(CreateTaskSuccess()); 18 | } catch (exp) { 19 | emit(CreateTaskFailure(error: exp)); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/feature/home/create_task/bloc/create_task_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | @immutable 5 | abstract class CreateTaskState extends Equatable { 6 | @override 7 | List get props => []; 8 | 9 | @override 10 | String toString() { 11 | return this.runtimeType.toString(); 12 | } 13 | } 14 | 15 | /// The initial state / awaiting user input 16 | class AwaitUserInput extends CreateTaskState {} 17 | 18 | /// The task is being created 19 | class CreateTaskInProgress extends CreateTaskState {} 20 | 21 | /// The task is successfully created 22 | class CreateTaskSuccess extends CreateTaskState {} 23 | 24 | /// There was an error when creating the task 25 | class CreateTaskFailure extends CreateTaskState { 26 | final dynamic error; 27 | 28 | CreateTaskFailure({this.error}); 29 | 30 | @override 31 | String toString() { 32 | return 'CreateTaskFailure {error: $error}'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/feature/home/router/home_nav_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter_template/model/task/task.dart'; 4 | 5 | @immutable 6 | abstract class HomeNavState extends Equatable { 7 | const HomeNavState._(); 8 | 9 | const factory HomeNavState.taskList() = TaskListNavState; 10 | 11 | const factory HomeNavState.taskDetail(Task task) = TaskDetailNavState; 12 | 13 | @override 14 | List get props => []; 15 | 16 | @override 17 | String toString() { 18 | return this.runtimeType.toString(); 19 | } 20 | } 21 | 22 | @immutable 23 | class TaskListNavState extends HomeNavState { 24 | const TaskListNavState() : super._(); 25 | } 26 | 27 | @immutable 28 | class TaskDetailNavState extends HomeNavState { 29 | final Task task; 30 | 31 | const TaskDetailNavState(this.task) : super._(); 32 | } 33 | -------------------------------------------------------------------------------- /lib/feature/home/router/home_router.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_template/feature/home/router/home_router_delegate.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | /// Nested router that hosts all task screens and manages navigation among them. 6 | class HomeRouter extends StatelessWidget { 7 | final GlobalKey navigatorKey; 8 | 9 | const HomeRouter(this.navigatorKey, {Key? key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final childBackButtonDispatcher = 14 | ChildBackButtonDispatcher(Router.of(context).backButtonDispatcher!) 15 | ..takePriority(); 16 | 17 | return ChangeNotifierProvider( 18 | create: (_) => HomeRouterDelegate(navigatorKey), 19 | child: Consumer( 20 | builder: (context, homeRouterDelegate, child) => Router( 21 | routerDelegate: homeRouterDelegate, 22 | backButtonDispatcher: childBackButtonDispatcher, 23 | ))); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/feature/home/router/home_router_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_template/feature/home/router/home_nav_state.dart'; 4 | import 'package:flutter_template/feature/home/task_detail/ui/task_detail_page.dart'; 5 | import 'package:flutter_template/feature/home/task_list/ui/task_list_page.dart'; 6 | import 'package:flutter_template/feature/settings/ui/settings_page.dart'; 7 | import 'package:flutter_template/model/task/task.dart'; 8 | 9 | class HomeRouterDelegate extends RouterDelegate 10 | with ChangeNotifier, PopNavigatorRouterDelegateMixin { 11 | final GlobalKey navigatorKey; 12 | 13 | HomeRouterDelegate(this.navigatorKey, 14 | [this.homeNavState = const HomeNavState.taskList()]); 15 | 16 | HomeNavState homeNavState = HomeNavState.taskList(); 17 | bool isSettingsShownState = false; 18 | 19 | void setTaskDetailNavState(Task selectedTask) { 20 | homeNavState = HomeNavState.taskDetail(selectedTask); 21 | notifyListeners(); 22 | } 23 | 24 | void setIsSettingsShownState(bool isShown){ 25 | isSettingsShownState = isShown; 26 | notifyListeners(); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return Navigator( 32 | key: navigatorKey, 33 | pages: [ 34 | TaskListPage(), 35 | if (homeNavState is TaskDetailNavState) 36 | TaskDetailPage(task: (homeNavState as TaskDetailNavState).task), 37 | if (isSettingsShownState) SettingsPage() 38 | ], 39 | onPopPage: (route, result) { 40 | if(isSettingsShownState) isSettingsShownState = false; 41 | homeNavState = HomeNavState.taskList(); 42 | return route.didPop(result); 43 | }); 44 | } 45 | 46 | @override 47 | Future setNewRoutePath(configuration) async { 48 | /* no-op */ 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/feature/home/task_detail/ui/task_detail_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_template/feature/home/task_detail/ui/task_detail_view.dart'; 4 | import 'package:flutter_template/model/task/task.dart'; 5 | 6 | class TaskDetailPage extends Page { 7 | final Task task; 8 | 9 | TaskDetailPage({required this.task}); 10 | 11 | @override 12 | Route createRoute(BuildContext context) { 13 | return CupertinoPageRoute( 14 | settings: this, 15 | builder: (BuildContext context) => TaskDetailView(task), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/feature/home/task_detail/ui/task_detail_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_template/model/task/task.dart'; 3 | 4 | class TaskDetailView extends StatelessWidget { 5 | final Task task; 6 | 7 | TaskDetailView(this.task); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | title: Text(task.title) 14 | ), 15 | body: Padding( 16 | padding: const EdgeInsets.all(8.0), 17 | child: Column( 18 | crossAxisAlignment: CrossAxisAlignment.start, 19 | children: [ 20 | Text(task.title, style: Theme.of(context).textTheme.headline6), 21 | Text(_description(task), style: Theme.of(context).textTheme.subtitle1) 22 | ], 23 | ), 24 | ), 25 | ); 26 | } 27 | 28 | String _description(Task task) => task.description ?? ''; 29 | } 30 | -------------------------------------------------------------------------------- /lib/feature/home/task_list/bloc/task_list_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter_template/model/task/task.dart'; 4 | import 'package:flutter_template/model/task/task_group.dart'; 5 | 6 | @immutable 7 | abstract class TaskListEvent extends Equatable { 8 | @override 9 | List get props => []; 10 | 11 | @override 12 | String toString() { 13 | return this.runtimeType.toString(); 14 | } 15 | } 16 | 17 | /// Requests the tasks to be loaded. Can be used for refreshing the list. 18 | class LoadTasks extends TaskListEvent {} 19 | 20 | /// Requests for a [Task] to be marked as completed. 21 | class TaskCompleted extends TaskListEvent { 22 | final Task task; 23 | 24 | TaskCompleted(this.task); 25 | } 26 | 27 | /// Requests for a [Task] to be marked as not done. 28 | class TaskReopened extends TaskListEvent { 29 | final Task task; 30 | 31 | TaskReopened(this.task); 32 | } 33 | 34 | /// Requests for reordering the tasks. 35 | class TasksReordered extends TaskListEvent { 36 | final TaskGroup key; 37 | final int oldIndex; 38 | final int newIndex; 39 | 40 | TasksReordered(this.key, this.oldIndex, this.newIndex); 41 | } 42 | 43 | /// Triggers a logout event. 44 | class Logout extends TaskListEvent {} 45 | -------------------------------------------------------------------------------- /lib/feature/home/task_list/bloc/task_list_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter_template/model/task/task.dart'; 4 | import 'package:flutter_template/model/task/task_group.dart'; 5 | 6 | @immutable 7 | abstract class TaskListState extends Equatable { 8 | @override 9 | List get props => []; 10 | 11 | @override 12 | String toString() { 13 | return this.runtimeType.toString(); 14 | } 15 | } 16 | 17 | /// The task list is being loaded 18 | class TasksLoadInProgress extends TaskListState {} 19 | 20 | /// The tasks are successfully loaded 21 | class TasksLoadSuccess extends TaskListState { 22 | final Map> tasksGrouped; 23 | final taskCount; 24 | 25 | TasksLoadSuccess(this.tasksGrouped) 26 | : taskCount = tasksGrouped.values.fold( 27 | 0, (previousValue, element) => previousValue + element.length); 28 | 29 | @override 30 | String toString() { 31 | return 'TasksLoadSuccess{tasksCount: $taskCount}'; 32 | } 33 | } 34 | 35 | /// There was an error when loading the tasks 36 | class TasksLoadFailure extends TaskListState { 37 | final dynamic error; 38 | 39 | TasksLoadFailure({this.error}); 40 | 41 | @override 42 | String toString() { 43 | return 'TaskLoadFailure {error: $error}'; 44 | } 45 | } 46 | 47 | /// A task operation failed 48 | class TaskOpFailure extends TaskListState { 49 | final TaskListState prevState; 50 | final Task task; 51 | final dynamic error; 52 | 53 | TaskOpFailure(this.prevState, this.task, this.error); 54 | } 55 | -------------------------------------------------------------------------------- /lib/feature/home/task_list/ui/task_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_template/data/repository/tasks/tasks_repository.dart'; 5 | import 'package:flutter_template/di/service_locator.dart'; 6 | import 'package:flutter_template/feature/home/task_list/bloc/task_list_bloc.dart'; 7 | import 'package:flutter_template/feature/home/task_list/ui/task_list_view.dart'; 8 | import 'package:flutter_template/user/user_manager.dart'; 9 | 10 | class TaskListPage extends Page { 11 | @override 12 | Route createRoute(BuildContext context) { 13 | return CupertinoPageRoute( 14 | settings: this, 15 | builder: (BuildContext context) => BlocProvider( 16 | create: (BuildContext context) => TaskListBloc( 17 | serviceLocator.get(), 18 | serviceLocator.get())..add(LoadTasks()), 19 | child: TaskListView(), 20 | ), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/feature/loading/ui/circular_progress_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class PlatformCircularProgressIndicator extends StatelessWidget { 7 | final double padding; 8 | final double height; 9 | final double width; 10 | 11 | const PlatformCircularProgressIndicator({ 12 | Key? key, 13 | this.padding = 0, 14 | this.height = 30, 15 | this.width = 30, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Padding( 21 | padding: EdgeInsets.all(padding), 22 | child: Center( 23 | child: SizedBox( 24 | child: Platform.isIOS 25 | ? CupertinoActivityIndicator(radius: height / 2.0) 26 | : CircularProgressIndicator(), 27 | height: height, 28 | width: width, 29 | ), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/feature/loading/ui/loading_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_template/feature/loading/ui/circular_progress_indicator.dart'; 3 | 4 | class LoadingPage extends Page { 5 | @override 6 | Route createRoute(BuildContext context) { 7 | return MaterialPageRoute( 8 | settings: this, 9 | builder: (BuildContext context) => Scaffold( 10 | appBar: AppBar(), 11 | body: Padding( 12 | padding: const EdgeInsets.all(16.0), 13 | child: Center( 14 | child: PlatformCircularProgressIndicator(), 15 | ), 16 | ), 17 | ), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/feature/settings/ui/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_template/feature/settings/ui/settings_view.dart'; 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | 5 | class SettingsPage extends Page { 6 | @override 7 | Route createRoute(BuildContext context) { 8 | return MaterialPageRoute( 9 | settings: this, 10 | builder: (BuildContext context) => Scaffold( 11 | appBar: AppBar( 12 | title: Text(AppLocalizations.of(context)!.settings), 13 | ), 14 | body: Center( 15 | child: SettingsView(), 16 | ), 17 | ), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/feature/settings/ui/settings_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_template/feature/settings/preferences_helper.dart'; 3 | import 'package:flutter_template/feature/settings/ui/widget/settings_language_widget.dart'; 4 | import 'package:flutter_template/feature/settings/ui/widget/settings_theme_switch_widget.dart'; 5 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 6 | 7 | import 'package:flutter_template/di/service_locator.dart'; 8 | import 'widget/settings_header_widget.dart'; 9 | 10 | class SettingsView extends StatelessWidget { 11 | const SettingsView({Key? key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | child: Column( 17 | crossAxisAlignment: CrossAxisAlignment.stretch, 18 | children: [ 19 | SettingsHeader(header: AppLocalizations.of(context)!.theme), 20 | SettingsThemeSwitch(), 21 | Container(color: Colors.grey, height: 1), 22 | SettingsHeader(header: AppLocalizations.of(context)!.language), 23 | SettingsLanguageWidget( 24 | selectedLanguage: 25 | serviceLocator.get().languagePreferred) 26 | ], 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/feature/settings/ui/widget/settings_header_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SettingsHeader extends StatelessWidget { 4 | final String header; 5 | 6 | const SettingsHeader({Key? key, required this.header}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Padding( 11 | padding: const EdgeInsets.fromLTRB(56, 12, 0, 0), 12 | child: Text( 13 | header, 14 | style: TextStyle( 15 | color: Theme.of(context).brightness == Brightness.dark 16 | ? Theme.of(context).primaryColorLight 17 | : Theme.of(context).primaryColorDark, 18 | fontSize: 16, 19 | fontStyle: FontStyle.italic), 20 | ), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/feature/settings/ui/widget/settings_language_icon_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SettingsLanguageIcon extends StatelessWidget { 4 | final String languageCode; 5 | 6 | const SettingsLanguageIcon({Key? key, required this.languageCode}) 7 | : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Padding( 12 | padding: const EdgeInsets.all(16.0), 13 | child: Container( 14 | height: 24, 15 | width: 24, 16 | child: Center( 17 | child: Text( 18 | languageCode, 19 | style: TextStyle(fontSize: 12), 20 | ), 21 | ), 22 | decoration: BoxDecoration( 23 | border: Border.all( 24 | width: 1.5, 25 | color: Theme.of(context).brightness == Brightness.dark 26 | ? Colors.white 27 | : Colors.black), 28 | borderRadius: BorderRadius.all( 29 | Radius.circular(50), 30 | ), 31 | ), 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/log/abstract_logger.dart: -------------------------------------------------------------------------------- 1 | /// Base class for all logger implementations. 2 | abstract class AbstractLogger { 3 | /// Debug 4 | void d(String message); 5 | 6 | /// Warning 7 | void w(String message); 8 | 9 | /// Error 10 | void e(Object error); 11 | } 12 | -------------------------------------------------------------------------------- /lib/log/bloc_events_logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_template/config/pre_app_config.dart'; 4 | import 'package:flutter_template/log/log.dart'; 5 | 6 | /// Logs blog events. 7 | /// 8 | /// To apply globally, before the app starts, set it as bloc observer: 9 | /// Bloc.observer = BlocEventsLogger(); 10 | /// 11 | /// See [preAppConfig] 12 | class BlocEventsLogger extends BlocObserver { 13 | @override 14 | void onEvent(Bloc bloc, Object? event) { 15 | if (event != null) { 16 | Log.d(event.toString()); 17 | } 18 | super.onEvent(bloc, event); 19 | } 20 | 21 | @override 22 | void onTransition(Bloc bloc, Transition transition) { 23 | Log.d(transition.toString()); 24 | super.onTransition(bloc, transition); 25 | } 26 | 27 | @override 28 | void onError(BlocBase bloc, Object error, StackTrace stackTrace) { 29 | Log.e(error); 30 | super.onError(bloc, error, stackTrace); 31 | } 32 | } 33 | 34 | R runZonedWithBlocEventsLogger(R Function() body) => 35 | BlocOverrides.runZoned(body, blocObserver: BlocEventsLogger()); 36 | -------------------------------------------------------------------------------- /lib/log/console_logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/log/abstract_logger.dart'; 2 | import 'package:logger/logger.dart' as console; 3 | 4 | /// Logger that outputs messages to the console. 5 | class ConsoleLogger implements AbstractLogger { 6 | final console.Logger logger; 7 | 8 | factory ConsoleLogger.create() { 9 | return ConsoleLogger(console.Logger( 10 | printer: console.SimplePrinter(colors: true, printTime: false), 11 | )); 12 | } 13 | 14 | ConsoleLogger(this.logger); 15 | 16 | @override 17 | void d(String message) => logger.d(message); 18 | 19 | @override 20 | void w(String message) => logger.w(message); 21 | 22 | @override 23 | void e(Object error) => logger.e(error.toString(), error); 24 | } 25 | -------------------------------------------------------------------------------- /lib/log/filtered_logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/config/flavor_config.dart'; 2 | import 'package:flutter_template/log/abstract_logger.dart'; 3 | 4 | enum LogLevel { debug, warning, error } 5 | 6 | /// Logger that applies a filter to log messages. 7 | class FilteredLogger implements AbstractLogger { 8 | final AbstractLogger _inner; 9 | final bool Function(LogLevel logLevel) shouldLog; 10 | 11 | FilteredLogger(this._inner, this.shouldLog); 12 | 13 | @override 14 | void d(String message) { 15 | if (shouldLog(LogLevel.debug)) { 16 | _inner.d(message); 17 | } 18 | } 19 | 20 | @override 21 | void w(String message) { 22 | if (shouldLog(LogLevel.warning)) { 23 | _inner.w(message); 24 | } 25 | } 26 | 27 | @override 28 | void e(Object error) { 29 | if (shouldLog(LogLevel.error)) { 30 | _inner.e(error); 31 | } 32 | } 33 | } 34 | 35 | /// This log filter always logs except in production and tests. 36 | bool Function(LogLevel _) noLogsInProductionOrTests() => 37 | (_) => FlavorConfig.isInitialized() && !FlavorConfig.isProduction(); 38 | 39 | /// This log filter always logs except in tests. 40 | bool Function(LogLevel _) noLogsInTests() => 41 | (_) => FlavorConfig.isInitialized(); 42 | 43 | extension Filter on AbstractLogger { 44 | FilteredLogger makeFiltered(bool Function(LogLevel logLevel) shouldLog) => 45 | FilteredLogger(this, shouldLog); 46 | } 47 | -------------------------------------------------------------------------------- /lib/log/firebase_logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter_template/log/abstract_logger.dart'; 4 | 5 | /// Logger that outputs messages to firebase console to be added to a crash report. 6 | class FirebaseLogger implements AbstractLogger { 7 | final FirebaseCrashlytics _crashlytics; 8 | 9 | FirebaseLogger.instance() : _crashlytics = FirebaseCrashlytics.instance; 10 | 11 | FirebaseLogger(this._crashlytics); 12 | 13 | @override 14 | void d(String message) => _crashlytics.log('(D) $message'); 15 | 16 | @override 17 | void w(String message) => _crashlytics.log('(W) $message'); 18 | 19 | @override 20 | void e(Object error) => 21 | _crashlytics.recordFlutterError(FlutterErrorDetails(exception: error)); 22 | } 23 | -------------------------------------------------------------------------------- /lib/log/log.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/log/abstract_logger.dart'; 2 | 3 | /// Static logger. Use this to log events across the app. 4 | /// 5 | /// __Before use you must set logger implementation.__ 6 | /// __See the setter for [logger].__ 7 | abstract class Log { 8 | static AbstractLogger? _logger; 9 | 10 | static set logger(AbstractLogger value) => _logger = value; 11 | 12 | static void d(String message) => _logger?.d(message); 13 | 14 | static void w(String message) => _logger?.w(message); 15 | 16 | static void e(Object error) => _logger?.e(error); 17 | } 18 | 19 | /// Convenience error function to use w/ [Future]s. 20 | void onErrorLog(error) => Log.e(error); 21 | -------------------------------------------------------------------------------- /lib/log/multi_logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/log/abstract_logger.dart'; 2 | 3 | /// Logger that outputs messages to multiple logger implementations. 4 | class MultiLogger implements AbstractLogger { 5 | final List loggers; 6 | 7 | MultiLogger(Iterable loggers) 8 | : this.loggers = loggers.toList(growable: false); 9 | 10 | @override 11 | void d(String message) => _log((logger) => logger.d(message)); 12 | 13 | @override 14 | void w(String message) => _log((logger) => logger.w(message)); 15 | 16 | @override 17 | void e(Object error) => _log((logger) => logger.e(error)); 18 | 19 | void _log(void log(AbstractLogger logger)) { 20 | loggers.forEach((logger) => log(logger)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/log/stub_logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/log/abstract_logger.dart'; 2 | 3 | /// Stub implementation of [AbstractLogger]. 4 | class StubLogger implements AbstractLogger { 5 | @override 6 | void d(String message) {} 7 | 8 | @override 9 | void e(Object error) {} 10 | 11 | @override 12 | void w(String message) {} 13 | } 14 | -------------------------------------------------------------------------------- /lib/main_dev.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_template/config/firebase_config.dart'; 3 | import 'package:flutter_template/config/pre_app_config.dart'; 4 | import 'package:flutter_template/log/bloc_events_logger.dart'; 5 | 6 | import 'app.dart'; 7 | import 'config/flavor_config.dart'; 8 | import 'config/network_constants.dart'; 9 | 10 | void main() async { 11 | FlavorConfig.set( 12 | Flavor.DEV, 13 | FlavorValues( 14 | baseUrlApi: baseUrlDev + apiPrefix, 15 | ), 16 | ); 17 | 18 | await preAppConfig(); 19 | 20 | runZonedGuardedWithErrorHandler( 21 | () => runZonedWithBlocEventsLogger( 22 | () => runApp(App()), 23 | ), 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /lib/main_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_template/config/firebase_config.dart'; 3 | import 'package:flutter_template/config/pre_app_config.dart'; 4 | import 'package:flutter_template/log/bloc_events_logger.dart'; 5 | 6 | import 'app.dart'; 7 | import 'config/flavor_config.dart'; 8 | import 'config/network_constants.dart'; 9 | 10 | Future main() async { 11 | FlavorConfig.set( 12 | Flavor.MOCK, 13 | FlavorValues( 14 | baseUrlApi: baseUrlDev + apiPrefix, 15 | ), 16 | ); 17 | 18 | await preAppConfig(); 19 | 20 | runZonedGuardedWithErrorHandler( 21 | () => runZonedWithBlocEventsLogger( 22 | () => runApp(App()), 23 | ), 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /lib/main_production.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_template/config/firebase_config.dart'; 3 | import 'package:flutter_template/config/pre_app_config.dart'; 4 | import 'package:flutter_template/log/bloc_events_logger.dart'; 5 | 6 | import 'app.dart'; 7 | import 'config/flavor_config.dart'; 8 | import 'config/network_constants.dart'; 9 | 10 | Future main() async { 11 | FlavorConfig.set( 12 | Flavor.PRODUCTION, 13 | FlavorValues( 14 | baseUrlApi: baseUrlProd + apiPrefix, 15 | ), 16 | ); 17 | 18 | await preAppConfig(); 19 | 20 | runZonedGuardedWithErrorHandler( 21 | () => runZonedWithBlocEventsLogger( 22 | () => runApp(App()), 23 | ), 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /lib/main_staging.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_template/config/firebase_config.dart'; 3 | import 'package:flutter_template/config/pre_app_config.dart'; 4 | import 'package:flutter_template/log/bloc_events_logger.dart'; 5 | 6 | import 'app.dart'; 7 | import 'config/flavor_config.dart'; 8 | import 'config/network_constants.dart'; 9 | 10 | Future main() async { 11 | FlavorConfig.set( 12 | Flavor.STAGING, 13 | FlavorValues( 14 | baseUrlApi: baseUrlStage + apiPrefix, 15 | ), 16 | ); 17 | 18 | await preAppConfig(); 19 | 20 | runZonedGuardedWithErrorHandler( 21 | () => runZonedWithBlocEventsLogger( 22 | () => runApp(App()), 23 | ), 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /lib/model/task/api/create_task.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter_template/model/task/task.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | import 'package:flutter_template/model/task/task_status.dart'; 5 | 6 | part 'create_task.g.dart'; 7 | 8 | @JsonSerializable() 9 | @immutable 10 | class CreateTask { 11 | final String title; 12 | final String? description; 13 | @JsonKey(name: 'status') 14 | final TaskStatus taskStatus; 15 | 16 | CreateTask( 17 | {required this.title, this.description = "", required this.taskStatus}); 18 | 19 | CreateTask.fromTask(Task task) 20 | : this.title = task.title, 21 | this.description = task.description, 22 | this.taskStatus = task.status; 23 | 24 | factory CreateTask.fromJson(Map json) => 25 | _$CreateTaskFromJson(json); 26 | 27 | Map toJson() => _$CreateTaskToJson(this); 28 | 29 | @override 30 | String toString() { 31 | return 'CreateTask{' 32 | 'title: $title, ' 33 | 'description: $description, ' 34 | 'taskStatus: $taskStatus' 35 | '}'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/model/task/api/create_task.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'create_task.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | CreateTask _$CreateTaskFromJson(Map json) => CreateTask( 10 | title: json['title'] as String, 11 | description: json['description'] as String? ?? "", 12 | taskStatus: $enumDecode(_$TaskStatusEnumMap, json['status']), 13 | ); 14 | 15 | Map _$CreateTaskToJson(CreateTask instance) => 16 | { 17 | 'title': instance.title, 18 | 'description': instance.description, 19 | 'status': _$TaskStatusEnumMap[instance.taskStatus]!, 20 | }; 21 | 22 | const _$TaskStatusEnumMap = { 23 | TaskStatus.notDone: 0, 24 | TaskStatus.done: 1, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/model/task/api/create_task_group.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter_template/model/task/task_group.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | 5 | part 'create_task_group.g.dart'; 6 | 7 | @JsonSerializable() 8 | @immutable 9 | class CreateTaskGroup { 10 | final String name; 11 | final List taskIds; 12 | 13 | CreateTaskGroup({required this.name, required this.taskIds}); 14 | 15 | CreateTaskGroup.fromTaskGroup(TaskGroup taskGroup) 16 | : this.name = taskGroup.name, 17 | this.taskIds = taskGroup.taskIds; 18 | 19 | factory CreateTaskGroup.fromJson(Map json) => 20 | _$CreateTaskGroupFromJson(json); 21 | 22 | Map toJson() => _$CreateTaskGroupToJson(this); 23 | 24 | @override 25 | String toString() { 26 | return 'CreateTaskGroup{name: $name, taskIds: $taskIds}'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/model/task/api/create_task_group.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'create_task_group.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | CreateTaskGroup _$CreateTaskGroupFromJson(Map json) => 10 | CreateTaskGroup( 11 | name: json['name'] as String, 12 | taskIds: 13 | (json['taskIds'] as List).map((e) => e as String).toList(), 14 | ); 15 | 16 | Map _$CreateTaskGroupToJson(CreateTaskGroup instance) => 17 | { 18 | 'name': instance.name, 19 | 'taskIds': instance.taskIds, 20 | }; 21 | -------------------------------------------------------------------------------- /lib/model/task/task.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter_template/model/task/task_status.dart'; 4 | import 'package:flutter_template/util/check_result.dart'; 5 | import 'package:json_annotation/json_annotation.dart'; 6 | 7 | part 'task.g.dart'; 8 | 9 | /// A task as part of a TO-DO list. 10 | @JsonSerializable() 11 | @immutable 12 | class Task extends Equatable { 13 | final String id; 14 | final String title; 15 | final String? description; 16 | final TaskStatus status; 17 | 18 | const Task({ 19 | required this.id, 20 | required this.status, 21 | required this.title, 22 | this.description, 23 | }); 24 | 25 | factory Task.fromJson(Map json) => _$TaskFromJson(json); 26 | 27 | Map toJson() => _$TaskToJson(this); 28 | 29 | Task copy({ 30 | String? title, 31 | String? description, 32 | TaskStatus? taskStatus, 33 | }) => 34 | Task( 35 | id: this.id, 36 | title: title ?? this.title, 37 | description: description ?? this.description, 38 | status: taskStatus ?? this.status, 39 | ); 40 | 41 | @checkResult 42 | Task changeStatus(TaskStatus newStatus) { 43 | return new Task( 44 | id: id, 45 | title: title, 46 | description: description, 47 | status: newStatus, 48 | ); 49 | } 50 | 51 | @override 52 | List get props => [id]; 53 | 54 | @override 55 | String toString() { 56 | return 'Task{' 57 | 'id: $id, ' 58 | 'title: $title, ' 59 | 'description: $description, ' 60 | 'taskStatus: $status' 61 | '}'; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/model/task/task.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'task.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Task _$TaskFromJson(Map json) => Task( 10 | id: json['id'] as String, 11 | status: $enumDecode(_$TaskStatusEnumMap, json['status']), 12 | title: json['title'] as String, 13 | description: json['description'] as String?, 14 | ); 15 | 16 | Map _$TaskToJson(Task instance) => { 17 | 'id': instance.id, 18 | 'title': instance.title, 19 | 'description': instance.description, 20 | 'status': _$TaskStatusEnumMap[instance.status]!, 21 | }; 22 | 23 | const _$TaskStatusEnumMap = { 24 | TaskStatus.notDone: 0, 25 | TaskStatus.done: 1, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/model/task/task_group.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_template/model/task/task.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | part 'task_group.g.dart'; 7 | 8 | /// Collection of grouped [Task]s. 9 | @JsonSerializable() 10 | @immutable 11 | class TaskGroup extends Equatable { 12 | final String id; 13 | final String name; 14 | final List taskIds; 15 | 16 | const TaskGroup(this.id, this.name, this.taskIds); 17 | 18 | factory TaskGroup.fromJson(Map json) => 19 | _$TaskGroupFromJson(json); 20 | 21 | Map toJson() => _$TaskGroupToJson(this); 22 | 23 | TaskGroup copy({String? id, String? name, List? newTaskIds}) { 24 | return TaskGroup( 25 | id ?? this.id, name ?? this.name, newTaskIds ?? this.taskIds); 26 | } 27 | 28 | @override 29 | List get props => [id, name, taskIds]; 30 | 31 | @override 32 | String toString() { 33 | return 'TaskGroup{id: $id, name: $name, taskIds: $taskIds}'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/model/task/task_group.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'task_group.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | TaskGroup _$TaskGroupFromJson(Map json) => TaskGroup( 10 | json['id'] as String, 11 | json['name'] as String, 12 | (json['taskIds'] as List).map((e) => e as String).toList(), 13 | ); 14 | 15 | Map _$TaskGroupToJson(TaskGroup instance) => { 16 | 'id': instance.id, 17 | 'name': instance.name, 18 | 'taskIds': instance.taskIds, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/model/task/task_status.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/util/collections_util.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | const DONE = TaskStatus.done; 5 | const NOT_DONE = TaskStatus.notDone; 6 | 7 | /// Current status of a [Task]. 8 | enum TaskStatus { 9 | @JsonValue(0) notDone, 10 | @JsonValue(1) done, 11 | } 12 | 13 | const _apiKeyLookupMap = { 14 | 0: TaskStatus.notDone, 15 | 1: TaskStatus.done, 16 | }; 17 | 18 | /// Extension functions for converting the enum value to its key 19 | /// and the reverse op - doing lookup from key. 20 | extension TaskStatusLookup on TaskStatus { 21 | static TaskStatus? fromApiKey(int key, {TaskStatus? defaultValue}) => 22 | _apiKeyLookupMap[key] ?? defaultValue; 23 | 24 | int toApiKey() => _apiKeyLookupMap.getByValue(this)!; 25 | } 26 | -------------------------------------------------------------------------------- /lib/model/user/credentials.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter_template/model/user/refresh_token.dart'; 4 | import 'package:flutter_template/util/string_util.dart'; 5 | import 'package:json_annotation/json_annotation.dart'; 6 | import 'package:jwt_decoder/jwt_decoder.dart'; 7 | 8 | part 'credentials.g.dart'; 9 | 10 | @JsonSerializable() 11 | @immutable 12 | class Credentials extends Equatable { 13 | final String token; 14 | final RefreshToken refreshToken; 15 | 16 | @JsonKey(ignore: true) 17 | final String _displayToken; 18 | 19 | Credentials(this.token, this.refreshToken) 20 | : _displayToken = token.shortenForPrint(); 21 | 22 | factory Credentials.fromJson(Map json) => 23 | _$CredentialsFromJson(json); 24 | 25 | Map toJson() => _$CredentialsToJson(this); 26 | 27 | @override 28 | List get props => [token, refreshToken]; 29 | 30 | bool isTokenExpired() => JwtDecoder.isExpired(token); 31 | 32 | bool isRefreshTokenExpired() => 33 | refreshToken.expiresAt < DateTime.now().millisecondsSinceEpoch; 34 | 35 | @override 36 | String toString() { 37 | return 'Credentials{token: $_displayToken, refreshToken: $refreshToken}'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/model/user/credentials.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'credentials.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Credentials _$CredentialsFromJson(Map json) => Credentials( 10 | json['token'] as String, 11 | RefreshToken.fromJson(json['refreshToken'] as Map), 12 | ); 13 | 14 | Map _$CredentialsToJson(Credentials instance) => 15 | { 16 | 'token': instance.token, 17 | 'refreshToken': instance.refreshToken, 18 | }; 19 | -------------------------------------------------------------------------------- /lib/model/user/refresh_token.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter_template/util/string_util.dart'; 4 | import 'package:json_annotation/json_annotation.dart'; 5 | 6 | part 'refresh_token.g.dart'; 7 | 8 | @JsonSerializable() 9 | @immutable 10 | class RefreshToken extends Equatable { 11 | final String token; 12 | final int expiresAt; 13 | 14 | @JsonKey(ignore: true) 15 | final String _displayToken; 16 | 17 | RefreshToken(this.token, this.expiresAt) 18 | : _displayToken = token.shortenForPrint(); 19 | 20 | factory RefreshToken.fromJson(Map map) => 21 | _$RefreshTokenFromJson(map); 22 | 23 | Map toJson() => _$RefreshTokenToJson(this); 24 | 25 | @override 26 | String toString() { 27 | return 'RefreshToken{token: $_displayToken, expiresAt: $expiresAt}'; 28 | } 29 | 30 | @override 31 | List get props => [token, expiresAt]; 32 | } 33 | -------------------------------------------------------------------------------- /lib/model/user/refresh_token.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'refresh_token.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | RefreshToken _$RefreshTokenFromJson(Map json) => RefreshToken( 10 | json['token'] as String, 11 | json['expiresAt'] as int, 12 | ); 13 | 14 | Map _$RefreshTokenToJson(RefreshToken instance) => 15 | { 16 | 'token': instance.token, 17 | 'expiresAt': instance.expiresAt, 18 | }; 19 | -------------------------------------------------------------------------------- /lib/model/user/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | 5 | part 'user.g.dart'; 6 | 7 | @JsonSerializable() 8 | @immutable 9 | class User extends Equatable { 10 | @JsonKey(name: 'uuid') 11 | final String id; 12 | final String email; 13 | final String? firstName; 14 | final String? lastName; 15 | final DateTime? dateOfBirth; 16 | 17 | User({ 18 | required this.id, 19 | required this.email, 20 | this.firstName, 21 | this.lastName, 22 | this.dateOfBirth, 23 | }); 24 | 25 | factory User.fromJson(Map json) => _$UserFromJson(json); 26 | 27 | Map toJson() => _$UserToJson(this); 28 | 29 | @override 30 | List get props => [id, email, firstName, lastName, dateOfBirth]; 31 | 32 | @override 33 | String toString() { 34 | return 'User{' 35 | 'id: $id, ' 36 | 'email: $email, ' 37 | 'firstName: $firstName, ' 38 | 'lastName: $lastName, ' 39 | 'dateOfBirth: $dateOfBirth' 40 | '}'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/model/user/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | User _$UserFromJson(Map json) => User( 10 | id: json['uuid'] as String, 11 | email: json['email'] as String, 12 | firstName: json['firstName'] as String?, 13 | lastName: json['lastName'] as String?, 14 | dateOfBirth: json['dateOfBirth'] == null 15 | ? null 16 | : DateTime.parse(json['dateOfBirth'] as String), 17 | ); 18 | 19 | Map _$UserToJson(User instance) => { 20 | 'uuid': instance.id, 21 | 'email': instance.email, 22 | 'firstName': instance.firstName, 23 | 'lastName': instance.lastName, 24 | 'dateOfBirth': instance.dateOfBirth?.toIso8601String(), 25 | }; 26 | -------------------------------------------------------------------------------- /lib/model/user/user_credentials.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user_credentials.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | UserCredentials _$UserCredentialsFromJson(Map json) => 10 | UserCredentials( 11 | User.fromJson(json['user'] as Map), 12 | json['credentials'] == null 13 | ? null 14 | : Credentials.fromJson(json['credentials'] as Map), 15 | ); 16 | 17 | Map _$UserCredentialsToJson(UserCredentials instance) => 18 | { 19 | 'user': instance.user, 20 | 'credentials': instance.credentials, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/network/chopper/authenticator/refresh_token_authenticator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:chopper/chopper.dart'; 4 | import 'package:flutter_template/network/chopper/authenticator/authenticator_helper_jwt.dart'; 5 | 6 | class RefreshTokenAuthenticator implements Authenticator { 7 | final AuthenticatorHelperJwt _authenticator; 8 | 9 | RefreshTokenAuthenticator(this._authenticator); 10 | 11 | @override 12 | FutureOr authenticate(Request request, Response response, 13 | [Request? originalRequest]) { 14 | return _authenticator 15 | .interceptResponse(request, response) 16 | .catchError((_) => null); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/network/chopper/converters/json_convert_adapter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | typedef dynamic ToJsonElement(T item); 4 | 5 | typedef T FromJsonElement(dynamic data); 6 | 7 | /// Adapts concrete types to and from JSON objects 8 | /// for use with [jsonEncode] and [jsonDecode]. 9 | class JsonConvertAdapter { 10 | final ToJsonElement toJsonElement; 11 | 12 | final FromJsonElement fromJsonElement; 13 | 14 | JsonConvertAdapter(this.toJsonElement, this.fromJsonElement); 15 | } -------------------------------------------------------------------------------- /lib/network/chopper/converters/json_type_converter_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/network/chopper/converters/json_convert_adapter.dart'; 2 | import 'package:flutter_template/network/chopper/converters/json_type_converter.dart'; 3 | 4 | import 'map_converter.dart' show FromMap, MapConverter, ToMap; 5 | 6 | /// Builder class for [JsonTypeConverter] 7 | class JsonTypeConverterBuilder { 8 | final Map> _converterMap = {}; 9 | 10 | JsonTypeConverterBuilder( 11 | [Map> converterMap = const {}]) { 12 | _converterMap.addAll(converterMap); 13 | } 14 | 15 | /// Registers `Map` converter for the concrete type. 16 | /// Use when dealing with JSON objects. 17 | JsonTypeConverterBuilder registerConverter({ 18 | required ToMap toMap, 19 | required FromMap fromMap, 20 | }) { 21 | _converterMap[T] = MapConverter(toMap, fromMap); 22 | return this; 23 | } 24 | 25 | /// Registers `dynamic` converter for the concrete type. 26 | /// Use when the JSON is a list or a primitive, not an object. 27 | JsonTypeConverterBuilder registerCustomConverter({ 28 | required ToJsonElement toJsonElement, 29 | required FromJsonElement fromJsonElement, 30 | }) { 31 | _converterMap[T] = JsonConvertAdapter(toJsonElement, fromJsonElement); 32 | return this; 33 | } 34 | 35 | JsonTypeConverter build() => 36 | JsonTypeConverter(Map.unmodifiable(_converterMap)); 37 | } 38 | -------------------------------------------------------------------------------- /lib/network/chopper/converters/map_converter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/network/chopper/converters/json_convert_adapter.dart'; 2 | 3 | typedef Map ToMap(T item); 4 | 5 | typedef T FromMap(Map data); 6 | 7 | /// Converts concrete types to and from `Map`. 8 | class MapConverter extends JsonConvertAdapter { 9 | MapConverter(ToMap toMap, FromMap fromMap) 10 | : super( 11 | (item) => toMap(item), 12 | (data) => fromMap(data), 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /lib/network/chopper/converters/response_to_type_converter.dart: -------------------------------------------------------------------------------- 1 | import 'package:chopper/chopper.dart'; 2 | import 'package:flutter_template/network/util/http_exception_code.dart'; 3 | import 'package:http/http.dart' as http; 4 | 5 | Type _typeOf() => T; 6 | 7 | extension ResponseToTypeConverter on Future> { 8 | Future toType() => this.then((response) { 9 | if (response.isSuccessful) { 10 | if (T == _typeOf()) { 11 | return Future.value(); 12 | } else if (T == http.BaseResponse || T == http.Response) { 13 | return response.base as T; 14 | } else { 15 | return response.body!; 16 | } 17 | } else { 18 | return _httpError(response); 19 | } 20 | }); 21 | 22 | Future _httpError(Response response) { 23 | return Future.error(HttpExceptionCode( 24 | 'Http error -> status code: ${response.statusCode}, response: ${response.body}', 25 | statusCode: response.statusCode, 26 | uri: response.base.request?.url, 27 | errorResponse: response.error, 28 | )); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/network/chopper/generated/chopper_user_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:chopper/chopper.dart'; 2 | import 'package:flutter_template/model/user/credentials.dart'; 3 | import 'package:flutter_template/model/user/user.dart'; 4 | import 'package:flutter_template/network/util/http_util.dart'; 5 | 6 | part 'chopper_user_api_service.chopper.dart'; 7 | 8 | /// User api service. 9 | /// 10 | /// To obtain an instance use `serviceLocator.get()` 11 | @ChopperApi() 12 | abstract class ChopperUserApiService extends ChopperService { 13 | static create([ChopperClient? client]) => _$ChopperUserApiService(client); 14 | 15 | /// Registers a user 16 | @Post(path: '/user/register') 17 | Future signUp(@Body() User user); 18 | 19 | /// Gets the logged in user 20 | @Post(path: '/user/login') 21 | @FactoryConverter(request: FormUrlEncodedConverter.requestFactory) 22 | Future> login( 23 | @Field() String username, 24 | @Field() String password, 25 | ); 26 | 27 | /// Returns user profile details 28 | @Get(path: '/user') 29 | Future> getUserProfile({ 30 | @Header(authHeaderKey) String? authHeader, 31 | }); 32 | 33 | /// Updates user profile details 34 | @Put(path: '/user') 35 | Future> updateUserProfile(@Body() User user); 36 | 37 | /// Sends a request for resetting the user's password 38 | @Post(path: '/user/reset-password') 39 | Future> resetPassword(@Body() String email); 40 | 41 | /// Adds token needed for logged in user to receive push notifications 42 | @Post(path: '/user/notifications-token') 43 | Future addNotificationsToken(@Body() String token); 44 | 45 | /// Logs out the user from server 46 | @Post(path: '/user/logout', optionalBody: true) 47 | Future> logout(); 48 | 49 | /// Deactivates the user 50 | @Delete(path: '/user') 51 | Future> deactivate(); 52 | } 53 | -------------------------------------------------------------------------------- /lib/network/chopper/generated/chopper_user_auth_api_service.chopper.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'chopper_user_auth_api_service.dart'; 4 | 5 | // ************************************************************************** 6 | // ChopperGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps 10 | class _$ChopperUserAuthApiService extends ChopperUserAuthApiService { 11 | _$ChopperUserAuthApiService([ChopperClient? client]) { 12 | if (client == null) return; 13 | this.client = client; 14 | } 15 | 16 | @override 17 | final definitionType = ChopperUserAuthApiService; 18 | 19 | @override 20 | Future> refreshToken(String refreshToken) { 21 | final $url = '/user/refresh-token'; 22 | final $body = refreshToken; 23 | final $request = Request( 24 | 'POST', 25 | $url, 26 | client.baseUrl, 27 | body: $body, 28 | ); 29 | return client.send($request); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/network/chopper/generated/chopper_user_auth_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:chopper/chopper.dart'; 2 | import 'package:flutter_template/model/user/credentials.dart'; 3 | 4 | part 'chopper_user_auth_api_service.chopper.dart'; 5 | 6 | /// User re-authentication api service. 7 | /// 8 | /// To obtain an instance use `serviceLocator.get()` 9 | @ChopperApi() 10 | abstract class ChopperUserAuthApiService extends ChopperService { 11 | 12 | static create([ChopperClient? client]) => _$ChopperUserAuthApiService(client); 13 | 14 | /// Refresh token. 15 | @Post(path: '/user/refresh-token') 16 | Future> refreshToken(@Body() String refreshToken); 17 | } 18 | -------------------------------------------------------------------------------- /lib/network/chopper/interceptors/auth_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:chopper/chopper.dart'; 2 | import 'package:flutter_template/network/chopper/authenticator/authenticator_helper_jwt.dart'; 3 | 4 | /// [RequestInterceptor] that adds authorization header on each request. 5 | class AuthInterceptor implements RequestInterceptor { 6 | final AuthenticatorHelperJwt _authenticator; 7 | 8 | AuthInterceptor(this._authenticator); 9 | 10 | @override 11 | Future onRequest(Request request) => 12 | _authenticator.interceptRequest(request); 13 | } 14 | -------------------------------------------------------------------------------- /lib/network/chopper/interceptors/error_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:chopper/chopper.dart'; 4 | import 'package:flutter_template/feature/force_update/force_update_exception.dart'; 5 | import 'package:flutter_template/feature/force_update/force_update_handler.dart'; 6 | import 'package:flutter_template/user/unauthorized_user_exception.dart'; 7 | import 'package:flutter_template/user/unauthorized_user_handler.dart'; 8 | 9 | /// [RequestInterceptor] that parses errors. 10 | class ErrorInterceptor implements ResponseInterceptor { 11 | 12 | final UnauthorizedUserHandler _unauthorizedUserHandler; 13 | final ForceUpdateHandler _forceUpdateHandler; 14 | 15 | ErrorInterceptor(this._unauthorizedUserHandler, this._forceUpdateHandler); 16 | 17 | @override 18 | FutureOr onResponse(Response response) { 19 | 20 | // todo modify when implementing force update 21 | // if (response.statusCode == 412) { 22 | // _forceUpdateHandler.onForceUpdateEvent(); 23 | // throw ForceUpdateException(response.base.reasonPhrase); 24 | // } 25 | 26 | // Unauthorized user after failed refresh token attempt 27 | if (response.statusCode == 401) { 28 | _unauthorizedUserHandler.onUnauthorizedUserEvent(); 29 | throw UnauthorizedUserException(response.base.reasonPhrase); 30 | } 31 | 32 | return response; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/network/chopper/interceptors/http_logger_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:chopper/chopper.dart'; 4 | import 'package:flutter_template/log/log.dart'; 5 | import 'package:http/http.dart' as http; 6 | 7 | /// [HttpLoggingInterceptor] that uses [Log] instead of chopper logger. 8 | class HttpLoggerInterceptor implements RequestInterceptor, ResponseInterceptor { 9 | @override 10 | FutureOr onRequest(Request request) async { 11 | final base = await request.toBaseRequest(); 12 | Log.d('--> ${base.method} ${base.url}'); 13 | base.headers.forEach((k, v) => Log.d('$k: $v')); 14 | 15 | var bytes = ''; 16 | if (base is http.Request) { 17 | final body = base.body; 18 | if (body.isNotEmpty) { 19 | Log.d(body); 20 | bytes = ' (${base.bodyBytes.length}-byte body)'; 21 | } 22 | } 23 | 24 | Log.d('--> END ${base.method}$bytes'); 25 | return request; 26 | } 27 | 28 | @override 29 | FutureOr onResponse(Response response) { 30 | final base = response.base.request; 31 | Log.d('<-- ${response.statusCode} ${base!.url}'); 32 | 33 | response.base.headers.forEach((k, v) => Log.d('$k: $v')); 34 | 35 | var bytes; 36 | if (response.base is http.Response) { 37 | final resp = response.base as http.Response; 38 | if (resp.body.isNotEmpty) { 39 | Log.d(resp.body); 40 | bytes = ' (${response.bodyBytes.length}-byte body)'; 41 | } 42 | } 43 | 44 | Log.d('--> END ${base.method}$bytes'); 45 | return response; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/network/chopper/interceptors/language_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chopper/chopper.dart'; 4 | import 'package:single_item_storage/storage.dart'; 5 | 6 | /// [RequestInterceptor] that adds preferred language header on each request. 7 | class LanguageInterceptor implements RequestInterceptor { 8 | final Storage _localeStore; 9 | 10 | LanguageInterceptor(this._localeStore); 11 | 12 | @override 13 | Future onRequest(Request request) async { 14 | final String? locale = await _localeStore.get(); 15 | if (locale != null) { 16 | return applyHeader(request, HttpHeaders.acceptLanguageHeader, locale); 17 | } else { 18 | return request; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/network/chopper/interceptors/version_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:chopper/chopper.dart'; 2 | import 'package:flutter_template/network/util/http_util.dart'; 3 | import 'package:package_info/package_info.dart'; 4 | 5 | /// [RequestInterceptor] that adds the supported app version header on each request. 6 | class VersionInterceptor implements RequestInterceptor { 7 | final PackageInfo? _packageInfo; 8 | 9 | VersionInterceptor([this._packageInfo]); 10 | 11 | @override 12 | Future onRequest(Request request) async { 13 | if (_packageInfo != null) { 14 | return applyHeader( 15 | request, 16 | versionHeaderKey, 17 | headerAppVersionValue(_packageInfo!), 18 | ); 19 | } else { 20 | return request; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/network/user_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/model/user/credentials.dart'; 2 | import 'package:flutter_template/model/user/user.dart'; 3 | import 'package:flutter_template/network/chopper/generated/chopper_user_api_service.dart'; 4 | import 'package:flutter_template/network/chopper/converters/response_to_type_converter.dart'; 5 | 6 | /// User api service. 7 | /// 8 | /// To obtain an instance use `serviceLocator.get()` 9 | class UserApiService { 10 | final ChopperUserApiService _chopper; 11 | 12 | UserApiService(this._chopper); 13 | 14 | /// Registers a user 15 | Future signUp(User user) => _chopper.signUp(user).toType(); 16 | 17 | /// Gets the logged in user 18 | Future login(String username, String password) => 19 | _chopper.login(username, password).toType(); 20 | 21 | /// Returns user profile details 22 | Future getUserProfile({String? authHeader}) => 23 | _chopper.getUserProfile(authHeader: authHeader).toType(); 24 | 25 | /// Updates user profile details 26 | Future updateUserProfile(User user) => 27 | _chopper.updateUserProfile(user).toType(); 28 | 29 | /// Sends a request for resetting the user's password 30 | Future resetPassword(String email) => 31 | _chopper.resetPassword(email).toType(); 32 | 33 | /// Adds token needed for logged in user to receive push notifications 34 | Future addNotificationsToken(String token) => 35 | _chopper.addNotificationsToken(token).toType(); 36 | 37 | /// Logs out the user from server 38 | Future logout() => _chopper.logout().toType(); 39 | 40 | /// Deactivates the user 41 | Future deactivate() => _chopper.deactivate().toType(); 42 | } 43 | -------------------------------------------------------------------------------- /lib/network/user_auth_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/model/user/credentials.dart'; 2 | import 'package:flutter_template/network/chopper/converters/response_to_type_converter.dart'; 3 | import 'package:flutter_template/network/chopper/generated/chopper_user_auth_api_service.dart'; 4 | 5 | /// User re-authentication api service. 6 | /// 7 | /// To obtain an instance use `serviceLocator.get()` 8 | class UserAuthApiService { 9 | final ChopperUserAuthApiService _chopper; 10 | 11 | UserAuthApiService(this._chopper); 12 | 13 | /// Refresh token. 14 | Future refreshToken(String refreshToken) => 15 | _chopper.refreshToken(refreshToken).toType(); 16 | } 17 | -------------------------------------------------------------------------------- /lib/network/util/http_exception_code.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | class HttpExceptionCode extends HttpException { 4 | final int? statusCode; 5 | final dynamic errorResponse; 6 | 7 | HttpExceptionCode( 8 | String message, { 9 | Uri? uri, 10 | required this.statusCode, 11 | this.errorResponse = 'no error response', 12 | }) : super(message, uri: uri); 13 | 14 | @override 15 | String toString() { 16 | return 'HttpExceptionCode{' 17 | 'message: $message, ' 18 | 'uri: $uri, ' 19 | 'statusCode: $statusCode, ' 20 | 'errorResponse: $errorResponse}'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/network/util/network_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:connectivity/connectivity.dart'; 4 | 5 | /// Network connectivity utils. 6 | /// 7 | /// To obtain instance use serviceLocator.get() 8 | class NetworkUtils { 9 | final Connectivity _connectivity; 10 | 11 | NetworkUtils(this._connectivity); 12 | 13 | Stream get connectionUpdates => 14 | _connectivity.onConnectivityChanged; 15 | 16 | Future getConnectivityResult() => 17 | _connectivity.checkConnectivity(); 18 | 19 | /// There is no guarantee that the user has network connection 20 | /// it only returns true if the device is connected with wi-fi or mobile data 21 | Future isConnected() async { 22 | var result = await _connectivity.checkConnectivity(); 23 | return result != ConnectivityResult.none; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/notifications/data/filter/message_filter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/notifications/data/model/message.dart'; 2 | 3 | /// Filters a remote [Message] to not be handled by [MessageHandler]. 4 | /// 5 | /// Register a global message filter in [DataNotificationManager]. 6 | abstract class MessageFilter { 7 | /// Return false to discard the message, true otherwise. 8 | Future filterMessage(Message message); 9 | } 10 | 11 | /// Filters all notifications if there is logged in user only. 12 | class LoggedInUserOnlyFilter implements MessageFilter { 13 | late final Future Function() isUserLoggedIn; 14 | 15 | @override 16 | Future filterMessage(Message _) => isUserLoggedIn(); 17 | } 18 | 19 | /// You shall not pass filter. 20 | class DiscardAllFilter implements MessageFilter { 21 | @override 22 | Future filterMessage(Message _) async => false; 23 | } 24 | -------------------------------------------------------------------------------- /lib/notifications/data/handler/message_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/notifications/data/model/message.dart'; 2 | 3 | /// Handler for a single push notification [Message]. 4 | /// 5 | /// A handler does an action when a push notification arrives. For example: 6 | /// - display a notification 7 | /// - navigate to specific screen 8 | /// - call another app component 9 | /// - make a network call 10 | /// 11 | /// Extend this class to implement handling for a single or multiple 12 | /// message types ([MessageType]). 13 | /// 14 | /// Mind that the app could be terminated or in the background. 15 | /// See https://firebase.flutter.dev/docs/messaging/usage/#background-messages 16 | /// 17 | /// Register all message handlers in [DataNotificationConsumerFactory]. 18 | abstract class MessageHandler { 19 | //Fixme the generic type is not propagated in data notification consumer 20 | 21 | /// Handle message while the app is in foreground 22 | Future handleMessage(E message); 23 | 24 | /// Handle message while the app is in background 25 | /// Mind that this runs on separate isolate on Android: 26 | /// - you can't do UI updates 27 | /// - you need to create all necessary components since the memory is not shared 28 | Future handleBackgroundMessage(E message); 29 | 30 | /// Handle app opened from message in any running or terminated state 31 | Future handleAppOpenedFromMessage(E message, String? action); 32 | } 33 | -------------------------------------------------------------------------------- /lib/notifications/data/model/message.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter_template/notifications/data/model/message_type.dart'; 4 | import 'package:json_annotation/json_annotation.dart'; 5 | 6 | part 'message.g.dart'; 7 | 8 | /// Push notification base message. Extend to add more data. 9 | @immutable 10 | @JsonSerializable() 11 | class Message extends Equatable { 12 | @JsonKey(toJson: typeToJson, fromJson: typeFromJson) 13 | final MessageType type; 14 | final String? messageId; 15 | final String? title; 16 | final String? body; 17 | final bool? fullScreenIntent; //android only 18 | 19 | Message({ 20 | this.type = MessageType.UNKNOWN, 21 | this.messageId, 22 | this.title, 23 | this.body, 24 | this.fullScreenIntent, 25 | }); 26 | 27 | factory Message.fromJson(Map json) => 28 | _$MessageFromJson(json); 29 | 30 | Map toJson() => _$MessageToJson(this); 31 | 32 | @override 33 | List get props => [messageId, type, title, body]; 34 | 35 | @override 36 | String toString() { 37 | return 'Message{' 38 | 'type: $type, ' 39 | 'messageId: $messageId, ' 40 | 'title: $title, ' 41 | 'body: $body, ' 42 | 'fullScreenIntent (Android): $fullScreenIntent' 43 | '}'; 44 | } 45 | } 46 | 47 | String typeToJson(MessageType value) => value.getKey(); 48 | 49 | MessageType typeFromJson(String value) => value.toMessageType(); 50 | -------------------------------------------------------------------------------- /lib/notifications/data/model/message.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'message.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Message _$MessageFromJson(Map json) => Message( 10 | type: json['type'] == null 11 | ? MessageType.UNKNOWN 12 | : typeFromJson(json['type'] as String), 13 | messageId: json['messageId'] as String?, 14 | title: json['title'] as String?, 15 | body: json['body'] as String?, 16 | fullScreenIntent: json['fullScreenIntent'] as bool?, 17 | ); 18 | 19 | Map _$MessageToJson(Message instance) => { 20 | 'type': typeToJson(instance.type), 21 | 'messageId': instance.messageId, 22 | 'title': instance.title, 23 | 'body': instance.body, 24 | 'fullScreenIntent': instance.fullScreenIntent, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/notifications/data/model/message_serializer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_template/notifications/data/parser/base_message_type_parser.dart'; 4 | 5 | import 'message.dart'; 6 | import 'message_type.dart'; 7 | 8 | extension SerializableMessage on Message? { 9 | static Message? deserialize(String? serialized) { 10 | if (serialized == null) return null; 11 | 12 | final Map jsonData = jsonDecode(serialized); 13 | final messageType = getTypeFromMappedData(jsonData); 14 | 15 | switch (messageType) { 16 | case MessageType.A: 17 | //return AMessage.fromJson(jsonData); 18 | case MessageType.B: 19 | //return BMessage.fromJson(jsonData); 20 | default: 21 | return Message.fromJson(jsonData); 22 | } 23 | } 24 | 25 | String? serialize() { 26 | return this != null ? jsonEncode(this!.toJson()) : null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/notifications/data/model/message_type.dart: -------------------------------------------------------------------------------- 1 | /// Enumeration that determines type of the incoming remote message 2 | enum MessageType { 3 | A, 4 | B, 5 | UNKNOWN 6 | } 7 | 8 | extension ParseMessageType on String { 9 | /// Concerts raw [String] to [MessageType]. 10 | MessageType toMessageType() { 11 | switch (this) { 12 | case 'A': 13 | return MessageType.A; 14 | case 'B': 15 | return MessageType.B; 16 | default: 17 | return MessageType.UNKNOWN; 18 | } 19 | } 20 | } 21 | 22 | extension MessageDetails on MessageType { 23 | /// Returns the remote key for the [MessageType] 24 | String getKey() { 25 | switch (this) { 26 | case MessageType.A: 27 | return 'A'; 28 | case MessageType.B: 29 | return 'B'; 30 | case MessageType.UNKNOWN: 31 | return 'n/a'; 32 | } 33 | } 34 | 35 | bool shouldShowAlert() { 36 | switch (this) { 37 | case MessageType.A: 38 | return true; 39 | case MessageType.B: 40 | return true; 41 | default: 42 | return false; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/notifications/data/notification_consumer.dart: -------------------------------------------------------------------------------- 1 | /// Consumes new push notifications as they arrive 2 | abstract class NotificationConsumer { 3 | /// Called when a new push notification arrives and app is in foreground 4 | /// 5 | /// Returns `true` if the message was handled successfully, `false` otherwise. 6 | Future onNotificationMessageForeground(dynamic remoteRawData); 7 | 8 | /// Called when a new push notification arrives and app is in background or terminated 9 | /// 10 | /// Returns `true` if the event was handled successfully, `false` otherwise. 11 | /// Please note that this code will run on a separate isolate on Android platform. 12 | Future onNotificationMessageBackground(dynamic remoteRawData); 13 | 14 | /// Called when the app is opened from a push notification, 15 | /// regardless if in foreground or terminated 16 | /// 17 | /// If the app is opened from notification action the action parameter will contain the action name 18 | /// 19 | /// Returns `true` if the event was handled successfully, `false` otherwise. 20 | Future onAppOpenedFromMessage( 21 | dynamic remoteRawData, 22 | String? action, 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /lib/notifications/fcm/firebase_user_hook.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 4 | import 'package:flutter_template/model/user/user_credentials.dart'; 5 | import 'package:flutter_template/notifications/fcm/fcm_notifications_listener.dart'; 6 | import 'package:flutter_template/user/user_event_hook.dart'; 7 | import 'package:flutter_template/util/date_time_util.dart'; 8 | 9 | /// Listens for user updates events and configures firebase accordingly. 10 | /// 11 | /// - Sets user identifier 12 | /// - Enables/Disables [FcmNotificationsListener] 13 | /// - Cleans up user data on logout 14 | class FirebaseUserHook extends UserEventHook { 15 | final FirebaseCrashlytics _crashlytics; 16 | final FcmNotificationsListener _notificationListener; 17 | 18 | FirebaseUserHook(this._crashlytics, this._notificationListener); 19 | 20 | @override 21 | Future onUserAuthorized(UserCredentials userCredentials, bool _) async { 22 | await _crashlytics.setUserIdentifier(userCredentials.user.id); 23 | 24 | if (!_notificationListener.setupInitialized) { 25 | await _notificationListener.setupPushNotifications(); 26 | } 27 | } 28 | 29 | @override 30 | Future onUserUnauthorized(bool _) async { 31 | await _notificationListener.disablePushNotifications(); 32 | await _crashlytics.setUserIdentifier('n/a: ${DateUtils.timestamp}'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/notifications/local/android_notification_channels.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/notifications/data/model/message_type.dart'; 2 | 3 | //todo modify this 4 | enum AndroidNotificationChannels { A, B, C } 5 | 6 | const Map notificationChannelKeys = { 7 | AndroidNotificationChannels.A: 'a_channel_id', 8 | AndroidNotificationChannels.B: 'b_channel_id', 9 | AndroidNotificationChannels.C: 'c_channel_id', 10 | }; 11 | 12 | const Map notificationChannelNames = { 13 | AndroidNotificationChannels.A: 'androidNotifChannelName_A', 14 | AndroidNotificationChannels.B: 'androidNotifChannelName_B', 15 | AndroidNotificationChannels.C: 'androidNotifChannelName_C', 16 | }; 17 | 18 | extension AndroidNotificationChannelsMethods on AndroidNotificationChannels { 19 | String get key => notificationChannelKeys[this]!; 20 | 21 | String get visibleName => notificationChannelNames[this]!; 22 | } 23 | 24 | AndroidNotificationChannels getChannelForMessageType(MessageType type) { 25 | switch (type) { 26 | case MessageType.A: 27 | case MessageType.B: 28 | return AndroidNotificationChannels.A; 29 | case MessageType.UNKNOWN: 30 | return AndroidNotificationChannels.B; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/notifications/local/android_notification_details.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter_template/notifications/data/model/message_type.dart'; 4 | import 'package:flutter_template/notifications/local/android_notification_channels.dart'; 5 | 6 | /// Contains notification details specific to Android 7 | class AndroidNotificationDetails { 8 | final AndroidNotificationChannels channel; 9 | final String? iconResId; 10 | final int importance; 11 | final int priority; 12 | final Color? color; 13 | final String? category; 14 | final bool fullScreenIntent; 15 | final bool wakeUpScreen; 16 | 17 | const AndroidNotificationDetails( 18 | this.channel, { 19 | this.iconResId, 20 | this.importance = 3, 21 | this.priority = 0, 22 | this.color, 23 | this.category, 24 | this.fullScreenIntent = false, 25 | this.wakeUpScreen = false, 26 | }); 27 | } 28 | 29 | //todo modify this too 30 | 31 | final typeANotificationDetails = AndroidNotificationDetails( 32 | AndroidNotificationChannels.A, 33 | category: 'call', 34 | importance: 5, //max 35 | priority: 2, //max 36 | wakeUpScreen: true, 37 | fullScreenIntent: false, //if true is indistinguishable from actual taps 38 | ); 39 | 40 | final infoNotificationDetails = AndroidNotificationDetails( 41 | AndroidNotificationChannels.B, 42 | priority: 1, //high 43 | ); 44 | 45 | AndroidNotificationDetails getNotifDetailsForMessageType(MessageType type) { 46 | switch (type) { 47 | case MessageType.A: 48 | case MessageType.B: 49 | return typeANotificationDetails; 50 | case MessageType.UNKNOWN: 51 | return infoNotificationDetails; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/notifications/local/android_notification_ids.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/notifications/data/model/message_type.dart'; 2 | 3 | //todo modify this 4 | 5 | const TYPE_A_NOTIFICATION_ID = 111111; 6 | const TYPE_B_NOTIFICATION_ID = 222222; 7 | const TYPE_C_NOTIFICATION_ID = 333333; 8 | 9 | int getMessageIdForType(MessageType type) { 10 | switch (type) { 11 | case MessageType.A: 12 | return TYPE_A_NOTIFICATION_ID; 13 | case MessageType.B: 14 | return TYPE_B_NOTIFICATION_ID; 15 | case MessageType.UNKNOWN: 16 | return DateTime.now().millisecondsSinceEpoch; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/notifications/local/local_notification_manager_aw_channels.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/notifications/local/android_notification_channels.dart'; 2 | import 'package:awesome_notifications/awesome_notifications.dart'; 3 | 4 | extension NotificationChannelsMethodsAw on AndroidNotificationChannels { 5 | static List allChannels() => _channels.values.toList(); 6 | 7 | NotificationChannel channel() => _channels[this]!; 8 | } 9 | 10 | //todo map this to android_notification_channels 11 | 12 | final Map _channels = { 13 | // A 14 | AndroidNotificationChannels.A: NotificationChannel( 15 | channelKey: AndroidNotificationChannels.A.key, 16 | channelName: AndroidNotificationChannels.A.visibleName, 17 | channelDescription: AndroidNotificationChannels.A.visibleName, 18 | importance: NotificationImportance.Max, 19 | locked: false, 20 | channelShowBadge: false, 21 | enableVibration: true, 22 | enableLights: true, 23 | playSound: true, 24 | soundSource: 'resource://raw/call', 25 | ), 26 | // B 27 | AndroidNotificationChannels.B: NotificationChannel( 28 | channelKey: AndroidNotificationChannels.B.key, 29 | channelName: AndroidNotificationChannels.B.visibleName, 30 | channelDescription: AndroidNotificationChannels.B.visibleName, 31 | importance: NotificationImportance.High, 32 | channelShowBadge: false, 33 | ), 34 | }; 35 | -------------------------------------------------------------------------------- /lib/platform_comm/app_platform_methods.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_template/log/log.dart'; 4 | import 'package:flutter_template/model/task/task_group.dart'; 5 | import 'package:flutter_template/platform_comm/platform_comm.dart'; 6 | import 'package:flutter_template/util/subscription.dart'; 7 | 8 | const String nativeLogs = 'nativeLogs'; 9 | const String platformTestMethod = 'platformTestMethod'; 10 | const String platformTestMethod2 = 'platformTestMethod2'; 11 | 12 | /// Platform methods specific for this app. 13 | extension AppPlatformMethods on PlatformComm { 14 | /// Listens for custom log messages from the platform side. 15 | Subscription listenToNativeLogs() => this.listenMethod( 16 | method: nativeLogs, callback: (logMessage) => Log.d(logMessage)); 17 | 18 | /// For testing only. 19 | Future echoMessage(String echoMessage) => 20 | this.invokeMethod(method: platformTestMethod, param: echoMessage); 21 | 22 | /// For testing only. 23 | Future echoObject(TaskGroup echoObject) => 24 | this.invokeMethod( 25 | method: platformTestMethod2, 26 | param: echoObject, 27 | serializeParam: (object) => jsonEncode(object.toJson()), 28 | deserializeResult: (data) => TaskGroup.fromJson(jsonDecode(data)), 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /lib/platform_comm/platform_callback.dart: -------------------------------------------------------------------------------- 1 | /// A call that's received from the native/platform side. 2 | typedef dynamic PlatformCallback

(P param); 3 | 4 | /// A call without params that's received from the native/platform side. 5 | typedef dynamic PlatformCallbackNoParams(); 6 | 7 | /// Internal. A platform call with raw unconverted params. 8 | typedef dynamic PlatformCallbackRaw(dynamic param); 9 | -------------------------------------------------------------------------------- /lib/resources/colors/color_palette.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class ColorPalette { 6 | 7 | // Light Theme 8 | static const Color primaryL = const Color(0xFF6200EE); 9 | static const Color primaryDarkL = const Color(0xFF3700B3); 10 | static const Color primaryLightL = const Color(0xFF9E47FF); 11 | static const Color primaryDisabledL = const Color(0x336200EE); 12 | static const Color accentL = const Color(0xFF03DAC6); 13 | 14 | // Dark Theme 15 | static const Color primaryD = const Color(0xFF121212); 16 | static const Color primaryDarkD = const Color(0xFF000000); 17 | static const Color primaryLightD = const Color(0xFF383838); 18 | static const Color primaryDisabledD = const Color(0x33121212); 19 | static const Color accentD = const Color(0xFFbb86fc); 20 | 21 | static const Color backgroundLightGreen = const Color(0xFFE2F0F1); 22 | static const Color backgroundLightGray = const Color(0xFFF5F5F5); 23 | static const Color backgroundGray = const Color(0xFFD8D8D8); 24 | 25 | static const Color white = Colors.white; 26 | static const Color black = Colors.black; 27 | static const Color textGray = const Color(0xFF8E9094); 28 | 29 | ColorPalette._(); 30 | } 31 | 32 | Color colorFromHex(String code) { 33 | String colorString = code.replaceAll(new RegExp('#'), ''); 34 | return new Color(int.parse(colorString, radix: 16) + 0xFF000000); 35 | } 36 | -------------------------------------------------------------------------------- /lib/resources/localization/l10n.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | import 'package:flutter/material.dart'; 3 | 4 | const EN = const Locale('en'); 5 | const MK = const Locale('mk'); 6 | 7 | class L10n { 8 | static Locale getLocale(String code) { 9 | switch (code) { 10 | case 'mk': 11 | return MK; 12 | case 'en': 13 | default: 14 | return EN; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/resources/localization/localization_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_template/di/service_locator.dart'; 3 | import 'package:flutter_template/feature/settings/preferences_helper.dart'; 4 | import 'package:flutter_template/resources/localization/l10n.dart'; 5 | 6 | class LocalizationNotifier extends ChangeNotifier { 7 | late Locale _locale; 8 | 9 | Locale get locale => _locale; 10 | 11 | LocalizationNotifier(String storedLanguageCode) { 12 | _locale = L10n.getLocale(storedLanguageCode); 13 | } 14 | 15 | Future setLocale(String languageCode) async { 16 | _locale = L10n.getLocale(languageCode); 17 | await serviceLocator 18 | .get() 19 | .setPreferredLanguage(_locale.languageCode); 20 | notifyListeners(); 21 | } 22 | 23 | Future clearLocale() async { 24 | _locale = L10n.getLocale('en'); 25 | await serviceLocator 26 | .get() 27 | .setPreferredLanguage(_locale.languageCode); 28 | notifyListeners(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/resources/theme/app_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../colors/color_palette.dart'; 3 | 4 | extension on ThemeData { 5 | ThemeData setCommonThemeElements() => this.copyWith( 6 | visualDensity: VisualDensity.adaptivePlatformDensity, 7 | ); 8 | } 9 | 10 | ThemeData themeLight() => ThemeData( 11 | brightness: Brightness.light, 12 | primaryColor: ColorPalette.primaryL, 13 | accentColor: ColorPalette.accentL, 14 | scaffoldBackgroundColor: ColorPalette.backgroundGray, 15 | cardColor: ColorPalette.white, 16 | elevatedButtonTheme: ElevatedButtonThemeData( 17 | style: TextButton.styleFrom( 18 | primary: ColorPalette.black, 19 | backgroundColor: ColorPalette.accentL, 20 | ), 21 | ), 22 | textButtonTheme: TextButtonThemeData( 23 | style: TextButton.styleFrom( 24 | primary: ColorPalette.accentL, 25 | ), 26 | ), 27 | textTheme: TextTheme( 28 | subtitle1: TextStyle( 29 | fontWeight: FontWeight.bold, 30 | color: ColorPalette.textGray, 31 | ), 32 | )).setCommonThemeElements(); 33 | 34 | ThemeData themeDark() => ThemeData( 35 | brightness: Brightness.dark, 36 | primaryColor: ColorPalette.primaryD, 37 | accentColor: ColorPalette.accentD, 38 | scaffoldBackgroundColor: ColorPalette.primaryLightD, 39 | cardColor: ColorPalette.primaryDisabledD, 40 | elevatedButtonTheme: ElevatedButtonThemeData( 41 | style: TextButton.styleFrom( 42 | primary: ColorPalette.black, 43 | backgroundColor: ColorPalette.accentD, 44 | ), 45 | ), 46 | textButtonTheme: TextButtonThemeData( 47 | style: TextButton.styleFrom( 48 | primary: ColorPalette.accentD, 49 | ), 50 | ), 51 | textTheme: TextTheme( 52 | subtitle1: TextStyle( 53 | fontWeight: FontWeight.bold, 54 | color: ColorPalette.white, 55 | ), 56 | )).setCommonThemeElements(); 57 | -------------------------------------------------------------------------------- /lib/resources/theme/theme_change_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_template/di/service_locator.dart'; 4 | import 'package:flutter_template/feature/settings/preferences_helper.dart'; 5 | 6 | class ThemeChangeNotifier extends ChangeNotifier { 7 | bool _isDarkTheme; 8 | 9 | ThemeChangeNotifier.darkTheme() : _isDarkTheme = true; 10 | 11 | ThemeChangeNotifier.lightTheme() : _isDarkTheme = false; 12 | 13 | ThemeChangeNotifier.systemTheme(BuildContext context) 14 | : _isDarkTheme = 15 | MediaQuery.platformBrightnessOf(context) == Brightness.dark; 16 | 17 | ThemeChangeNotifier.fromThemeMode(BuildContext context, ThemeMode themeMode) 18 | : _isDarkTheme = themeMode == ThemeMode.dark || 19 | (themeMode == ThemeMode.system && 20 | MediaQuery.platformBrightnessOf(context) == Brightness.dark); 21 | 22 | bool get isDarkTheme => _isDarkTheme; 23 | 24 | ThemeMode get getThemeMode => isDarkTheme ? ThemeMode.dark : ThemeMode.light; 25 | 26 | Future setDarkTheme() async { 27 | _isDarkTheme = true; 28 | await serviceLocator 29 | .get() 30 | .setIsDarkThemePreferred(_isDarkTheme); 31 | notifyListeners(); 32 | } 33 | 34 | Future setLightTheme() async { 35 | _isDarkTheme = false; 36 | await serviceLocator 37 | .get() 38 | .setIsDarkThemePreferred(_isDarkTheme); 39 | notifyListeners(); 40 | } 41 | 42 | /// Toggles the current theme value. Returns `true` if dark is the new theme. 43 | Future toggleTheme() async { 44 | _isDarkTheme = !_isDarkTheme; 45 | await serviceLocator 46 | .get() 47 | .setIsDarkThemePreferred(_isDarkTheme); 48 | notifyListeners(); 49 | return _isDarkTheme; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/routing/app_nav_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | 4 | @immutable 5 | abstract class AppNavState extends Equatable { 6 | const AppNavState._(); 7 | 8 | const factory AppNavState.auth() = AuthNavState; 9 | 10 | const factory AppNavState.home() = HomeNavState; 11 | 12 | const factory AppNavState.forceUpdate() = ForceUpdateNavState; 13 | 14 | @override 15 | List get props => []; 16 | 17 | @override 18 | String toString() { 19 | return this.runtimeType.toString(); 20 | } 21 | } 22 | 23 | @immutable 24 | class AuthNavState extends AppNavState { 25 | const AuthNavState() : super._(); 26 | } 27 | 28 | @immutable 29 | class HomeNavState extends AppNavState { 30 | const HomeNavState() : super._(); 31 | } 32 | 33 | @immutable 34 | class ForceUpdateNavState extends AppNavState { 35 | const ForceUpdateNavState() : super._(); 36 | } 37 | -------------------------------------------------------------------------------- /lib/routing/no_animation_transition_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | class NoAnimationTransitionDelegate extends TransitionDelegate { 4 | @override 5 | Iterable resolve({ 6 | required List newPageRouteHistory, 7 | required Map locationToExitingPageRoute, 8 | required Map> pageRouteToPagelessRoutes, 9 | }) { 10 | final List results = []; 11 | for (final RouteTransitionRecord pageRoute in newPageRouteHistory) { 12 | if (pageRoute.isWaitingForEnteringDecision) { 13 | pageRoute.markForAdd(); 14 | } 15 | results.add(pageRoute); 16 | } 17 | for (final RouteTransitionRecord exitingPageRoute in locationToExitingPageRoute.values) { 18 | if (exitingPageRoute.isWaitingForExitingDecision) { 19 | exitingPageRoute.markForRemove(); 20 | final List? pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute]; 21 | if (pagelessRoutes != null) { 22 | for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) { 23 | pagelessRoute.markForRemove(); 24 | } 25 | } 26 | } 27 | results.add(exitingPageRoute); 28 | } 29 | return results; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/user/unauthorized_user_exception.dart: -------------------------------------------------------------------------------- 1 | class UnauthorizedUserException implements Exception { 2 | final String? message; 3 | 4 | UnauthorizedUserException([this.message]); 5 | 6 | @override 7 | String toString() { 8 | return 'UnauthorizedUserException{message: $message}'; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/user/unauthorized_user_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/model/user/user_credentials.dart'; 2 | import 'package:single_item_storage/storage.dart'; 3 | 4 | import 'unauthorized_user_exception.dart'; 5 | 6 | /// Handles [UnauthorizedUserException]s. 7 | class UnauthorizedUserHandler { 8 | final Storage _userStore; 9 | 10 | UnauthorizedUserHandler(this._userStore); 11 | 12 | Future onUnauthorizedUserEvent() => _userStore.delete(); 13 | // user_manager and then global_auth_bloc will pick up this change 14 | // in user store and navigate to login screen 15 | } 16 | -------------------------------------------------------------------------------- /lib/util/app_lifecycle_observer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_template/log/log.dart'; 3 | import 'package:flutter_template/util/updates_stream.dart'; 4 | 5 | /// Monitors app lifecycle events. 6 | /// 7 | /// Subscribe to listen for updates. 8 | /// Mind to unsubscribe. 9 | /// 10 | /// The start and stop methods activate/deactivate this component globally; 11 | /// don't use them within the app. 12 | /// 13 | /// To obtain an instance use `serviceLocator.get()` 14 | class AppLifecycleObserver 15 | with WidgetsBindingObserver, UpdatesStream { 16 | 17 | bool _activated = false; 18 | AppLifecycleState _lastState = AppLifecycleState.inactive; 19 | 20 | AppLifecycleState get lastState => _lastState; 21 | 22 | /// Global method to start monitoring application state. 23 | void activate() { 24 | if (_activated) { 25 | Log.w('Warning: AppLifecycleObserver is already activated.'); 26 | return; 27 | } 28 | _activated = true; 29 | WidgetsBinding.instance!.addObserver(this); 30 | } 31 | 32 | /// Global method to stop monitoring application state. 33 | void deactivate() { 34 | if (!_activated) { 35 | Log.w('Warning: AppLifecycleObserver is already deactivated.'); 36 | return; 37 | } 38 | _activated = false; 39 | WidgetsBinding.instance!.removeObserver(this); 40 | } 41 | 42 | @override 43 | void didChangeAppLifecycleState(AppLifecycleState state) { 44 | super.didChangeAppLifecycleState(state); 45 | _lastState = state; 46 | addUpdate(state); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/util/check_result.dart: -------------------------------------------------------------------------------- 1 | /// Check result annotation. Used for methods with return vital result, 2 | /// but that might not be obvious to the developer. 3 | /// 4 | /// Example: 5 | /// immutableObject.changeStatus(); // WRONG, the result is missed 6 | /// final changedObject = immutableObject.changeStatus(); // CORRECT 7 | /// 8 | /// See https://github.com/dart-lang/linter/issues/697 9 | class CheckResult { 10 | const CheckResult(); 11 | } 12 | 13 | const CheckResult checkResult = CheckResult(); 14 | -------------------------------------------------------------------------------- /lib/util/collections_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | extension MapOperation on Map { 4 | /// Returns the first key for the provided value. 5 | /// If a match can not be found `null` is returned. 6 | K? getByValue(V value, {K? orElse()?}) { 7 | for (var key in this.keys) { 8 | if (this[key] == value) return key; 9 | } 10 | return orElse != null ? orElse() : null; 11 | } 12 | } 13 | 14 | extension FirstWhereOrElseExtension on Iterable { 15 | E? firstWhereOrElseNullable(bool Function(E) test, {E? orElse()?}) { 16 | for (E element in this) { 17 | if (test(element)) return element; 18 | } 19 | return orElse != null ? orElse() : null; 20 | } 21 | } 22 | 23 | extension ContainsRuntimeType on ListQueue { 24 | void addUniqueElement(Object? element) { 25 | if (!containsRuntimeType(element)) { 26 | add(element); 27 | } 28 | } 29 | 30 | bool containsRuntimeType(Object? element) { 31 | int length = this.length; 32 | for (int i = 0; i < length; i++) { 33 | if (elementAt(i).runtimeType == element) return true; 34 | if (length != this.length) { 35 | throw new ConcurrentModificationError(this); 36 | } 37 | } 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/util/date_time_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | extension DateFormatter on DateTime { 4 | String hourMinAndSeconds(String localeName) { 5 | return DateFormat.Hms(localeName).format(this); 6 | } 7 | 8 | String hourAndMin(String localeName) { 9 | return DateFormat.Hm(localeName).format(this); 10 | } 11 | 12 | String dayMonthYear(String localeName) { 13 | return DateFormat.yMMMMd(localeName).format(this); 14 | } 15 | 16 | String dayMonthYearHourMinute(String localeName) { 17 | return DateFormat.yMMMMd(localeName).format(this) + 18 | ', ' + 19 | DateFormat.Hm(localeName).format(this); 20 | } 21 | } 22 | 23 | extension DateOnlyCompare on DateTime { 24 | ///Compares only year, month and day components 25 | bool isSameDate(DateTime other) { 26 | return year == other.year && month == other.month && day == other.day; 27 | } 28 | } 29 | 30 | extension DateUtils on DateTime { 31 | static int get timestamp => DateTime.now().millisecondsSinceEpoch; 32 | 33 | bool get isToday { 34 | final now = DateTime.now(); 35 | return now.day == day && now.month == month && now.year == year; 36 | } 37 | 38 | bool get isTomorrow { 39 | final tomorrow = DateTime.now().add(const Duration(days: 1)); 40 | return tomorrow.day == day && 41 | tomorrow.month == month && 42 | tomorrow.year == year; 43 | } 44 | 45 | bool get isYesterday { 46 | final yesterday = DateTime.now().subtract(const Duration(days: 1)); 47 | return yesterday.day == day && 48 | yesterday.month == month && 49 | yesterday.year == year; 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /lib/util/either.dart: -------------------------------------------------------------------------------- 1 | typedef O ValueToPass(); 2 | 3 | /// Error handling helper 4 | /// 5 | /// Use [Either] to wrap your return type when there is a chance to receive [Exception] 6 | /// instead of the expected value 7 | abstract class Either { 8 | Either(this.isSuccess); 9 | 10 | final bool isSuccess; 11 | 12 | factory Either.success(O value) { 13 | return Success(value); 14 | } 15 | 16 | factory Either.error(E e) { 17 | return Error(e); 18 | } 19 | 20 | factory Either.build(ValueToPass func) { 21 | try { 22 | return Success(func()); 23 | } on E catch (e) { 24 | return Error(e); 25 | } 26 | } 27 | 28 | void expose(Function(E error) onError, Function(O success) onSuccess); 29 | } 30 | 31 | /// Success contains the value expected 32 | class Success extends Either { 33 | final O value; 34 | 35 | Success(this.value) : super(true); 36 | 37 | @override 38 | expose(Function(E error) onError, Function(O success) onSuccess) { 39 | onSuccess(value); 40 | } 41 | } 42 | 43 | /// Error contains the thrown [Exception] 44 | class Error extends Either { 45 | final E error; 46 | 47 | Error(this.error) : super(false); 48 | 49 | @override 50 | expose(Function(E error) onError, Function(O success) onSuccess) { 51 | onError(error); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/util/enum_util.dart: -------------------------------------------------------------------------------- 1 | import 'collections_util.dart'; 2 | 3 | /// Checks if item is enum 4 | bool isEnum(item) { 5 | final split = item.toString().split('.'); 6 | return split.length > 1 && split[0] == item.runtimeType.toString(); 7 | } 8 | 9 | /// Convert an enum to a string 10 | String enumToString(dynamic enumItem) { 11 | assert(enumItem != null); 12 | assert(isEnum(enumItem), 13 | 'Item $enumItem of type ${enumItem.runtimeType.toString()} is not enum'); 14 | return enumItem.toString().split('.')[1]; 15 | } 16 | 17 | /// Returns enum from a string value using a provided enum values list. 18 | /// If the value can not be found `null` is returned. 19 | E? enumFromString(List enumValues, String? value) { 20 | if (value == null) { 21 | return null; 22 | } 23 | 24 | return enumValues 25 | .firstWhereOrElseNullable((enumItem) => enumToString(enumItem) == value); 26 | } 27 | 28 | /// Returns enum from a string value using a provided lookup map. 29 | /// If the value can not be found `null` is returned. 30 | E? enumFromStringLookupMap(Map enumStringMap, String? value) { 31 | if (value == null) { 32 | return null; 33 | } 34 | 35 | Iterable?> enumEntries = enumStringMap.entries; 36 | return enumEntries 37 | .firstWhereOrElseNullable((e) => e?.value == value, orElse: () => null) 38 | ?.key; 39 | } 40 | -------------------------------------------------------------------------------- /lib/util/nullable_util.dart: -------------------------------------------------------------------------------- 1 | extension NullSafeFuture on Future { 2 | 3 | Future asNonNullable() => this.then((value) => value!); 4 | } 5 | 6 | extension NullSafeStream on Stream { 7 | 8 | Stream ignoreNullItems() => 9 | this.where((item) => item != null).map((item) => item!); 10 | } 11 | -------------------------------------------------------------------------------- /lib/util/screen_size_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | class ScreenSizeUtil { 4 | static bool isNarrow(BuildContext context) { 5 | Size screenSize = MediaQuery.of(context).size; 6 | return screenSize.width <= 380; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/util/string_util.dart: -------------------------------------------------------------------------------- 1 | extension Prettify on String { 2 | /// Shortens a long string by taking the start n chars (5 is default) 3 | /// and the last n chars (if long enough). Useful for logging/printing. 4 | String shortenForPrint([int n = 5]) { 5 | if (this.length <= 2 * n) { 6 | return this; 7 | } else { 8 | return this.substring(0, n) + '...' + this.substring(this.length - n); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/util/subscription.dart: -------------------------------------------------------------------------------- 1 | /// Service subscription. Call [cancel] to unsubscribe. 2 | class Subscription { 3 | void Function()? _cancel; 4 | 5 | Subscription({required void Function() cancel}) : _cancel = cancel; 6 | 7 | /// Cancels the subscription. 8 | /// 9 | /// Can be called once. Subsequent calls have no effect. 10 | void cancel() { 11 | _cancel?.call(); 12 | _cancel = null; 13 | } 14 | } 15 | 16 | /// Service subscription with async cancel. Call [cancel] to unsubscribe. 17 | class AsyncSubscription { 18 | Future Function()? _cancel; 19 | 20 | AsyncSubscription({required Future Function() cancel}) 21 | : _cancel = cancel; 22 | 23 | /// Cancels the subscription. 24 | /// 25 | /// Can be called once. Subsequent calls have no effect. 26 | Future cancel() async { 27 | await _cancel?.call(); 28 | _cancel = null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/util/text_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TextUtil { 4 | static Size calcTextSize(String text, TextStyle style) { 5 | final TextPainter textPainter = TextPainter( 6 | text: TextSpan(text: text, style: style), 7 | maxLines: 1, 8 | textDirection: TextDirection.ltr, 9 | )..layout(minWidth: 0, maxWidth: double.infinity); 10 | return textPainter.size; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/util/validations.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | enum ValidatorType { 4 | LENGTH, 5 | MATCH, 6 | NO_MATCH, 7 | EMAIL, 8 | NON_EMPTY, 9 | LETTERS_ONLY, 10 | NO_VALIDATION 11 | } 12 | 13 | bool isEmailValid(String value) { 14 | Pattern pattern = 15 | r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'; 16 | RegExp regex = new RegExp(pattern.toString()); 17 | return (!regex.hasMatch(value)) ? false : true; 18 | } 19 | 20 | bool isPasswordValid(String value) { 21 | return value.length >= 8; 22 | } 23 | 24 | bool doPasswordsMatch(String password, String passwordConfirm) => 25 | (password == passwordConfirm); 26 | 27 | TextInputFormatter lettersOnlyFormatter() => FilteringTextInputFormatter.deny( 28 | RegExp( 29 | "[0-9\\.\\,\\!\\@\\#\\%\\^\\&\\*\\(\\)\\_\\-\\+\\=\\{\\[\\]\\}\\;\\:\\'\\/\\>\\<\\?\"\$]"), 30 | ); 31 | -------------------------------------------------------------------------------- /lib/widgets/debug_overlay.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_template/di/service_locator.dart'; 5 | 6 | void debugOverlay(BuildContext context) { 7 | Overlay.of(context)?.insert( 8 | OverlayEntry(builder: (context) { 9 | var safePadding = MediaQuery.of(context).padding.bottom; 10 | final size = MediaQuery.of(context).size; 11 | final textSize = (TextPainter( 12 | text: TextSpan( 13 | text: 14 | serviceLocator.get(instanceName: buildVersionKey), 15 | style: Theme.of(context).textTheme.caption), 16 | maxLines: 1, 17 | textScaleFactor: MediaQuery.of(context).textScaleFactor, 18 | textDirection: TextDirection.ltr) 19 | ..layout()) 20 | .size; 21 | 22 | return Positioned( 23 | height: 56, 24 | top: size.height - max(safePadding, 20), 25 | left: (size.width - textSize.width) / 2, 26 | child: IgnorePointer( 27 | child: Text( 28 | serviceLocator.get(instanceName: buildVersionKey), 29 | style: Theme.of(context).textTheme.caption, 30 | ), 31 | ), 32 | ); 33 | }), 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /lib/widgets/flavor_banner.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_template/config/flavor_config.dart'; 3 | 4 | class FlavorBanner extends StatelessWidget { 5 | final Widget child; 6 | BannerConfig? bannerConfig; 7 | 8 | FlavorBanner({required this.child}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | if(FlavorConfig.isProduction()) return child; 13 | 14 | bannerConfig ??= _getDefaultBanner(); 15 | 16 | return Stack( 17 | children: [ 18 | child, 19 | _buildBanner(context) 20 | ], 21 | ); 22 | } 23 | 24 | BannerConfig _getDefaultBanner() { 25 | return BannerConfig( 26 | bannerName: FlavorConfig.flavorName, 27 | ); 28 | } 29 | 30 | Widget _buildBanner(BuildContext context) { 31 | return Container( 32 | width: 50, 33 | height: 50, 34 | child: CustomPaint( 35 | painter: BannerPainter( 36 | message: bannerConfig!.bannerName, 37 | textDirection: Directionality.of(context), 38 | layoutDirection: Directionality.of(context), 39 | location: BannerLocation.topStart, 40 | color: Colors.purple 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | 47 | class BannerConfig { 48 | final String bannerName; 49 | 50 | BannerConfig({required this.bannerName}); 51 | } -------------------------------------------------------------------------------- /lib/widgets/keyboard_dismissal_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Widget class representing a reusable container meant to contain a child 4 | /// that has a keyboard dismissal property (when pressed anywhere on screen 5 | /// opened keyboard should be dismissed). 6 | class KeyBoardDismissalContainer extends StatelessWidget { 7 | final Widget child; 8 | 9 | const KeyBoardDismissalContainer({ 10 | Key? key, 11 | required this.child, 12 | }) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return GestureDetector( 17 | behavior: HitTestBehavior.translucent, 18 | onTap: () { 19 | FocusScope.of(context).unfocus(); 20 | }, 21 | child: child); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/widgets/loading_overlay.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class LoadingOverlay { 7 | BuildContext _context; 8 | 9 | void hide() { 10 | Navigator.of(_context).pop(); 11 | } 12 | 13 | void show({bool shouldPop = true}) { 14 | showDialog( 15 | useSafeArea: false, 16 | context: _context, 17 | barrierDismissible: false, 18 | builder: (builderContext) => WillPopScope( 19 | onWillPop: () async => shouldPop, 20 | child: _FullScreenLoader(), 21 | ), 22 | ); 23 | } 24 | 25 | Future during(Future future) { 26 | show(); 27 | return future.whenComplete(() => hide()); 28 | } 29 | 30 | LoadingOverlay._create(this._context); 31 | 32 | factory LoadingOverlay.of(BuildContext context) { 33 | return LoadingOverlay._create(context); 34 | } 35 | } 36 | 37 | class _FullScreenLoader extends StatelessWidget { 38 | @override 39 | Widget build(BuildContext context) { 40 | final themeData = Theme.of(context); 41 | 42 | return Theme( 43 | data: themeData.copyWith( 44 | cupertinoOverrideTheme: CupertinoThemeData(brightness: Brightness.dark), 45 | ), 46 | child: Container( 47 | decoration: BoxDecoration( 48 | color: Color.fromRGBO(0, 0, 0, 0.5), 49 | ), 50 | child: Center( 51 | child: Platform.isIOS 52 | ? CupertinoActivityIndicator( 53 | radius: 15, 54 | ) 55 | : CircularProgressIndicator(), 56 | ), 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/widgets/modal_sheet_presentation.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; 5 | 6 | Future showModalSheet( 7 | BuildContext context, { 8 | required Widget content, 9 | bool dragEnabled = true, 10 | }) async { 11 | if (Platform.isIOS) { 12 | return showCupertinoModalBottomSheet( 13 | context: context, 14 | expand: true, 15 | enableDrag: dragEnabled, 16 | builder: (context) => content, 17 | ); 18 | } else { 19 | return showMaterialModalBottomSheet( 20 | context: context, 21 | enableDrag: dragEnabled, 22 | builder: (context) => content, 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/widgets/transparent_appbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TransparentAppBar extends StatelessWidget with PreferredSizeWidget { 4 | @override 5 | final Size preferredSize; 6 | final Widget? title; 7 | final Widget? action; 8 | final Widget? leading; 9 | final double leadingWidth; 10 | 11 | TransparentAppBar({ 12 | Key? key, 13 | this.title, 14 | this.action, 15 | this.leading, 16 | this.leadingWidth = 56.0, 17 | }) : preferredSize = Size.fromHeight(50.0), 18 | super(key: key); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | List actions = []; 23 | if (action != null) { 24 | actions.add(action!); 25 | } 26 | return AppBar( 27 | centerTitle: true, 28 | title: title, 29 | backgroundColor: Colors.transparent, 30 | brightness: Brightness.light, 31 | elevation: 0, 32 | iconTheme: IconThemeData( 33 | color: Colors.black, //change your color here 34 | ), 35 | leading: leading, 36 | leadingWidth: leadingWidth, 37 | actions: actions, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/dart_strange_runtime_type_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | //real life scenario - dynamic json converter: 4 | // https://github.com/webfactorymk/flutter-template/commit/6e4cac846bd54b7b1f6e8b2587b40e110fe54b33#diff-55b2e560073dacce762ef47b5007071836476ff07abd8d264297a9fcbade4245 5 | void main() { 6 | test('runtimeType type of wrapper not determined', () { 7 | Map> mapOfPrintersForAnything = Map(); 8 | 9 | mapOfPrintersForAnything['user-printer'] = 10 | PrinterWrapper((user) => print('Mr/Ms ${user.lastName}')); 11 | 12 | final dynamicUserPrinter = mapOfPrintersForAnything['user-printer']!; 13 | 14 | // if we use 15 | // dynamicUserPrinter.printer(User('Smith')); 16 | // we'd get runtime error: 17 | // type '(User) => void' is not a subtype of type '(dynamic) => void' 18 | 19 | 20 | // ---- the tricky solution ---- 21 | final userPrinter; 22 | if (dynamicUserPrinter.runtimeType == dynamicUserPrinter.runtimeType) { 23 | // gets to know the runtime type instead of using dynamic 24 | userPrinter = dynamicUserPrinter; 25 | } else { 26 | userPrinter = null; 27 | } 28 | 29 | userPrinter.printer(User('Smith')); 30 | }); 31 | } 32 | 33 | class User { 34 | final lastName; 35 | 36 | User(this.lastName); 37 | } 38 | 39 | typedef void Printer(T item); 40 | 41 | class PrinterWrapper { 42 | final Printer printer; 43 | 44 | PrinterWrapper(this.printer); 45 | } 46 | -------------------------------------------------------------------------------- /test/data/repository/tasks/tasks_cache_data_source_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/data/repository/tasks/tasks_cache_data_source.dart'; 2 | 3 | import 'tasks_data_source_base_test.dart'; 4 | 5 | void main() { 6 | executeTasksDataSourceBaseTests(() => new TasksCacheDataSource('userId')); 7 | } -------------------------------------------------------------------------------- /test/data/repository/tasks/tasks_stub_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/data/data_not_found_exception.dart'; 2 | import 'package:flutter_template/data/repository/tasks/tasks_data_source.dart'; 3 | import 'package:flutter_template/model/task/task.dart'; 4 | import 'package:flutter_template/model/task/task_group.dart'; 5 | 6 | /// Stub implementation of [TasksDataSource]. 7 | class TasksStubDataSource implements TasksDataSource { 8 | @override 9 | String get userId => 'userId'; 10 | 11 | @override 12 | Future completeTask(String taskId) => Future.value(); 13 | 14 | @override 15 | Future reopenTask(String taskId) => Future.value(); 16 | 17 | @override 18 | Future createTask(Task createTask) => Future.value(createTask); 19 | 20 | @override 21 | Future createTaskGroup(TaskGroup ctg) => Future.value(ctg); 22 | 23 | @override 24 | Future deleteAllTaskGroups() => Future.value(createTask); 25 | 26 | @override 27 | Future>> getAllTasksGrouped() => 28 | Future.error(DataNotFoundException()); 29 | 30 | @override 31 | Future getTask(String taskId) => Future.error(DataNotFoundException()); 32 | 33 | @override 34 | Future> getTaskGroups() => 35 | Future.error(DataNotFoundException()); 36 | 37 | @override 38 | Future> getTasks(String taskGroupId) => 39 | Future.error(DataNotFoundException()); 40 | 41 | @override 42 | Future deleteAllData() => Future.value(); 43 | 44 | @override 45 | Future updateTaskGroup(TaskGroup taskGroup) => Future.value(); 46 | } 47 | -------------------------------------------------------------------------------- /test/log/console_logger_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/log/console_logger.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:logger/logger.dart' as console; 4 | import 'package:mockito/annotations.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | import 'console_logger_test.mocks.dart'; 8 | 9 | const logMessage = 'log at me when i\'m talking to you'; 10 | 11 | /// Tests for [ConsoleLogger]. 12 | @GenerateMocks([console.Logger]) 13 | void main() { 14 | late MockLogger logger; 15 | late ConsoleLogger consoleLogger; 16 | 17 | setUp(() { 18 | logger = MockLogger(); 19 | consoleLogger = ConsoleLogger(logger); 20 | 21 | when(logger.log(any, any)).thenAnswer((invocation) => Future.value()); 22 | }); 23 | 24 | test('log debug, warn and error', () { 25 | //setup 26 | final Exception error = Exception('Test exp'); 27 | 28 | //execute 29 | consoleLogger.d(logMessage); 30 | consoleLogger.w(logMessage); 31 | consoleLogger.e(error); 32 | 33 | //expect and verify 34 | verify(logger.d(logMessage)).called(1); 35 | verify(logger.w(logMessage)).called(1); 36 | verify(logger.e('Exception: Test exp', error)).called(1); 37 | verifyNoMoreInteractions(logger); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /test/log/filtered_logger_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/log/abstract_logger.dart'; 2 | import 'package:flutter_template/log/filtered_logger.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | 6 | const logMessage = 'log at me when i\'m talking to you'; 7 | 8 | class MockLogger extends Mock implements AbstractLogger {} 9 | 10 | /// Tests for [FilteredLogger]. 11 | void main() { 12 | late MockLogger logger; 13 | 14 | setUp(() { 15 | logger = MockLogger(); 16 | }); 17 | 18 | test('log debug, warn and error', () { 19 | //setup 20 | final Exception error = Exception('Test exp'); 21 | final FilteredLogger filteredLogger = FilteredLogger(logger, (_) => true); 22 | 23 | //execute 24 | filteredLogger.d(logMessage); 25 | filteredLogger.w(logMessage); 26 | filteredLogger.e(error); 27 | 28 | //expect and verify 29 | verify(logger.d(logMessage)).called(1); 30 | verify(logger.w(logMessage)).called(1); 31 | verify(logger.e(error)).called(1); 32 | verifyNoMoreInteractions(logger); 33 | }); 34 | 35 | test('log filter all logs', () { 36 | //setup 37 | final Exception error = Exception('Test exp'); 38 | final FilteredLogger filteredLogger = FilteredLogger(logger, (_) => false); 39 | 40 | //execute 41 | filteredLogger.d(logMessage); 42 | filteredLogger.w(logMessage); 43 | filteredLogger.e(error); 44 | 45 | //expect and verify 46 | verifyNever(logger.d(logMessage)); 47 | verifyNever(logger.w(logMessage)); 48 | verifyNever(logger.e(error)); 49 | verifyNoMoreInteractions(logger); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /test/log/firebase_logger_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 2 | import 'package:flutter_template/log/firebase_logger.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:mockito/annotations.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | import 'firebase_logger_test.mocks.dart'; 8 | 9 | const logMessage = 'log at me when i\'m talking to you'; 10 | 11 | /// Tests for [FirebaseLogger]. 12 | @GenerateMocks([FirebaseCrashlytics]) 13 | void main() { 14 | late MockFirebaseCrashlytics crashlytics; 15 | late FirebaseLogger firebaseLogger; 16 | 17 | /// Setup called before every test 18 | setUp(() { 19 | crashlytics = MockFirebaseCrashlytics(); 20 | firebaseLogger = FirebaseLogger(crashlytics); 21 | 22 | // Stub a mock methods before interacting 23 | // Put general behavior here. For more specific behavior 24 | // put the stubs in the test methods. 25 | when(crashlytics.log(any)).thenAnswer((invocation) => Future.value()); 26 | }); 27 | 28 | // Called after every test 29 | tearDown(() { 30 | // no need to cleanup resources for the [FirebaseLogger] 31 | }); 32 | 33 | test('log debug, warn, and error', () { 34 | //setup 35 | final Exception error = Exception('Test exp'); 36 | 37 | //execute 38 | firebaseLogger.d(logMessage); 39 | firebaseLogger.w(logMessage); 40 | firebaseLogger.e(error); 41 | 42 | //expect and verify 43 | verify(crashlytics.log('(D) $logMessage')).called(1); 44 | verify(crashlytics.log('(W) $logMessage')).called(1); 45 | verify(crashlytics.log('(E) Exception: Test exp')).called(1); 46 | verifyNoMoreInteractions(crashlytics); 47 | }); 48 | 49 | test('log null', () { 50 | // ignore: null_check_always_fails 51 | expect(() => firebaseLogger.e(null!), throwsA(isInstanceOf())); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /test/log/multi_logger_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/log/abstract_logger.dart'; 2 | import 'package:flutter_template/log/multi_logger.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | 6 | const logMessage = 'log at me when i\'m talking to you'; 7 | 8 | class MockLogger extends Mock implements AbstractLogger {} 9 | 10 | /// Tests for [MultiLogger]. 11 | void main() { 12 | late MockLogger mockLogger1; 13 | late MockLogger mockLogger2; 14 | late MultiLogger logger; 15 | 16 | setUp(() { 17 | mockLogger1 = MockLogger(); 18 | mockLogger2 = MockLogger(); 19 | 20 | var loggers = [mockLogger1, mockLogger2]; 21 | logger = MultiLogger(loggers); 22 | }); 23 | 24 | test('log debug, warn and error', () { 25 | //setup 26 | final Exception error = Exception('Test exp'); 27 | 28 | //execute 29 | logger.d(logMessage); 30 | logger.w(logMessage); 31 | logger.e(error); 32 | 33 | //expect and verify 34 | verify(mockLogger1.d(logMessage)).called(1); 35 | verify(mockLogger1.w(logMessage)).called(1); 36 | verify(mockLogger1.e(error)).called(1); 37 | verifyNoMoreInteractions(mockLogger1); 38 | 39 | verify(mockLogger2.d(logMessage)).called(1); 40 | verify(mockLogger2.w(logMessage)).called(1); 41 | verify(mockLogger2.e(error)).called(1); 42 | verifyNoMoreInteractions(mockLogger2); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /test/network/chopper/converters/date_time_parse_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_template/network/chopper/converters/json_convert_adapter.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | void main() { 7 | TestWidgetsFlutterBinding.ensureInitialized(); 8 | 9 | late final JsonConvertAdapter converter; 10 | 11 | setUp(() { 12 | converter = JsonConvertAdapter( 13 | (dateTime) => dateTime.toIso8601String(), 14 | (dateTime) => DateTime.parse(dateTime), 15 | ); 16 | }); 17 | 18 | test('Decode items list', () { 19 | final List convertedResponse = 20 | (jsonDecode(jsonResponse) as Iterable) 21 | .map((item) => converter.fromJsonElement(item)) 22 | .cast() 23 | .toList(); 24 | expect(convertedResponse, equals(dateTimeList)); 25 | }); 26 | } 27 | 28 | final List dateTimeList = [ 29 | DateTime(2021, 12, 29), 30 | DateTime(2021, 12, 28), 31 | DateTime(2021, 12, 27), 32 | DateTime(2021, 12, 26), 33 | DateTime(2021, 12, 23), 34 | ]; 35 | 36 | const String jsonResponse = "[" + 37 | "\"2021-12-29\"," + 38 | "\"2021-12-28\"," + 39 | "\"2021-12-27\"," + 40 | "\"2021-12-26\"," + 41 | "\"2021-12-23\"" + 42 | "]"; 43 | -------------------------------------------------------------------------------- /test/network/chopper/converters/user_parse_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_template/model/user/user.dart'; 4 | import 'package:flutter_template/network/chopper/converters/json_convert_adapter.dart'; 5 | import 'package:flutter_template/network/chopper/converters/map_converter.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | 8 | void main() { 9 | TestWidgetsFlutterBinding.ensureInitialized(); 10 | 11 | late final JsonConvertAdapter converter; 12 | 13 | setUp(() { 14 | converter = MapConverter( 15 | (user) => user.toJson(), 16 | (data) => User.fromJson(data), 17 | ); 18 | }); 19 | 20 | test('Decode item', () { 21 | final User convertedResponse = 22 | converter.fromJsonElement(jsonDecode(jsonResponse)); 23 | expect(convertedResponse, equals(user)); 24 | }); 25 | } 26 | 27 | final User user = User(id: '1', email: 'user@example.com'); 28 | 29 | const String jsonResponse = 30 | "{" + "\"uuid\": \"1\"," + "\"email\": \"user@example.com\"" + "}"; 31 | -------------------------------------------------------------------------------- /test/network/chopper/interceptors/language_interceptor_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chopper/chopper.dart'; 4 | import 'package:flutter_template/network/chopper/interceptors/language_interceptor.dart'; 5 | import 'package:flutter_template/network/util/http_util.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:single_item_storage/cached_storage.dart'; 8 | import 'package:single_item_storage/memory_storage.dart'; 9 | import 'package:single_item_storage/storage.dart'; 10 | 11 | import '../../network_test_helper.dart'; 12 | 13 | void main() { 14 | late Storage storage; 15 | late LanguageInterceptor languageInterceptor; 16 | 17 | setUp(() { 18 | storage = CachedStorage(MemoryStorage()); 19 | languageInterceptor = LanguageInterceptor(storage); 20 | }); 21 | 22 | group('onRequest', () { 23 | test('onRequest, no locale stored', () async { 24 | // assert 25 | Request expected = Request('GET', 'task/2', 'http://example.com', 26 | headers: {authHeaderKey: 'Bearer ' + NetworkTestHelper.validToken}); 27 | 28 | // act 29 | Request actual = await languageInterceptor.onRequest(expected); 30 | 31 | // assert 32 | expect(actual, equals(expected)); 33 | expect(actual.headers[HttpHeaders.acceptLanguageHeader], null); 34 | }); 35 | 36 | test('onRequest, no locale stored', () async { 37 | // assert 38 | String expectedLocale = 'en-expected'; 39 | storage.save(expectedLocale); 40 | Request expected = Request('GET', 'task/2', 'http://example.com', 41 | headers: {authHeaderKey: 'Bearer ' + NetworkTestHelper.validToken}); 42 | 43 | // act 44 | Request actual = await languageInterceptor.onRequest(expected); 45 | 46 | // assert 47 | expect(actual, isNot(equals(expected))); 48 | expect(actual.headers[HttpHeaders.acceptLanguageHeader], expectedLocale); 49 | }); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /test/network/chopper/interceptors/version_interceptor_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:chopper/chopper.dart'; 2 | import 'package:flutter_template/network/chopper/interceptors/version_interceptor.dart'; 3 | import 'package:flutter_template/network/util/http_util.dart'; 4 | import 'package:mockito/annotations.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | import 'package:package_info/package_info.dart'; 8 | 9 | import '../../network_test_helper.dart'; 10 | import 'version_interceptor_test.mocks.dart'; 11 | 12 | @GenerateMocks([PackageInfo]) 13 | void main() { 14 | late VersionInterceptor versionInterceptor; 15 | 16 | group('onRequest', () { 17 | test('onRequest, no packageInfo', () async { 18 | // assert 19 | versionInterceptor = VersionInterceptor(); 20 | Request expected = Request('GET', 'task/2', 'http://example.com', 21 | headers: {authHeaderKey: 'Bearer ' + NetworkTestHelper.validToken}); 22 | 23 | // act 24 | Request actual = await versionInterceptor.onRequest(expected); 25 | 26 | // assert 27 | expect(actual, equals(expected)); 28 | expect(actual.headers[versionHeaderKey], null); 29 | }); 30 | 31 | test('onRequest, no locale stored', () async { 32 | // assert 33 | MockPackageInfo mockPackageInfo = MockPackageInfo(); 34 | versionInterceptor = VersionInterceptor(mockPackageInfo); 35 | when(mockPackageInfo.version).thenReturn('1-2'); 36 | Request expected = Request('GET', 'task/2', 'http://example.com', 37 | headers: {authHeaderKey: 'Bearer ' + NetworkTestHelper.validToken}); 38 | 39 | // act 40 | Request actual = await versionInterceptor.onRequest(expected); 41 | 42 | // assert 43 | expect(actual, isNot(equals(expected))); 44 | expect(actual.headers[versionHeaderKey], '1'); 45 | }); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /test/network/chopper/interceptors/version_interceptor_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.3.2 from annotations 2 | // in flutter_template/test/network/chopper/interceptors/version_interceptor_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'package:mockito/mockito.dart' as _i1; 7 | import 'package:package_info/package_info.dart' as _i2; 8 | 9 | // ignore_for_file: type=lint 10 | // ignore_for_file: avoid_redundant_argument_values 11 | // ignore_for_file: avoid_setters_without_getters 12 | // ignore_for_file: comment_references 13 | // ignore_for_file: implementation_imports 14 | // ignore_for_file: invalid_use_of_visible_for_testing_member 15 | // ignore_for_file: prefer_const_constructors 16 | // ignore_for_file: unnecessary_parenthesis 17 | // ignore_for_file: camel_case_types 18 | // ignore_for_file: subtype_of_sealed_class 19 | 20 | /// A class which mocks [PackageInfo]. 21 | /// 22 | /// See the documentation for Mockito's code generation for more information. 23 | class MockPackageInfo extends _i1.Mock implements _i2.PackageInfo { 24 | MockPackageInfo() { 25 | _i1.throwOnMissingStub(this); 26 | } 27 | 28 | @override 29 | String get appName => (super.noSuchMethod( 30 | Invocation.getter(#appName), 31 | returnValue: '', 32 | ) as String); 33 | @override 34 | String get packageName => (super.noSuchMethod( 35 | Invocation.getter(#packageName), 36 | returnValue: '', 37 | ) as String); 38 | @override 39 | String get version => (super.noSuchMethod( 40 | Invocation.getter(#version), 41 | returnValue: '', 42 | ) as String); 43 | @override 44 | String get buildNumber => (super.noSuchMethod( 45 | Invocation.getter(#buildNumber), 46 | returnValue: '', 47 | ) as String); 48 | } 49 | -------------------------------------------------------------------------------- /test/network/network_test_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/model/user/credentials.dart'; 2 | import 'package:flutter_template/model/user/refresh_token.dart'; 3 | import 'package:flutter_template/model/user/user.dart'; 4 | import 'package:flutter_template/model/user/user_credentials.dart'; 5 | 6 | class NetworkTestHelper { 7 | // Valid until 4th of May 2080. 8 | static final String validToken = 9 | 'eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjM0ODIwNTAzNzcsImlhdCI6MTYyMDEzMDM3N30.Hk3l8Ro7W8tLg8u5MJ2JToj7g67t8kgirtsu5ajhAL4'; 10 | static final String expiredToken = 11 | 'eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODg1OTQzNzcsImlhdCI6MTU1Njk3MTk3N30.fkshAmwlsMGQ4jAFw4Axfoyl6VOcVntSB5XDfdDO9b0'; 12 | 13 | static final RefreshToken validRefreshToken = RefreshToken( 14 | validToken, 15 | DateTime.now().millisecondsSinceEpoch * 5); 16 | 17 | static final RefreshToken expiredRefreshToken = RefreshToken( 18 | validToken, 19 | DateTime.now().subtract(Duration(days: 1)).millisecondsSinceEpoch); 20 | 21 | static final Credentials validCredentials = 22 | Credentials(validToken, validRefreshToken); 23 | static final Credentials expiredCredentials = 24 | Credentials(expiredToken, validRefreshToken); 25 | static final UserCredentials validUserCredentials = 26 | UserCredentials(User(id: 'username', email: 'email'), validCredentials); 27 | 28 | static final UserCredentials inValidUserCredentials = 29 | UserCredentials(User(id: 'username', email: 'email'), null); 30 | static final UserCredentials expiredUserCredentials = 31 | UserCredentials(User(id: 'username', email: 'email'), expiredCredentials); 32 | } 33 | -------------------------------------------------------------------------------- /test/network/util/network_utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:connectivity/connectivity.dart'; 2 | import 'package:flutter_template/network/util/network_utils.dart'; 3 | import 'package:mockito/annotations.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | import 'network_utils_test.mocks.dart'; 8 | 9 | @GenerateMocks([Connectivity]) 10 | void main() { 11 | late MockConnectivity mockConnectivity; 12 | late NetworkUtils networkUtils; 13 | 14 | setUp(() { 15 | mockConnectivity = MockConnectivity(); 16 | networkUtils = NetworkUtils(mockConnectivity); 17 | }); 18 | 19 | test('isConnected, called once', () async { 20 | // arrange 21 | final connectivityResult = Future.value(ConnectivityResult.mobile); 22 | when(mockConnectivity.checkConnectivity()) 23 | .thenAnswer((_) async => connectivityResult); 24 | // act 25 | final actualStatus = await networkUtils.isConnected(); 26 | 27 | // assert 28 | verify(mockConnectivity.checkConnectivity()).called(1); 29 | expect(actualStatus, true); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /test/network/util/network_utils_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.3.2 from annotations 2 | // in flutter_template/test/network/util/network_utils_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i3; 7 | 8 | import 'package:connectivity/connectivity.dart' as _i2; 9 | import 'package:connectivity_platform_interface/connectivity_platform_interface.dart' 10 | as _i4; 11 | import 'package:mockito/mockito.dart' as _i1; 12 | 13 | // ignore_for_file: type=lint 14 | // ignore_for_file: avoid_redundant_argument_values 15 | // ignore_for_file: avoid_setters_without_getters 16 | // ignore_for_file: comment_references 17 | // ignore_for_file: implementation_imports 18 | // ignore_for_file: invalid_use_of_visible_for_testing_member 19 | // ignore_for_file: prefer_const_constructors 20 | // ignore_for_file: unnecessary_parenthesis 21 | // ignore_for_file: camel_case_types 22 | // ignore_for_file: subtype_of_sealed_class 23 | 24 | /// A class which mocks [Connectivity]. 25 | /// 26 | /// See the documentation for Mockito's code generation for more information. 27 | class MockConnectivity extends _i1.Mock implements _i2.Connectivity { 28 | MockConnectivity() { 29 | _i1.throwOnMissingStub(this); 30 | } 31 | 32 | @override 33 | _i3.Stream<_i4.ConnectivityResult> get onConnectivityChanged => 34 | (super.noSuchMethod( 35 | Invocation.getter(#onConnectivityChanged), 36 | returnValue: _i3.Stream<_i4.ConnectivityResult>.empty(), 37 | ) as _i3.Stream<_i4.ConnectivityResult>); 38 | @override 39 | _i3.Future<_i4.ConnectivityResult> checkConnectivity() => (super.noSuchMethod( 40 | Invocation.method( 41 | #checkConnectivity, 42 | [], 43 | ), 44 | returnValue: _i3.Future<_i4.ConnectivityResult>.value( 45 | _i4.ConnectivityResult.wifi), 46 | ) as _i3.Future<_i4.ConnectivityResult>); 47 | } 48 | -------------------------------------------------------------------------------- /test/platform_comm/test_method_channel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | class TestMethodChannel implements MethodChannel { 4 | Future Function(MethodCall call)? handler; 5 | 6 | TestMethodChannel(String name); 7 | 8 | @override 9 | void setMethodCallHandler(Future Function(MethodCall call)? handler) { 10 | this.handler = handler; 11 | } 12 | 13 | @override 14 | BinaryMessenger get binaryMessenger => throw UnimplementedError(); 15 | 16 | @override 17 | bool checkMethodCallHandler(Future Function(MethodCall call)? handler) { 18 | throw UnimplementedError(); 19 | } 20 | 21 | @override 22 | bool checkMockMethodCallHandler(Future Function(MethodCall call)? handler) { 23 | throw UnimplementedError(); 24 | } 25 | 26 | @override 27 | MethodCodec get codec => throw UnimplementedError(); 28 | 29 | @override 30 | Future?> invokeListMethod(String method, [arguments]) { 31 | throw UnimplementedError(); 32 | } 33 | 34 | @override 35 | Future?> invokeMapMethod(String method, [arguments]) { 36 | throw UnimplementedError(); 37 | } 38 | 39 | @override 40 | Future invokeMethod(String method, [arguments]) { 41 | throw UnimplementedError(); 42 | } 43 | 44 | @override 45 | String get name => throw UnimplementedError(); 46 | 47 | @override 48 | void setMockMethodCallHandler(Future? Function(MethodCall call)? handler) {} 49 | } 50 | -------------------------------------------------------------------------------- /test/user/test_user_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/model/user/user_credentials.dart'; 2 | import 'package:flutter_template/network/user_api_service.dart'; 3 | import 'package:flutter_template/user/user_event_hook.dart'; 4 | import 'package:flutter_template/user/user_manager.dart'; 5 | import 'package:single_item_storage/observed_storage.dart'; 6 | 7 | class TestUserManager extends UserManager { 8 | TestUserManager( 9 | UserApiService apiService, 10 | ObservedStorage userStore, { 11 | Iterable> userEventHooks = const [], 12 | }) : super( 13 | apiService, 14 | userStore, 15 | userEventHooks: userEventHooks, 16 | ); 17 | 18 | @override 19 | Future isLoggedIn() => getLoggedInUser() 20 | .then((userCredentials) => userCredentials?.credentials != null); 21 | } 22 | -------------------------------------------------------------------------------- /test/util/collections_util_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/util/collections_util.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | /// Tests for collection utils. 5 | void main() { 6 | final Map map = Map.unmodifiable({5: 'five', 10: 'ten'}); 7 | 8 | group('getByValue', () { 9 | test('get existing', () { 10 | int actual = map.getByValue('ten')!; 11 | expect(actual, equals(10)); 12 | }); 13 | 14 | test('get non-existent', () { 15 | int? actual = map.getByValue('imaginary-number'); 16 | expect(actual, isNull); 17 | }); 18 | 19 | test('get or else', () { 20 | int? actual = map.getByValue('imaginary-number', orElse: () => -10); 21 | expect(actual, equals(-10)); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/util/date_time_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | /// Tests for [DateTime]. 4 | void main() { 5 | const String dateTimeApiResponse = '2020-09-17T14:05:21.519Z'; 6 | 7 | test("Parse Api response (JsonSerializable)", () { 8 | final actual = DateTime.parse(dateTimeApiResponse); 9 | final expected = DateTime.utc(2020, 9, 17, 14, 05, 21, 519); 10 | 11 | expect(expected, equals(actual)); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /test/util/input_buffer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/util/input_buffer.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | /// Tests for [InputBuffer]. 5 | void main() { 6 | late InputBuffer inputBuffer; 7 | 8 | setUp(() { 9 | inputBuffer = InputBuffer(coolDown: Duration(milliseconds: 99)); 10 | }); 11 | 12 | tearDown(() { 13 | inputBuffer.close(); 14 | }); 15 | 16 | test('input buffer basic test', () async { 17 | expect( 18 | inputBuffer.consumePeriodicInputChanges(), 19 | emitsInOrder(['app', 'apple']), 20 | ); 21 | 22 | inputBuffer.reportInputChange('a'); 23 | inputBuffer.reportInputChange('app'); 24 | await Future.delayed(inputBuffer.coolDown); 25 | inputBuffer.reportInputChange('appl'); 26 | inputBuffer.reportInputChange(null); 27 | inputBuffer.reportInputChange('apple'); 28 | await Future.delayed(inputBuffer.coolDown); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/util/nullable_util_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/util/nullable_util.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | /// Tests for nullable utils. 5 | void main() { 6 | final Map map = Map.unmodifiable({5: 'five', 10: 'ten'}); 7 | 8 | group('NullSafeFuture', () { 9 | test('cast', () { 10 | Future nullableFuture = Future.value(10); 11 | 12 | Future nonNullableFuture = nullableFuture.asNonNullable(); 13 | 14 | expect(nonNullableFuture, completion(equals(10))); 15 | }); 16 | 17 | test('cast fail', () { 18 | Future nullableFuture = Future.value(null); 19 | 20 | Future nonNullableFuture = nullableFuture.asNonNullable(); 21 | 22 | expect(nonNullableFuture, throwsA(isInstanceOf())); 23 | }); 24 | 25 | test('cast redundant', () { 26 | Future nonNullableFuture1 = Future.value(10); 27 | 28 | Future nonNullableFuture2 = nonNullableFuture1.asNonNullable(); 29 | 30 | expect(nonNullableFuture2, completion(equals(10))); 31 | }); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/util/string_util_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:flutter_template/util/string_util.dart'; 3 | 4 | /// Tests for string utils. 5 | void main() { 6 | group('shortenForPrint()', () { 7 | final text = '0123456789'; 8 | 9 | test('shorten longer than n', () { 10 | final actual = text.shortenForPrint(2); 11 | final expected = '01...89'; 12 | 13 | expect(actual, equals(expected)); 14 | }); 15 | 16 | test('shorten equal to n', () { 17 | final actual = text.shortenForPrint(); //defaults to 5 18 | final expected = text; 19 | 20 | expect(actual, equals(expected)); 21 | }); 22 | 23 | test('shorten shorter than n', () { 24 | final actual = text.shortenForPrint(10); 25 | final expected = text; 26 | 27 | expect(actual, equals(expected)); 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/util/subscription_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_template/util/subscription.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | 5 | abstract class CancelFunction { 6 | void call(); 7 | } 8 | 9 | class CancelFunctionMock extends Mock implements CancelFunction {} 10 | 11 | /// Tests for [Subscription] 12 | void main() { 13 | group('synchronous', () { 14 | late Subscription subscription; 15 | late CancelFunctionMock cancelFunction; 16 | 17 | setUp(() { 18 | cancelFunction = CancelFunctionMock(); 19 | subscription = Subscription(cancel: cancelFunction); 20 | }); 21 | 22 | test('cancel called once', () { 23 | subscription.cancel(); 24 | 25 | verify(cancelFunction()).called(1); 26 | }); 27 | 28 | test('cancel called multiple times', () { 29 | subscription.cancel(); 30 | subscription.cancel(); 31 | 32 | verify(cancelFunction()).called(1); 33 | }); 34 | }); 35 | 36 | group('asynchronous', () { 37 | late AsyncSubscription subscription; 38 | late CancelFunctionMock cancelFunction; 39 | 40 | setUp(() { 41 | cancelFunction = CancelFunctionMock(); 42 | subscription = AsyncSubscription(cancel: () async => cancelFunction()); 43 | }); 44 | 45 | test('async cancel called once', () async { 46 | await subscription.cancel(); 47 | 48 | verify(cancelFunction()).called(1); 49 | }); 50 | 51 | test('cancel called multiple times', () async { 52 | await subscription.cancel(); 53 | await subscription.cancel(); 54 | 55 | verify(cancelFunction()).called(1); 56 | }); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /test_driver/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_driver/driver_extension.dart'; 2 | import 'main_test.dart' as app; 3 | 4 | // Instruments the app 5 | // See https://flutter.dev/docs/cookbook/testing/integration/introduction 6 | void main() async { 7 | // This line enables the extension. 8 | enableFlutterDriverExtension(); 9 | 10 | // Call the `main()` function of the app, or call `runApp` with 11 | // any widget you are interested in testing. 12 | await app.main(); 13 | } 14 | 15 | // To run the tests, run the following command from the root of the project: 16 | // 17 | /// fvm flutter drive --target=test_driver/app.dart --flavor=mock 18 | -------------------------------------------------------------------------------- /test_driver/app_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_driver/flutter_driver.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('PlatformComm', () { 6 | final textEchoMessage = find.byValueKey('echoMessage'); 7 | final textEchoTaskGroup = find.byValueKey('echoTaskGroup'); 8 | 9 | late FlutterDriver driver; 10 | 11 | setUpAll(() async { 12 | driver = await FlutterDriver.connect(); 13 | }); 14 | 15 | tearDownAll(() async { 16 | await driver.close(); 17 | }); 18 | 19 | test('Invoke method echo', () async { 20 | await driver.waitFor(textEchoMessage); 21 | expect(await driver.getText(textEchoMessage), equals('echo')); 22 | 23 | await driver.waitFor(textEchoTaskGroup); 24 | expect(await driver.getText(textEchoTaskGroup), 25 | equals('TaskGroup{id: TG-id, name: Test group, taskIds: [1, 2]}')); 26 | }); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /test_driver/main_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_template/config/flavor_config.dart'; 4 | import 'package:flutter_template/config/network_constants.dart'; 5 | import 'package:flutter_template/config/pre_app_config.dart'; 6 | 7 | import 'platform_comm_test_widget.dart'; 8 | 9 | Future main() async { 10 | FlavorConfig.set( 11 | Flavor.MOCK, 12 | FlavorValues( 13 | baseUrlApi: baseUrlDev + apiPrefix, 14 | ), 15 | ); 16 | 17 | await preAppConfig(); 18 | 19 | runApp(PlatformCommTestWidget()); 20 | } -------------------------------------------------------------------------------- /test_driver/platform_comm_test_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_template/di/service_locator.dart'; 4 | import 'package:flutter_template/model/task/task_group.dart'; 5 | import 'package:flutter_template/platform_comm/platform_comm.dart'; 6 | 7 | class PlatformCommTestWidget extends StatefulWidget { 8 | PlatformCommTestWidget({Key? key}) : super(key: key); 9 | 10 | @override 11 | _PlatformCommTestWidgetState createState() => _PlatformCommTestWidgetState(); 12 | } 13 | 14 | class _PlatformCommTestWidgetState extends State { 15 | String? echoMessage; 16 | TaskGroup? echoTaskGroup; 17 | 18 | @override 19 | void initState() { 20 | super.initState(); 21 | callPlatformMethods(); 22 | } 23 | 24 | Future callPlatformMethods() async { 25 | final platformComm = serviceLocator.get(); 26 | final message = await platformComm.echoMessage('echo'); 27 | final taskGroup = await platformComm 28 | .echoObject(TaskGroup('TG-id', 'Test group', List.of(['1', '2']))); 29 | 30 | setState(() { 31 | echoMessage = message; 32 | echoTaskGroup = taskGroup; 33 | }); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return Row( 39 | children: [ 40 | echoMessage != null 41 | ? Text(echoMessage!, key: Key('echoMessage')) 42 | : LinearProgressIndicator(value: null), 43 | echoTaskGroup != null 44 | ? Text(echoTaskGroup?.toString() ?? '', key: Key('echoTaskGroup')) 45 | : LinearProgressIndicator(value: null), 46 | ], 47 | ); 48 | } 49 | } 50 | --------------------------------------------------------------------------------