├── .fvm └── fvm_config.json ├── .gitattributes ├── .gitignore ├── .metadata ├── .run ├── Production [profile].run.xml ├── Production [release].run.xml ├── Production.run.xml ├── Staging [profile].run.xml ├── Staging [release].run.xml └── Staging.run.xml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TUTORIAL.md ├── __brick__ ├── .gitignore ├── .run │ ├── Production.run.xml │ └── Staging.run.xml ├── ios │ └── Podfile ├── lib │ ├── app │ │ ├── di │ │ │ └── inject_dependencies.dart │ │ ├── run_{{project_name}}_app.dart │ │ └── {{project_name}}_app.dart │ ├── common │ │ ├── error_handling │ │ │ ├── base │ │ │ │ ├── expected_exception.dart │ │ │ │ └── localized_exception.dart │ │ │ └── error_formatter.dart │ │ ├── flavor │ │ │ ├── app_build_mode.dart │ │ │ ├── flavor.dart │ │ │ ├── flavor_config.dart │ │ │ └── flavor_values.dart │ │ └── logger │ │ │ ├── custom_loggers.dart │ │ │ └── firebase_log_printer.dart │ ├── device │ │ └── di │ │ │ └── inject_dependencies.dart │ ├── domain │ │ └── di │ │ │ └── inject_dependencies.dart │ ├── main_production.dart │ ├── main_staging.dart │ ├── source_local │ │ └── di │ │ │ └── inject_dependencies.dart │ ├── source_remote │ │ └── di │ │ │ └── inject_dependencies.dart │ └── ui │ │ ├── common │ │ └── generic │ │ │ └── generic_error.dart │ │ └── home │ │ └── home_screen.dart ├── pubspec.yaml └── test │ └── widget_test.dart ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── infinum │ │ │ │ └── flutter_dasher │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── splash.png │ │ │ ├── drawable-mdpi │ │ │ └── splash.png │ │ │ ├── drawable-v21 │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ └── splash.png │ │ │ ├── drawable-xxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── splash.png │ │ │ ├── drawable │ │ │ ├── background.png │ │ │ └── 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-v31 │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── app_icons │ └── app_icon.png ├── png │ └── splash_center_logo.png └── svg │ ├── button_new_tweet.svg │ ├── logo.svg │ ├── navigation_bell.svg │ ├── navigation_home.svg │ ├── navigation_mail.svg │ ├── navigation_search.svg │ ├── tweet_comment.svg │ ├── tweet_like.svg │ ├── tweet_retweet.svg │ └── tweet_share.svg ├── brick.yaml ├── hooks ├── .gitignore ├── post_gen.dart ├── pre_gen.dart └── pubspec.yaml ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Production.xcconfig ├── 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 │ │ ├── LaunchBackground.imageset │ │ │ ├── Contents.json │ │ │ └── background.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h └── Staging.xcconfig ├── lib ├── app │ ├── dasher_app.dart │ └── run_dasher_app.dart ├── common │ ├── app_build_mode.dart │ ├── flavor │ │ ├── flavor.dart │ │ ├── flavor_config.dart │ │ └── flavor_values.dart │ └── model │ │ ├── authentication.dart │ │ ├── authentication.g.dart │ │ ├── new_tweet.dart │ │ ├── new_tweet.g.dart │ │ ├── tweet.dart │ │ ├── tweet.g.dart │ │ ├── user.dart │ │ └── user.g.dart ├── device │ ├── .gitkeep │ └── impl │ │ └── .gitkeep ├── domain │ ├── data │ │ ├── user_data_holder.dart │ │ └── user_data_holder.g.dart │ ├── interactor │ │ ├── dashboard │ │ │ ├── fetch_feed_interactor.dart │ │ │ ├── fetch_feed_interactor.g.dart │ │ │ └── fetch_feed_interactor_impl.dart │ │ ├── login │ │ │ ├── login_interactor.dart │ │ │ ├── login_interactor.g.dart │ │ │ └── login_interactor_impl.dart │ │ ├── new_tweet │ │ │ ├── new_tweet_interactor.dart │ │ │ ├── new_tweet_interactor.g.dart │ │ │ └── new_tweet_interactor_impl.dart │ │ └── profile │ │ │ ├── profile_tweets_interactor.dart │ │ │ ├── profile_tweets_interactor.g.dart │ │ │ └── profile_tweets_interactor_impl.dart │ └── repository │ │ ├── feed_repository.dart │ │ ├── feed_repository.g.dart │ │ ├── login_repository.dart │ │ ├── login_repository.g.dart │ │ ├── new_tweet_repository.dart │ │ ├── new_tweet_repository.g.dart │ │ ├── profile_repository.dart │ │ └── profile_repository.g.dart ├── gen │ └── assets.gen.dart ├── main_production.dart ├── main_staging.dart ├── source_dev │ ├── dev_feed_repository.dart │ └── dev_profile_repository.dart ├── source_local │ └── impl │ │ └── .gitkeep ├── source_remote │ ├── dio │ │ └── .gitkeep │ ├── impl │ │ ├── feed_repository_impl.dart │ │ ├── login_repository_impl.dart │ │ ├── new_tweet_repository_impl.dart │ │ └── profile_repository_impl.dart │ └── twitter │ │ ├── twitter_api_container.dart │ │ ├── twitter_api_container.g.dart │ │ ├── twitter_oauth_client.dart │ │ └── twitter_oauth_client.g.dart └── ui │ ├── common │ ├── bits │ │ └── request_provider │ │ │ ├── request_provider.dart │ │ │ ├── request_state.dart │ │ │ └── request_state.freezed.dart │ ├── buttons │ │ ├── primary_button.dart │ │ ├── primary_text_button.dart │ │ └── primary_variant_button.dart │ ├── dasher_bottom_navigation_bar.dart │ ├── dasher_new_tweet_button.dart │ ├── dasher_tweet.dart │ ├── dasher_tweets_list.dart │ ├── generic │ │ └── generic_error.dart │ └── look │ │ ├── look_data │ │ ├── look_data.dart │ │ └── specific_look_data │ │ │ ├── color_look_data.dart │ │ │ ├── motion_look_data.dart │ │ │ ├── shape_look_data.dart │ │ │ └── typography_look_data.dart │ │ ├── mapping │ │ └── theme_data_mapping │ │ │ └── theme_data_mapper.dart │ │ └── widget │ │ ├── look.dart │ │ ├── look_builder.dart │ │ ├── look_subtree.dart │ │ └── user_specific_color_provider.dart │ ├── dashboard │ ├── dashboard_screen.dart │ └── presenter │ │ ├── current_user_presenter.dart │ │ └── feed_request_presenter.dart │ ├── login │ ├── login_screen.dart │ └── presenter │ │ └── login_request_presenter.dart │ ├── new_tweet │ ├── new_tweet_screen.dart │ └── presenter │ │ ├── new_tweet_provider.dart │ │ └── new_tweet_request_provider.dart │ ├── profile │ ├── presenter │ │ └── profile_request_presenter.dart │ ├── profile_screen.dart │ └── widget │ │ ├── follow_counter_component.dart │ │ ├── header_bar_component.dart │ │ └── profile_info_component.dart │ └── routing │ ├── router.dart │ ├── routes.dart │ └── routes.g.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "3.16.5", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.freezed.dart -diff -gitlab-diff linguist-generated 2 | *.g.dart -diff -gitlab-diff linguist-generated 3 | *.gen.dart -diff -gitlab-diff linguist-generated 4 | *.mocks.dart -diff -gitlab-diff linguist-generated 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # VS Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | **/ios/Flutter/.last_build_id 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | 47 | # FVM related 48 | .fvm/* 49 | !.fvm/fvm_config.json -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 17 | base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 18 | - platform: android 19 | create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 20 | base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 21 | - platform: ios 22 | create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 23 | base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /.run/Production [profile].run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.run/Production [release].run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.run/Production.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.run/Staging [profile].run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.run/Staging [release].run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.run/Staging.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 2 | 3 | - A brick to create a clean Infinum architecture folder structure, as shown in Dasher app. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Infinum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /__brick__/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | .mason/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | **/ios/Flutter/.last_build_id 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .packages 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Web related 37 | lib/generated_plugin_registrant.dart 38 | 39 | # Symbolication related 40 | app.*.symbols 41 | 42 | # Obfuscation related 43 | app.*.map.json 44 | 45 | # Android Studio will place build artifacts here 46 | /android/app/debug 47 | /android/app/profile 48 | /android/app/release 49 | -------------------------------------------------------------------------------- /__brick__/.run/Production.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /__brick__/.run/Staging.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /__brick__/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '12.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 | end 41 | end 42 | -------------------------------------------------------------------------------- /__brick__/lib/app/di/inject_dependencies.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | 3 | import '../../device/di/inject_dependencies.dart' as device; 4 | import '../../domain/di/inject_dependencies.dart' as domain; 5 | import '../../source_local/di/inject_dependencies.dart' as source_local; 6 | import '../../source_remote/di/inject_dependencies.dart' as source_remote; 7 | 8 | void injectDependencies() { 9 | final getIt = GetIt.instance; 10 | 11 | device.injectDependencies(getIt); 12 | domain.injectDependencies(getIt); 13 | source_local.injectDependencies(getIt); 14 | source_remote.injectDependencies(getIt); 15 | } 16 | -------------------------------------------------------------------------------- /__brick__/lib/app/run_{{project_name}}_app.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:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter/services.dart'; 8 | import 'package:flutter_loggy/flutter_loggy.dart'; 9 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 10 | import 'package:get_it/get_it.dart'; 11 | import 'package:loggy/loggy.dart'; 12 | import '../app/di/inject_dependencies.dart' as app; 13 | import '{{project_name}}_app.dart'; 14 | import '../common/flavor/app_build_mode.dart'; 15 | import '../common/flavor/flavor_config.dart'; 16 | import '../common/logger/firebase_log_printer.dart'; 17 | import '../ui/home/home_screen.dart'; 18 | 19 | Future run{{project_name.pascalCase()}}App() async { 20 | await runZonedGuarded>(() async { 21 | // await Firebase.initializeApp(); 22 | 23 | // injection 24 | _injectAppBuildMode(); 25 | app.injectDependencies(); 26 | 27 | // pre-startup initialization 28 | _initLoggy(); 29 | _setupErrorCapture(); 30 | _lockOrientation(); 31 | // final locale = await _getSavedLocale(); 32 | 33 | SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarBrightness: Brightness.light)); 34 | runApp(ProviderScope(child: {{project_name.pascalCase()}}App(HomeScreen()))); 35 | }, (dynamic error, StackTrace stackTrace) async { 36 | await FlavorConfig.submitError( 37 | error, 38 | stackTrace: stackTrace, 39 | ); 40 | }); 41 | } 42 | 43 | void _injectAppBuildMode() { 44 | final appBuildMode = _determineAppBuildMode(); 45 | GetIt.instance.registerSingleton(appBuildMode); 46 | } 47 | 48 | void _initLoggy() { 49 | final minimumLogLevel = _determineMinimumLogLevel(); 50 | final appBuildMode = GetIt.instance.get(); 51 | 52 | Loggy.initLoggy( 53 | logPrinter: (appBuildMode == AppBuildMode.release) ? const FirebaseLogPrinter() : const PrettyDeveloperPrinter(), 54 | logOptions: LogOptions(minimumLogLevel), 55 | filters: [ 56 | if (appBuildMode == AppBuildMode.release) ReleaseLogFilter(), 57 | ], 58 | ); 59 | } 60 | 61 | void _setupErrorCapture() { 62 | FlutterError.onError = (FlutterErrorDetails details) async { 63 | final flavour = GetIt.instance.get(); 64 | if (flavour.flavor == AppBuildMode.debug) { 65 | FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; 66 | 67 | FlutterError.dumpErrorToConsole(details); 68 | } else { 69 | Zone.current.handleUncaughtError(details.exception, details.stack ?? StackTrace.empty); 70 | } 71 | }; 72 | } 73 | 74 | void _lockOrientation() { 75 | SystemChrome.setPreferredOrientations([ 76 | DeviceOrientation.portraitDown, 77 | DeviceOrientation.portraitUp, 78 | ]); 79 | } 80 | 81 | AppBuildMode _determineAppBuildMode() { 82 | if (kDebugMode) { 83 | return AppBuildMode.debug; 84 | } else if (kProfileMode) { 85 | return AppBuildMode.profile; 86 | } else { 87 | return AppBuildMode.release; 88 | } 89 | } 90 | 91 | LogLevel _determineMinimumLogLevel() { 92 | final appBuildMode = GetIt.instance.get(); 93 | switch (appBuildMode) { 94 | case AppBuildMode.debug: 95 | return LogLevel.all; 96 | default: 97 | return LogLevel.warning; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /__brick__/lib/app/{{project_name}}_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import '../ui/common/generic/generic_error.dart'; 4 | {{#brick_look}}import '../ui/common/look/mapping/theme_data_mapping/theme_data_mapper.dart'; 5 | import '../ui/common/look/widget/look.dart'; 6 | import '../ui/common/look/widget/look_subtree.dart';{{/brick_look}} 7 | 8 | class {{project_name.pascalCase()}}App extends HookConsumerWidget { 9 | {{project_name.pascalCase()}}App(this.screen, {Key? key}) : super(key: key); 10 | 11 | final GlobalKey _navigatorKey = GlobalKey(); 12 | 13 | final Widget screen; 14 | {{#brick_look}}@override 15 | Widget build(BuildContext context, WidgetRef ref) { 16 | return LookSubtree( 17 | child: Builder(builder: (context) { 18 | return MaterialApp( 19 | debugShowCheckedModeBanner: false, 20 | color: Look.of(context).color.background, 21 | useInheritedMediaQuery: true, 22 | navigatorKey: _navigatorKey, 23 | theme: ThemeDataMapper.map(Look.of(context)), 24 | // locale: _languageProvider.locale ?? locale, 25 | builder: _builder, 26 | home: screen, 27 | ); 28 | }), 29 | ); 30 | }{{/brick_look}}{{^brick_look}} 31 | @override 32 | Widget build(BuildContext context, WidgetRef ref) { 33 | return MaterialApp( 34 | debugShowCheckedModeBanner: false, 35 | color: Colors.white, 36 | useInheritedMediaQuery: true, 37 | navigatorKey: _navigatorKey, 38 | // locale: _languageProvider.locale ?? locale, 39 | builder: _builder, 40 | home: screen, 41 | ); 42 | }{{/brick_look}} 43 | 44 | Widget _builder(BuildContext context, Widget? child) { 45 | _createErrorWidget(context); 46 | return child ?? const SizedBox.shrink(); 47 | } 48 | 49 | void _createErrorWidget(BuildContext context) { 50 | ErrorWidget.builder = (FlutterErrorDetails errorDetails) { 51 | return Card(margin: const EdgeInsets.all(16), child: GenericError('Unexpected error')); 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /__brick__/lib/common/error_handling/base/expected_exception.dart: -------------------------------------------------------------------------------- 1 | /// Interface that just marks that exception is part of normal user flow and shouldn't be reported to crashlytics 2 | abstract class ExpectedException {} 3 | -------------------------------------------------------------------------------- /__brick__/lib/common/error_handling/base/localized_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | abstract class LocalizedException extends Exception { 4 | factory LocalizedException([String? message]) => LocalizedException(message); 5 | 6 | String toLocalizedMessage(BuildContext context); 7 | } 8 | -------------------------------------------------------------------------------- /__brick__/lib/common/error_handling/error_formatter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | import 'base/localized_exception.dart'; 4 | 5 | /// Formats the exception to user readable message 6 | class ErrorFormatter { 7 | ErrorFormatter._(); 8 | 9 | static String format(Exception exception, {required BuildContext context}) { 10 | if (exception is LocalizedException) { 11 | return exception.toLocalizedMessage(context); 12 | } else { 13 | return 'Unexpected error'; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /__brick__/lib/common/flavor/app_build_mode.dart: -------------------------------------------------------------------------------- 1 | enum AppBuildMode { 2 | debug, 3 | profile, 4 | release, 5 | } 6 | -------------------------------------------------------------------------------- /__brick__/lib/common/flavor/flavor.dart: -------------------------------------------------------------------------------- 1 | enum Flavor { 2 | production, 3 | staging, 4 | } 5 | -------------------------------------------------------------------------------- /__brick__/lib/common/flavor/flavor_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:loggy/loggy.dart'; 5 | 6 | import 'flavor.dart'; 7 | import 'flavor_values.dart'; 8 | 9 | @immutable 10 | class FlavorConfig { 11 | factory FlavorConfig({ 12 | required Flavor flavor, 13 | required FlavorValues values, 14 | required String name, 15 | }) { 16 | _instance = FlavorConfig._internal(flavor, values, name); 17 | return _instance; 18 | } 19 | 20 | FlavorConfig._internal(this.flavor, this.values, this.name) { 21 | logDebug('Running application with flavor: $flavor'); 22 | } 23 | 24 | final Flavor flavor; 25 | 26 | // final PackageInfo packageInfo; 27 | 28 | /// Flavor name formatted to show as text 29 | final String name; 30 | 31 | /// Possible flavor values that can change from flavor to flavor 32 | final FlavorValues values; 33 | 34 | /// Current instance of config 35 | static late FlavorConfig _instance; 36 | 37 | static FlavorConfig get instance => _instance; 38 | 39 | /// Return boolean for weather current build is production or staging 40 | static bool get isProduction => _instance.flavor == Flavor.production; 41 | 42 | static bool get isStaging => _instance.flavor == Flavor.staging; 43 | 44 | /// Submit error can be called from any part of the app, if sentry is set up 45 | /// that error will be sent to sentry as well 46 | static Future submitError(dynamic error, {StackTrace? stackTrace}) async { 47 | if (kDebugMode) { 48 | logDebug(stackTrace); 49 | logDebug('In debug mode. Not sending report to Crashlytics.'); 50 | return; 51 | } 52 | await FirebaseCrashlytics.instance.recordError(error, stackTrace, fatal: true).then((value) { 53 | logDebug('Crash reported to Crashlytics.'); 54 | }, onError: (dynamic e) { 55 | logDebug('Error reporting to Crashlytics. $e'); 56 | }); 57 | } 58 | 59 | static Future log(String message) async { 60 | await FirebaseCrashlytics.instance.log(message); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /__brick__/lib/common/flavor/flavor_values.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | @immutable 4 | class FlavorValues { 5 | const FlavorValues({ 6 | required this.baseUrl, 7 | }); 8 | 9 | final String baseUrl; 10 | } 11 | -------------------------------------------------------------------------------- /__brick__/lib/common/logger/custom_loggers.dart: -------------------------------------------------------------------------------- 1 | import 'package:loggy/loggy.dart'; 2 | 3 | mixin ProviderLogger implements LoggyType { 4 | @override 5 | Loggy get loggy => Loggy('Provider: $runtimeType'); 6 | } 7 | 8 | mixin InteractorLogger implements LoggyType { 9 | @override 10 | Loggy get loggy => Loggy('Interactor: $runtimeType'); 11 | } 12 | 13 | mixin RepositoryLogger implements LoggyType { 14 | @override 15 | Loggy get loggy => Loggy('Repository: $runtimeType'); 16 | } 17 | 18 | // For session related events 19 | mixin AuthenticationLoggy implements LoggyType { 20 | @override 21 | Loggy get loggy => Loggy('Authentication'); 22 | } 23 | 24 | class NotificationsLoggy implements LoggyType { 25 | @override 26 | Loggy get loggy => Loggy('Notifications'); 27 | } 28 | 29 | class DefaultLoggy implements LoggyType { 30 | @override 31 | Loggy get loggy => Loggy('Default'); 32 | } 33 | 34 | class AnalyticsLoggy implements LoggyType { 35 | @override 36 | Loggy get loggy => Loggy('Analytics event'); 37 | } 38 | -------------------------------------------------------------------------------- /__brick__/lib/common/logger/firebase_log_printer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_loggy_dio/flutter_loggy_dio.dart'; 2 | import 'package:loggy/loggy.dart'; 3 | 4 | import '../../common/error_handling/base/localized_exception.dart'; 5 | import '../flavor/flavor_config.dart'; 6 | 7 | /// This kind of logging will help in two ways: 8 | /// 9 | /// Exception is raised in interactor/repository, let's say unexpected JSON 10 | /// and our parsing breaks, the request_provider would catch that exception 11 | /// and show it to the user. Although this might be bug in our code and app 12 | /// will not work, this kind of error will not be registered on crashlytics 13 | /// since it's caught and handled. FirebaseLogPrinter would submit that 14 | /// error to Crashlytics. 15 | /// [Loggy for Crashlytics](https://github.com/infinum/flutter-bits/tree/master/loggy_crashlytics) 16 | 17 | class FirebaseLogPrinter extends LoggyPrinter { 18 | const FirebaseLogPrinter(); 19 | 20 | @override 21 | void onLog(LogRecord record) { 22 | if (record.object is LocalizedException) { 23 | print('Not logging expected exception.'); 24 | return; // Don't log expected exceptions as they are considered normal user flow 25 | } 26 | 27 | if (record.level.priority >= LogLevel.error.priority) { 28 | if (record.object is Error) { 29 | FlavorConfig.submitError(record.object, stackTrace: (record.object as Error).stackTrace); 30 | } else { 31 | FlavorConfig.submitError(record.object); 32 | } 33 | } else { 34 | final time = record.time.toIso8601String().split('T')[1]; 35 | final callerFrame = record.callerFrame == null ? '-' : '(${record.callerFrame?.location})'; 36 | FlavorConfig.log('$time $callerFrame ${record.message}'); 37 | } 38 | } 39 | } 40 | 41 | /// This works in tandem with above printer, it's filters unnecessary data that we don't need to log 42 | class ReleaseLogFilter extends LoggyFilter { 43 | @override 44 | bool shouldLog(LogLevel level, Type type) { 45 | if (type == DioLoggy) { 46 | // Don't log network traffic, it's huge amount of data and potentially sensitive 47 | return false; 48 | } 49 | 50 | return true; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /__brick__/lib/device/di/inject_dependencies.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | 3 | void injectDependencies(GetIt getIt) {} 4 | -------------------------------------------------------------------------------- /__brick__/lib/domain/di/inject_dependencies.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | 3 | void injectDependencies(GetIt getIt) {} 4 | -------------------------------------------------------------------------------- /__brick__/lib/main_production.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get_it/get_it.dart'; 3 | 4 | import 'app/run_{{project_name}}_app.dart'; 5 | import 'common/flavor/flavor.dart'; 6 | import 'common/flavor/flavor_config.dart'; 7 | import 'common/flavor/flavor_values.dart'; 8 | 9 | void main() { 10 | WidgetsFlutterBinding.ensureInitialized(); 11 | 12 | GetIt.I.registerSingleton(FlavorConfig( 13 | flavor: Flavor.production, 14 | name: 'Production', 15 | values: const FlavorValues( 16 | baseUrl: 'production URL', 17 | ), 18 | )); 19 | 20 | run{{project_name.pascalCase()}}App(); 21 | } 22 | -------------------------------------------------------------------------------- /__brick__/lib/main_staging.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get_it/get_it.dart'; 3 | 4 | import 'app/run_{{project_name}}_app.dart'; 5 | import 'common/flavor/flavor.dart'; 6 | import 'common/flavor/flavor_config.dart'; 7 | import 'common/flavor/flavor_values.dart'; 8 | 9 | void main() { 10 | WidgetsFlutterBinding.ensureInitialized(); 11 | 12 | GetIt.I.registerSingleton(FlavorConfig( 13 | flavor: Flavor.staging, 14 | name: 'Staging', 15 | values: const FlavorValues( 16 | baseUrl: 'staging URL', 17 | ), 18 | )); 19 | 20 | run{{project_name.pascalCase()}}App(); 21 | } 22 | -------------------------------------------------------------------------------- /__brick__/lib/source_local/di/inject_dependencies.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | 3 | void injectDependencies(GetIt getIt) {} 4 | -------------------------------------------------------------------------------- /__brick__/lib/source_remote/di/inject_dependencies.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | 3 | void injectDependencies(GetIt getIt) {} 4 | -------------------------------------------------------------------------------- /__brick__/lib/ui/common/generic/generic_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../common/error_handling/error_formatter.dart'; 4 | {{#brick_look}}import '../../../ui/common/look/widget/look.dart';{{/brick_look}} 5 | 6 | /// Shows generic error widget, with possibility to add retry button below it 7 | /// if [onRetry] is not null retry will be active 8 | /// 9 | /// [message] should be readable error message that will be shown to the user 10 | class GenericError extends StatelessWidget { 11 | const GenericError(this.message, {this.onRetry, Key? key}) : super(key: key); 12 | 13 | factory GenericError.exception(Exception exception, BuildContext context, {VoidCallback? onRetry, Key? key}) { 14 | return GenericError(ErrorFormatter.format(exception, context: context), onRetry: onRetry, key: key); 15 | } 16 | 17 | final String message; 18 | final VoidCallback? onRetry; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Padding( 23 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8.0), 24 | child: Center( 25 | child: Column( 26 | mainAxisSize: MainAxisSize.min, 27 | mainAxisAlignment: MainAxisAlignment.center, 28 | children: [ 29 | Column( 30 | mainAxisSize: MainAxisSize.min, 31 | mainAxisAlignment: MainAxisAlignment.center, 32 | children: [ 33 | CircleAvatar( 34 | backgroundColor: {{#brick_look}}Look.of(context).color.error.withOpacity(0.1),{{/brick_look}}{{^brick_look}}Colors.red.withOpacity(0.1),{{/brick_look}} 35 | child: Icon( 36 | Icons.error_outline, 37 | color: {{#brick_look}}Look.of(context).color.error,{{/brick_look}}{{^brick_look}}Colors.red{{/brick_look}} 38 | ), 39 | ), 40 | const SizedBox(width: 12), 41 | Text(message, textAlign: TextAlign.center, {{#brick_look}}style: Look.of(context).typography.body{{/brick_look}}), 42 | ], 43 | ), 44 | const SizedBox(height: 16), 45 | 46 | /// Show retry button below the widget if [onRetry] has been provided 47 | if (onRetry != null) _RetryButton(onRetry!), 48 | ], 49 | ), 50 | ), 51 | ); 52 | } 53 | } 54 | 55 | class _RetryButton extends StatelessWidget { 56 | const _RetryButton(this.onRetry, {Key? key}) : super(key: key); 57 | 58 | final VoidCallback onRetry; 59 | 60 | @override 61 | Widget build(BuildContext context) { 62 | return TextButton( 63 | onPressed: onRetry, 64 | child: Text('RETRY'), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /__brick__/lib/ui/home/home_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | class HomeScreen extends HookConsumerWidget { 5 | const HomeScreen({Key? key}) : super(key: key); 6 | 7 | static Route route() { 8 | return MaterialPageRoute( 9 | builder: (BuildContext context) { 10 | return const HomeScreen(); 11 | }, 12 | ); 13 | } 14 | 15 | @override 16 | Widget build(BuildContext context, WidgetRef ref) { 17 | return Scaffold( 18 | appBar: AppBar(), 19 | body: const Center( 20 | child: Text('Hello world!'), 21 | ), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /__brick__/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: infinum_architecture 2 | description: A new Flutter project with Infinum clean architecture. 3 | version: 1.0.0 4 | 5 | environment: 6 | sdk: ">=2.16.0 <3.0.0" 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | 12 | dev_dependencies: 13 | flutter_test: 14 | sdk: flutter 15 | 16 | flutter: 17 | uses-material-design: true 18 | -------------------------------------------------------------------------------- /__brick__/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | strong-mode: 5 | implicit-casts: false 6 | implicit-dynamic: false 7 | exclude: 8 | - "lib/**/*.freezed.dart" 9 | - "lib/**/*.g.dart" 10 | - "lib/**/*.gen.dart" 11 | 12 | linter: 13 | rules: 14 | - prefer_single_quotes -------------------------------------------------------------------------------- /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 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.infinum.flutter_dasher" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 50 | minSdkVersion localProperties.getProperty('flutter.minSdkVersion').toInteger() 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | 64 | flavorDimensions "flavors" 65 | productFlavors { 66 | staging { 67 | dimension "flavors" 68 | applicationIdSuffix ".staging" 69 | versionNameSuffix "-staging" 70 | } 71 | production { 72 | dimension "flavors" 73 | } 74 | } 75 | } 76 | 77 | flutter { 78 | source '../..' 79 | } 80 | 81 | dependencies { 82 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 83 | } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/infinum/flutter_dasher/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.infinum.flutter_dasher 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/android/app/src/main/res/drawable-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/android/app/src/main/res/drawable-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/android/app/src/main/res/drawable-v21/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/android/app/src/main/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/android/app/src/main/res/drawable-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/android/app/src/main/res/drawable-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/android/app/src/main/res/drawable/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 17 | 20 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /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.4-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/app_icons/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/assets/app_icons/app_icon.png -------------------------------------------------------------------------------- /assets/png/splash_center_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/assets/png/splash_center_logo.png -------------------------------------------------------------------------------- /assets/svg/button_new_tweet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/svg/navigation_bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/svg/navigation_home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/svg/navigation_mail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/svg/navigation_search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/svg/tweet_comment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/svg/tweet_like.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/svg/tweet_retweet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/svg/tweet_share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /brick.yaml: -------------------------------------------------------------------------------- 1 | name: infinum_architecture 2 | description: A brick to create a clean Infinum architecture folder structure, as shown in Dasher 3 | app. 4 | 5 | version: 1.0.0 6 | 7 | environment: 8 | mason: ">=0.1.0-dev.26 <0.1.0" 9 | 10 | repository: https://github.com/infinum/flutter-dasher 11 | 12 | vars: 13 | project_name: 14 | type: string 15 | description: Flutter project name 16 | default: example 17 | prompt: What is Flutter project name (camelCase naming)? 18 | flutter_version: 19 | type: string 20 | description: Flutter version 21 | default: stable 22 | prompt: Which Flutter version you want for this project? 23 | brick_look: 24 | type: boolean 25 | description: Install brick look 26 | default: true 27 | prompt: Do you want to add Look? 28 | brick_request_provider: 29 | type: boolean 30 | description: Install brick request_provider 31 | default: true 32 | prompt: Do you want to add Request Provider? -------------------------------------------------------------------------------- /hooks/.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool 2 | .packages 3 | pubspec.lock 4 | -------------------------------------------------------------------------------- /hooks/post_gen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:mason/mason.dart'; 4 | 5 | void run(HookContext context) async { 6 | final bool lookEnabled = context.vars['brick_look'] as bool; 7 | final bool requestProviderEnabled = context.vars['brick_request_provider'] as bool; 8 | 9 | var progress = context.logger.progress('Installing... flutter version: {{flutter_version}}'); 10 | await Process.run('fvm', ['install', '{{flutter_version}}'], runInShell: true); 11 | await Process.run('fvm', ['use', '{{flutter_version}}'], runInShell: true); 12 | progress.complete(); 13 | 14 | progress = context.logger.progress('Executing... pubspec update'); 15 | await Process.run( 16 | 'fvm', 17 | [ 18 | 'flutter', 19 | 'pub', 20 | 'add', 21 | 'alice', 22 | 'cupertino_icons', 23 | 'dio', 24 | 'firebase_crashlytics', 25 | 'flutter_hooks', 26 | 'flutter_loggy', 27 | 'flutter_loggy_dio', 28 | 'flutter_riverpod', 29 | 'get_it', 30 | 'hooks_riverpod', 31 | 'json_annotation', 32 | 'loggy', 33 | 'freezed_annotation' 34 | ], 35 | runInShell: true); 36 | await Process.run( 37 | 'fvm', 38 | [ 39 | 'flutter', 40 | 'pub', 41 | 'add', 42 | '--dev', 43 | 'build_runner', 44 | 'dart_code_metrics', 45 | 'flutter_gen_runner', 46 | 'flutter_lints', 47 | 'json_serializable', 48 | 'freezed' 49 | ], 50 | runInShell: true); 51 | progress.complete(); 52 | 53 | progress = context.logger.progress('Updating... pod repo'); 54 | await Process.run('pod', ['repo', 'update'], runInShell: true); 55 | progress.complete(); 56 | 57 | if (lookEnabled) { 58 | progress = context.logger.progress('Adding brick... look'); 59 | await Process.run('mason', ['add', 'look', '--git-url', 'https://github.com/infinum/flutter-bits.git', '--git-path', 'look/'], 60 | runInShell: true); 61 | progress.complete(); 62 | 63 | progress = context.logger.progress('Installing... look'); 64 | await Process.run('mason', ['make', 'look', '--output-dir', 'lib/ui/common'], runInShell: true); 65 | progress.complete(); 66 | } 67 | 68 | if (requestProviderEnabled) { 69 | progress = context.logger.progress('Adding brick... request_provider'); 70 | await Process.run('mason', 71 | ['add', 'request_provider', '--git-url', 'https://github.com/infinum/flutter-bits.git', '--git-path', 'request_provider/'], 72 | runInShell: true); 73 | progress.complete(); 74 | 75 | progress = context.logger.progress('Installing... request_provider'); 76 | await Process.run( 77 | 'mason', ['make', 'request_provider', '--on-conflict', 'overwrite', '--output-dir', 'lib/ui/common/bits/request_provider'], 78 | runInShell: true); 79 | progress.complete(); 80 | } 81 | 82 | progress = context.logger.progress('Cleaning bricks...'); 83 | await Process.run('mason', ['remove', 'hello'], runInShell: true); 84 | progress.complete(); 85 | 86 | progress = context.logger.progress('Updating... files structure'); 87 | await Process.run('rm', ['lib/main.dart'], runInShell: true); 88 | await Process.run('rm', ['.idea/runConfigurations/main_dart.xml'], runInShell: true); 89 | progress.complete(); 90 | } 91 | -------------------------------------------------------------------------------- /hooks/pre_gen.dart: -------------------------------------------------------------------------------- 1 | import 'package:mason/mason.dart'; 2 | 3 | void run(HookContext context) { 4 | // TODO: add pre-generation logic. 5 | } 6 | -------------------------------------------------------------------------------- /hooks/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_architecture_hooks 2 | 3 | environment: 4 | sdk: ">=2.12.0 <3.0.0" 5 | 6 | dependencies: 7 | mason: ^0.1.0-dev 8 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /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.delete 'IPHONEOS_DEPLOYMENT_TARGET' 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - flutter_inappwebview_ios (0.0.1): 4 | - Flutter 5 | - flutter_inappwebview_ios/Core (= 0.0.1) 6 | - OrderedSet (~> 5.0) 7 | - flutter_inappwebview_ios/Core (0.0.1): 8 | - Flutter 9 | - OrderedSet (~> 5.0) 10 | - flutter_native_splash (0.0.1): 11 | - Flutter 12 | - flutter_secure_storage (6.0.0): 13 | - Flutter 14 | - flutter_web_auth_2 (1.1.1): 15 | - Flutter 16 | - FMDB (2.7.5): 17 | - FMDB/standard (= 2.7.5) 18 | - FMDB/standard (2.7.5) 19 | - OrderedSet (5.0.0) 20 | - path_provider_foundation (0.0.1): 21 | - Flutter 22 | - FlutterMacOS 23 | - sqflite (0.0.3): 24 | - Flutter 25 | - FMDB (>= 2.7.5) 26 | - url_launcher_ios (0.0.1): 27 | - Flutter 28 | 29 | DEPENDENCIES: 30 | - Flutter (from `Flutter`) 31 | - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) 32 | - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) 33 | - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) 34 | - flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`) 35 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 36 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 37 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 38 | 39 | SPEC REPOS: 40 | trunk: 41 | - FMDB 42 | - OrderedSet 43 | 44 | EXTERNAL SOURCES: 45 | Flutter: 46 | :path: Flutter 47 | flutter_inappwebview_ios: 48 | :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" 49 | flutter_native_splash: 50 | :path: ".symlinks/plugins/flutter_native_splash/ios" 51 | flutter_secure_storage: 52 | :path: ".symlinks/plugins/flutter_secure_storage/ios" 53 | flutter_web_auth_2: 54 | :path: ".symlinks/plugins/flutter_web_auth_2/ios" 55 | path_provider_foundation: 56 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 57 | sqflite: 58 | :path: ".symlinks/plugins/sqflite/ios" 59 | url_launcher_ios: 60 | :path: ".symlinks/plugins/url_launcher_ios/ios" 61 | 62 | SPEC CHECKSUMS: 63 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 64 | flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 65 | flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef 66 | flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be 67 | flutter_web_auth_2: a1bc00762c408a8f80b72a538cd7ff5b601c3e71 68 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 69 | OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c 70 | path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 71 | sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a 72 | url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b 73 | 74 | PODFILE CHECKSUM: 0661a8b4d2adb53671731a5c90a8e5964fc4bf1f 75 | 76 | COCOAPODS: 1.13.0 77 | -------------------------------------------------------------------------------- /ios/Production.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Flutter/Generated.xcconfig" 3 | 4 | APP_DISPLAY_NAME=Dasher Production 5 | FLUTTER_TARGET=lib/main_production.dart 6 | -------------------------------------------------------------------------------- /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.xcodeproj/xcshareddata/xcschemes/Production.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Staging.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /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 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/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/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "background.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "LaunchImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "LaunchImage@3x.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/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /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/Base.lproj/LaunchScreen.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 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /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/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDisplayName 6 | $(APP_DISPLAY_NAME) 7 | CADisableMinimumFrameDurationOnPhone 8 | 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | flutter_dasher 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | UIStatusBarHidden 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/Staging.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Flutter/Generated.xcconfig" 3 | 4 | APP_DISPLAY_NAME=Dasher Staging 5 | FLUTTER_TARGET=lib/main_staging.dart 6 | -------------------------------------------------------------------------------- /lib/app/dasher_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_dasher/ui/routing/router.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | import '../ui/common/generic/generic_error.dart'; 6 | import '../ui/common/look/mapping/theme_data_mapping/theme_data_mapper.dart'; 7 | import '../ui/common/look/widget/look.dart'; 8 | import '../ui/common/look/widget/look_subtree.dart'; 9 | 10 | class DasherApp extends HookConsumerWidget { 11 | const DasherApp({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context, WidgetRef ref) { 15 | return LookSubtree( 16 | child: Builder(builder: (context) { 17 | return MaterialApp.router( 18 | routerConfig: router, 19 | debugShowCheckedModeBanner: false, 20 | color: Colors.white, 21 | theme: ThemeDataMapper.map(Look.of(context)), 22 | builder: _builder, 23 | ); 24 | }), 25 | ); 26 | } 27 | 28 | Widget _builder(BuildContext context, Widget? child) { 29 | _createErrorWidget(context); 30 | return child ?? const SizedBox.shrink(); 31 | } 32 | 33 | void _createErrorWidget(BuildContext context) { 34 | ErrorWidget.builder = (FlutterErrorDetails errorDetails) { 35 | return const Card(margin: EdgeInsets.all(16), child: GenericError('Unexpected error')); 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/app/run_dasher_app.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:flutter_native_splash/flutter_native_splash.dart'; 7 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 8 | 9 | import '../common/app_build_mode.dart'; 10 | import '../common/flavor/flavor_config.dart'; 11 | import 'dasher_app.dart'; 12 | 13 | // ignore_for_file: prefer-match-file-name 14 | Future runDasherApp() async { 15 | await runZonedGuarded>(() async { 16 | final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); 17 | FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); 18 | 19 | final appBuildMode = _determineAppBuildMode(); 20 | 21 | // pre-startup initialization 22 | _setupErrorCapture(appBuildMode); 23 | _lockOrientation(); 24 | // final locale = await _getSavedLocale(); 25 | 26 | SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarColor: Colors.transparent)); 27 | runApp( 28 | const ProviderScope( 29 | child: DasherApp(), 30 | ), 31 | ); 32 | FlutterNativeSplash.remove(); 33 | }, (dynamic error, StackTrace stackTrace) async { 34 | await FlavorConfig.submitError( 35 | error, 36 | stackTrace: stackTrace, 37 | ); 38 | }); 39 | } 40 | 41 | void _setupErrorCapture(AppBuildMode appBuildMode) { 42 | FlutterError.onError = (FlutterErrorDetails details) async { 43 | if (appBuildMode == AppBuildMode.debug) { 44 | FlutterError.dumpErrorToConsole(details); 45 | } else { 46 | Zone.current.handleUncaughtError(details.exception, details.stack ?? StackTrace.empty); 47 | } 48 | }; 49 | } 50 | 51 | void _lockOrientation() { 52 | SystemChrome.setPreferredOrientations([ 53 | DeviceOrientation.portraitDown, 54 | DeviceOrientation.portraitUp, 55 | ]); 56 | } 57 | 58 | AppBuildMode _determineAppBuildMode() { 59 | if (kDebugMode) { 60 | return AppBuildMode.debug; 61 | } else if (kProfileMode) { 62 | return AppBuildMode.profile; 63 | } else { 64 | return AppBuildMode.release; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/common/app_build_mode.dart: -------------------------------------------------------------------------------- 1 | enum AppBuildMode { 2 | debug, 3 | profile, 4 | release, 5 | } 6 | -------------------------------------------------------------------------------- /lib/common/flavor/flavor.dart: -------------------------------------------------------------------------------- 1 | enum Flavor { 2 | production, 3 | staging, 4 | } 5 | -------------------------------------------------------------------------------- /lib/common/flavor/flavor_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_dasher/common/flavor/flavor.dart'; 4 | import 'package:flutter_dasher/common/flavor/flavor_values.dart'; 5 | import 'package:loggy/loggy.dart'; 6 | 7 | @immutable 8 | class FlavorConfig { 9 | factory FlavorConfig({ 10 | required Flavor flavor, 11 | required FlavorValues values, 12 | required String name, 13 | }) { 14 | _instance = FlavorConfig._internal(flavor, values, name); 15 | return _instance; 16 | } 17 | 18 | FlavorConfig._internal(this.flavor, this.values, this.name) { 19 | logDebug('Running application with flavor: $flavor'); 20 | } 21 | 22 | final Flavor flavor; 23 | 24 | /// Flavor name formatted to show as text 25 | final String name; 26 | 27 | /// Possible flavor values that can change from flavor to flavor 28 | final FlavorValues values; 29 | 30 | /// Current instance of config 31 | static late FlavorConfig _instance; 32 | 33 | static FlavorConfig get instance => _instance; 34 | 35 | /// Return boolean for weather current build is production or staging 36 | static bool get isProduction => _instance.flavor == Flavor.production; 37 | 38 | static bool get isStaging => _instance.flavor == Flavor.staging; 39 | 40 | /// Submit error can be called from any part of the app, if sentry is set up 41 | /// that error will be sent to sentry as well 42 | static Future submitError(dynamic error, 43 | {StackTrace? stackTrace}) async { 44 | // Report to crashlytics 45 | } 46 | 47 | static Future log(String message) async { 48 | // Log to crashlytics 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/common/flavor/flavor_values.dart: -------------------------------------------------------------------------------- 1 | class FlavorValues { 2 | const FlavorValues({ 3 | required this.baseUrl, 4 | required this.clientId, 5 | required this.clientSecret, 6 | required this.redirectUri, 7 | required this.customUriScheme, 8 | }); 9 | 10 | final String baseUrl; 11 | final String clientId; 12 | final String clientSecret; 13 | final String redirectUri; 14 | final String customUriScheme; 15 | } 16 | -------------------------------------------------------------------------------- /lib/common/model/authentication.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'authentication.g.dart'; 4 | 5 | @JsonSerializable() 6 | class Authentication { 7 | Authentication({ 8 | required this.authorizationToken, 9 | }); 10 | 11 | factory Authentication.fromJson(Map json) => _$AuthenticationFromJson(json); 12 | 13 | Map toJson() => _$AuthenticationToJson(this); 14 | 15 | @JsonKey(name: 'token') 16 | final String authorizationToken; 17 | } 18 | -------------------------------------------------------------------------------- /lib/common/model/authentication.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'authentication.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Authentication _$AuthenticationFromJson(Map json) => 10 | Authentication( 11 | authorizationToken: json['token'] as String, 12 | ); 13 | 14 | Map _$AuthenticationToJson(Authentication instance) => 15 | { 16 | 'token': instance.authorizationToken, 17 | }; 18 | -------------------------------------------------------------------------------- /lib/common/model/new_tweet.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'new_tweet.g.dart'; 4 | 5 | @JsonSerializable() 6 | class NewTweet { 7 | NewTweet(this.tweetText); 8 | 9 | factory NewTweet.fromJson(Map json) => _$NewTweetFromJson(json); 10 | 11 | final String tweetText; 12 | } 13 | -------------------------------------------------------------------------------- /lib/common/model/new_tweet.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'new_tweet.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NewTweet _$NewTweetFromJson(Map json) => NewTweet( 10 | json['tweetText'] as String, 11 | ); 12 | 13 | Map _$NewTweetToJson(NewTweet instance) => { 14 | 'tweetText': instance.tweetText, 15 | }; 16 | -------------------------------------------------------------------------------- /lib/common/model/tweet.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'tweet.g.dart'; 4 | 5 | @JsonSerializable(explicitToJson: true, fieldRename: FieldRename.snake, createToJson: false) 6 | class Tweet { 7 | Tweet( 8 | this.id, 9 | this.text, 10 | this.profileImageUrl, 11 | this.name, 12 | this.username, 13 | this.likeCount, 14 | this.retweetCount, 15 | this.replyCount, 16 | this.createdAt, 17 | ); 18 | 19 | final String id; 20 | final String text; 21 | final String? profileImageUrl; 22 | final String? name; 23 | final String? username; 24 | final int likeCount; 25 | final int retweetCount; 26 | final int replyCount; 27 | final String? createdAt; 28 | 29 | factory Tweet.fromJson(Map json) => _$TweetFromJson(json); 30 | } 31 | -------------------------------------------------------------------------------- /lib/common/model/tweet.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'tweet.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Tweet _$TweetFromJson(Map json) => Tweet( 10 | json['id'] as String, 11 | json['text'] as String, 12 | json['profile_image_url'] as String?, 13 | json['name'] as String?, 14 | json['username'] as String?, 15 | json['like_count'] as int, 16 | json['retweet_count'] as int, 17 | json['reply_count'] as int, 18 | json['created_at'] as String?, 19 | ); 20 | -------------------------------------------------------------------------------- /lib/common/model/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'user.g.dart'; 4 | 5 | @JsonSerializable() 6 | class User { 7 | User({ 8 | required this.id, 9 | required this.name, 10 | required this.username, 11 | this.imageUrl, 12 | this.description, 13 | this.followers, 14 | this.following, 15 | }); 16 | 17 | final String id; 18 | final String name; 19 | final String username; 20 | final String? imageUrl; 21 | final String? description; 22 | final int? followers; 23 | final int? following; 24 | 25 | factory User.fromJson(Map json) => _$UserFromJson(json); 26 | } 27 | -------------------------------------------------------------------------------- /lib/common/model/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['id'] as String, 11 | name: json['name'] as String, 12 | username: json['username'] as String, 13 | imageUrl: json['imageUrl'] as String?, 14 | description: json['description'] as String?, 15 | followers: json['followers'] as int?, 16 | following: json['following'] as int?, 17 | ); 18 | 19 | Map _$UserToJson(User instance) => { 20 | 'id': instance.id, 21 | 'name': instance.name, 22 | 'username': instance.username, 23 | 'imageUrl': instance.imageUrl, 24 | 'description': instance.description, 25 | 'followers': instance.followers, 26 | 'following': instance.following, 27 | }; 28 | -------------------------------------------------------------------------------- /lib/device/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/lib/device/.gitkeep -------------------------------------------------------------------------------- /lib/device/impl/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/lib/device/impl/.gitkeep -------------------------------------------------------------------------------- /lib/domain/data/user_data_holder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/user.dart'; 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | part 'user_data_holder.g.dart'; 5 | 6 | @Riverpod(keepAlive: true) 7 | UserDataHolder userDataHolder(UserDataHolderRef ref) { 8 | return UserDataHolder(); 9 | } 10 | 11 | class UserDataHolder { 12 | User? user; 13 | } 14 | -------------------------------------------------------------------------------- /lib/domain/data/user_data_holder.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user_data_holder.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$userDataHolderHash() => r'cb82df9a67e30bd0956f82504b11b32cedaed29d'; 10 | 11 | /// See also [userDataHolder]. 12 | @ProviderFor(userDataHolder) 13 | final userDataHolderProvider = Provider.internal( 14 | userDataHolder, 15 | name: r'userDataHolderProvider', 16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 17 | ? null 18 | : _$userDataHolderHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef UserDataHolderRef = ProviderRef; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 26 | -------------------------------------------------------------------------------- /lib/domain/interactor/dashboard/fetch_feed_interactor.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/tweet.dart'; 2 | import 'package:flutter_dasher/domain/interactor/dashboard/fetch_feed_interactor_impl.dart'; 3 | import 'package:flutter_dasher/domain/repository/feed_repository.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | 6 | part 'fetch_feed_interactor.g.dart'; 7 | 8 | @riverpod 9 | FetchFeedInteractor fetchFeedInteractor(FetchFeedInteractorRef ref) { 10 | return FetchFeedInteractorImpl(ref.read(feedRepositoryProvider)); 11 | } 12 | 13 | abstract class FetchFeedInteractor { 14 | Future> fetchFeedTimeline(); 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/interactor/dashboard/fetch_feed_interactor.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'fetch_feed_interactor.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$fetchFeedInteractorHash() => 10 | r'984b41a9e9436a73c8052161e868b7effcd68f42'; 11 | 12 | /// See also [fetchFeedInteractor]. 13 | @ProviderFor(fetchFeedInteractor) 14 | final fetchFeedInteractorProvider = 15 | AutoDisposeProvider.internal( 16 | fetchFeedInteractor, 17 | name: r'fetchFeedInteractorProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$fetchFeedInteractorHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef FetchFeedInteractorRef = AutoDisposeProviderRef; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 28 | -------------------------------------------------------------------------------- /lib/domain/interactor/dashboard/fetch_feed_interactor_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/tweet.dart'; 2 | import 'package:flutter_dasher/domain/interactor/dashboard/fetch_feed_interactor.dart'; 3 | import 'package:flutter_dasher/domain/repository/feed_repository.dart'; 4 | 5 | class FetchFeedInteractorImpl implements FetchFeedInteractor { 6 | FetchFeedInteractorImpl(this._feedRepository); 7 | 8 | final FeedRepository _feedRepository; 9 | 10 | @override 11 | Future> fetchFeedTimeline() { 12 | return _feedRepository.fetchFeedTimeline(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/interactor/login/login_interactor.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/domain/data/user_data_holder.dart'; 2 | import 'package:flutter_dasher/domain/interactor/login/login_interactor_impl.dart'; 3 | import 'package:flutter_dasher/domain/repository/login_repository.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | 6 | part 'login_interactor.g.dart'; 7 | 8 | @riverpod 9 | LoginInteractor loginInteractor(LoginInteractorRef ref) { 10 | return LoginInteractorImpl(ref.watch(loginRepositoryProvider), ref.watch(userDataHolderProvider)); 11 | } 12 | 13 | abstract class LoginInteractor { 14 | Future login(); 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/interactor/login/login_interactor.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'login_interactor.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$loginInteractorHash() => r'16d71bfd45806c4e00882cb15547008972ee236c'; 10 | 11 | /// See also [loginInteractor]. 12 | @ProviderFor(loginInteractor) 13 | final loginInteractorProvider = AutoDisposeProvider.internal( 14 | loginInteractor, 15 | name: r'loginInteractorProvider', 16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 17 | ? null 18 | : _$loginInteractorHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef LoginInteractorRef = AutoDisposeProviderRef; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 26 | -------------------------------------------------------------------------------- /lib/domain/interactor/login/login_interactor_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/domain/data/user_data_holder.dart'; 2 | import 'package:flutter_dasher/domain/interactor/login/login_interactor.dart'; 3 | import 'package:flutter_dasher/domain/repository/login_repository.dart'; 4 | 5 | class LoginInteractorImpl implements LoginInteractor { 6 | LoginInteractorImpl( 7 | this._loginRepository, 8 | this._userDataHolder, 9 | ); 10 | 11 | final LoginRepository _loginRepository; 12 | final UserDataHolder _userDataHolder; 13 | 14 | @override 15 | Future login() async { 16 | final user = await _loginRepository.login(); 17 | 18 | _userDataHolder.user = user; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/domain/interactor/new_tweet/new_tweet_interactor.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/new_tweet.dart'; 2 | import 'package:flutter_dasher/domain/interactor/new_tweet/new_tweet_interactor_impl.dart'; 3 | import 'package:flutter_dasher/domain/repository/new_tweet_repository.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | 6 | part 'new_tweet_interactor.g.dart'; 7 | 8 | @riverpod 9 | NewTweetInteractor newTweetInteractor(NewTweetInteractorRef ref) { 10 | return NewTweetInteractorImpl(ref.read(newTweetRepositoryProvider)); 11 | } 12 | 13 | abstract class NewTweetInteractor { 14 | Future postNewTweet(NewTweet newTweet); 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/interactor/new_tweet/new_tweet_interactor.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'new_tweet_interactor.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$newTweetInteractorHash() => 10 | r'6ee43806457f628722ec2e32f08021be8460be70'; 11 | 12 | /// See also [newTweetInteractor]. 13 | @ProviderFor(newTweetInteractor) 14 | final newTweetInteractorProvider = 15 | AutoDisposeProvider.internal( 16 | newTweetInteractor, 17 | name: r'newTweetInteractorProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$newTweetInteractorHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef NewTweetInteractorRef = AutoDisposeProviderRef; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 28 | -------------------------------------------------------------------------------- /lib/domain/interactor/new_tweet/new_tweet_interactor_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/new_tweet.dart'; 2 | import 'package:flutter_dasher/domain/interactor/new_tweet/new_tweet_interactor.dart'; 3 | import 'package:flutter_dasher/domain/repository/new_tweet_repository.dart'; 4 | 5 | class NewTweetInteractorImpl implements NewTweetInteractor { 6 | NewTweetInteractorImpl(this._newTweetRepository); 7 | 8 | final NewTweetRepository _newTweetRepository; 9 | 10 | @override 11 | Future postNewTweet(NewTweet newTweet) async { 12 | await _newTweetRepository.postNewTweet(newTweet); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/interactor/profile/profile_tweets_interactor.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/tweet.dart'; 2 | import 'package:flutter_dasher/domain/interactor/profile/profile_tweets_interactor_impl.dart'; 3 | import 'package:flutter_dasher/domain/repository/profile_repository.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | 6 | part 'profile_tweets_interactor.g.dart'; 7 | 8 | @riverpod 9 | ProfileTweetsInteractor profileTweetsInteractor(ProfileTweetsInteractorRef ref) { 10 | return ProfileTweetsInteractorImpl(ref.watch(profileRepositoryProvider)); 11 | } 12 | 13 | abstract class ProfileTweetsInteractor { 14 | Future> fetchProfileTweets(); 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/interactor/profile/profile_tweets_interactor.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'profile_tweets_interactor.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$profileTweetsInteractorHash() => 10 | r'eae073a1a508d5970e7fb3bc804cf901aefbea5d'; 11 | 12 | /// See also [profileTweetsInteractor]. 13 | @ProviderFor(profileTweetsInteractor) 14 | final profileTweetsInteractorProvider = 15 | AutoDisposeProvider.internal( 16 | profileTweetsInteractor, 17 | name: r'profileTweetsInteractorProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$profileTweetsInteractorHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef ProfileTweetsInteractorRef 26 | = AutoDisposeProviderRef; 27 | // ignore_for_file: type=lint 28 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 29 | -------------------------------------------------------------------------------- /lib/domain/interactor/profile/profile_tweets_interactor_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/tweet.dart'; 2 | import 'package:flutter_dasher/domain/interactor/profile/profile_tweets_interactor.dart'; 3 | import 'package:flutter_dasher/domain/repository/profile_repository.dart'; 4 | 5 | class ProfileTweetsInteractorImpl implements ProfileTweetsInteractor { 6 | ProfileTweetsInteractorImpl(this._profileRepository); 7 | 8 | final ProfileRepository _profileRepository; 9 | 10 | @override 11 | Future> fetchProfileTweets() { 12 | return _profileRepository.fetchProfileTweets(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/repository/feed_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/tweet.dart'; 2 | import 'package:flutter_dasher/source_dev/dev_feed_repository.dart'; 3 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 | 5 | part 'feed_repository.g.dart'; 6 | 7 | @Riverpod(keepAlive: true) 8 | FeedRepository feedRepository(FeedRepositoryRef ref) { 9 | return DevFeedRepository(); 10 | } 11 | 12 | abstract class FeedRepository { 13 | Future> fetchFeedTimeline(); 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/repository/feed_repository.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'feed_repository.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$feedRepositoryHash() => r'89113f59ef05f9da58f9828185b0c8ecddf263b6'; 10 | 11 | /// See also [feedRepository]. 12 | @ProviderFor(feedRepository) 13 | final feedRepositoryProvider = Provider.internal( 14 | feedRepository, 15 | name: r'feedRepositoryProvider', 16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 17 | ? null 18 | : _$feedRepositoryHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef FeedRepositoryRef = ProviderRef; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 26 | -------------------------------------------------------------------------------- /lib/domain/repository/login_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/user.dart'; 2 | import 'package:flutter_dasher/source_remote/impl/login_repository_impl.dart'; 3 | import 'package:flutter_dasher/source_remote/twitter/twitter_api_container.dart'; 4 | import 'package:flutter_dasher/source_remote/twitter/twitter_oauth_client.dart'; 5 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 6 | 7 | part 'login_repository.g.dart'; 8 | 9 | @Riverpod(keepAlive: true) 10 | LoginRepository loginRepository(LoginRepositoryRef ref) { 11 | return LoginRepositoryImpl(ref.watch(twitterOAuthClientProvider), ref.watch(twitterApiContainerProvider)); 12 | } 13 | 14 | abstract class LoginRepository { 15 | Future login(); 16 | } 17 | -------------------------------------------------------------------------------- /lib/domain/repository/login_repository.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'login_repository.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$loginRepositoryHash() => r'e31f79647d1a664b7a52343d5d0c97f33943fb60'; 10 | 11 | /// See also [loginRepository]. 12 | @ProviderFor(loginRepository) 13 | final loginRepositoryProvider = Provider.internal( 14 | loginRepository, 15 | name: r'loginRepositoryProvider', 16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 17 | ? null 18 | : _$loginRepositoryHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef LoginRepositoryRef = ProviderRef; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 26 | -------------------------------------------------------------------------------- /lib/domain/repository/new_tweet_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/new_tweet.dart'; 2 | import 'package:flutter_dasher/source_remote/impl/new_tweet_repository_impl.dart'; 3 | import 'package:flutter_dasher/source_remote/twitter/twitter_api_container.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | 6 | part 'new_tweet_repository.g.dart'; 7 | 8 | @Riverpod(keepAlive: true) 9 | NewTweetRepository newTweetRepository(NewTweetRepositoryRef ref) { 10 | return NewTweetRepositoryImpl(ref.watch(twitterApiContainerProvider)); 11 | } 12 | 13 | abstract class NewTweetRepository { 14 | Future postNewTweet(NewTweet newTweet); 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/repository/new_tweet_repository.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'new_tweet_repository.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$newTweetRepositoryHash() => 10 | r'ed53a6eb729b1e8f38a24bafe0429af4b0452d7f'; 11 | 12 | /// See also [newTweetRepository]. 13 | @ProviderFor(newTweetRepository) 14 | final newTweetRepositoryProvider = Provider.internal( 15 | newTweetRepository, 16 | name: r'newTweetRepositoryProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$newTweetRepositoryHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef NewTweetRepositoryRef = ProviderRef; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 27 | -------------------------------------------------------------------------------- /lib/domain/repository/profile_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/tweet.dart'; 2 | import 'package:flutter_dasher/source_dev/dev_profile_repository.dart'; 3 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 | 5 | part 'profile_repository.g.dart'; 6 | 7 | @Riverpod(keepAlive: true) 8 | ProfileRepository profileRepository(ProfileRepositoryRef ref) { 9 | return DevProfileRepository(); 10 | } 11 | 12 | abstract class ProfileRepository { 13 | Future> fetchProfileTweets(); 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/repository/profile_repository.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'profile_repository.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$profileRepositoryHash() => r'8a89692527444d8eae494446545aa3bc37e14c0c'; 10 | 11 | /// See also [profileRepository]. 12 | @ProviderFor(profileRepository) 13 | final profileRepositoryProvider = Provider.internal( 14 | profileRepository, 15 | name: r'profileRepositoryProvider', 16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 17 | ? null 18 | : _$profileRepositoryHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef ProfileRepositoryRef = ProviderRef; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 26 | -------------------------------------------------------------------------------- /lib/main_production.dart: -------------------------------------------------------------------------------- 1 | import 'app/run_dasher_app.dart'; 2 | import 'common/flavor/flavor.dart'; 3 | import 'common/flavor/flavor_config.dart'; 4 | import 'common/flavor/flavor_values.dart'; 5 | 6 | void main() { 7 | FlavorConfig( 8 | flavor: Flavor.production, 9 | name: 'Production', 10 | values: const FlavorValues( 11 | baseUrl: 'production URL', 12 | clientId: 'Uk1pRElPZnd0TlBQSDFIY2VjUUM6MTpjaQ', 13 | clientSecret: 'DCxJ_zS2VNXIwmyfSBNUJBzeprYLgIiNYCIkixWdpt1W7s3qd2', 14 | redirectUri: 'org.example.android.oauth://callback/', 15 | customUriScheme: 'org.example.android.oauth', 16 | ), 17 | ); 18 | 19 | runDasherApp(); 20 | } 21 | -------------------------------------------------------------------------------- /lib/main_staging.dart: -------------------------------------------------------------------------------- 1 | import 'app/run_dasher_app.dart'; 2 | import 'common/flavor/flavor.dart'; 3 | import 'common/flavor/flavor_config.dart'; 4 | import 'common/flavor/flavor_values.dart'; 5 | 6 | void main() { 7 | FlavorConfig( 8 | flavor: Flavor.staging, 9 | name: 'Staging', 10 | values: const FlavorValues( 11 | baseUrl: 'staging URL', 12 | clientId: 'Uk1pRElPZnd0TlBQSDFIY2VjUUM6MTpjaQ', 13 | clientSecret: 'DCxJ_zS2VNXIwmyfSBNUJBzeprYLgIiNYCIkixWdpt1W7s3qd2', 14 | redirectUri: 'org.example.android.oauth://callback/', 15 | customUriScheme: 'org.example.android.oauth', 16 | ), 17 | ); 18 | 19 | runDasherApp(); 20 | } 21 | -------------------------------------------------------------------------------- /lib/source_dev/dev_feed_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/tweet.dart'; 2 | import 'package:flutter_dasher/domain/repository/feed_repository.dart'; 3 | 4 | class DevFeedRepository implements FeedRepository { 5 | @override 6 | Future> fetchFeedTimeline() async { 7 | await Future.delayed(const Duration(milliseconds: 500)); 8 | 9 | return [ 10 | _createTweet(text: 'Tweet 0'), 11 | _createTweet(text: 'Tweet 1'), 12 | _createTweet(text: 'Tweet 2'), 13 | _createTweet(text: 'Tweet 3'), 14 | _createTweet(text: 'Tweet 4'), 15 | _createTweet(text: 'Tweet 5'), 16 | ]; 17 | } 18 | 19 | Tweet _createTweet({ 20 | required String text, 21 | }) { 22 | return Tweet( 23 | '1', 24 | text, 25 | 'https://pbs.twimg.com/profile_images/562305884731105280/TjYLM95x_400x400.png', 26 | 'John', 27 | 'john_x', 28 | 10, 29 | 11, 30 | 12, 31 | '2023.1.1 12:00', 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/source_dev/dev_profile_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/tweet.dart'; 2 | import 'package:flutter_dasher/domain/repository/profile_repository.dart'; 3 | 4 | class DevProfileRepository implements ProfileRepository { 5 | @override 6 | Future> fetchProfileTweets() async { 7 | await Future.delayed(const Duration(milliseconds: 500)); 8 | 9 | return [ 10 | _createTweet(text: 'Tweet 0'), 11 | _createTweet(text: 'Tweet 1'), 12 | _createTweet(text: 'Tweet 2'), 13 | _createTweet(text: 'Tweet 3'), 14 | _createTweet(text: 'Tweet 4'), 15 | _createTweet(text: 'Tweet 5'), 16 | ]; 17 | } 18 | 19 | Tweet _createTweet({ 20 | required String text, 21 | }) { 22 | return Tweet( 23 | '1', 24 | text, 25 | 'https://pbs.twimg.com/profile_images/562305884731105280/TjYLM95x_400x400.png', 26 | 'Jim', 27 | 'jim_x', 28 | 10, 29 | 11, 30 | 12, 31 | '2023.1.1 12:00', 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/source_local/impl/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/lib/source_local/impl/.gitkeep -------------------------------------------------------------------------------- /lib/source_remote/dio/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/flutter-dasher/3cea9bae0b4d6c95daed644bfdfcb1362bcc370d/lib/source_remote/dio/.gitkeep -------------------------------------------------------------------------------- /lib/source_remote/impl/feed_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/tweet.dart'; 2 | import 'package:flutter_dasher/domain/data/user_data_holder.dart'; 3 | import 'package:flutter_dasher/domain/repository/feed_repository.dart'; 4 | import 'package:flutter_dasher/source_remote/twitter/twitter_api_container.dart'; 5 | import 'package:intl/intl.dart'; 6 | import 'package:twitter_api_v2/twitter_api_v2.dart'; 7 | 8 | class FeedRepositoryImpl implements FeedRepository { 9 | FeedRepositoryImpl(this.twitterApiContainer, this.userDataHolder); 10 | 11 | final TwitterApiContainer twitterApiContainer; 12 | final UserDataHolder userDataHolder; 13 | 14 | @override 15 | Future> fetchFeedTimeline() async { 16 | final response = await twitterApiContainer.getTwitterApi().tweetsService.lookupHomeTimeline( 17 | userId: userDataHolder.user!.id, 18 | tweetFields: [ 19 | TweetField.publicMetrics, 20 | TweetField.createdAt, 21 | ], 22 | userFields: [ 23 | UserField.createdAt, 24 | UserField.profileImageUrl, 25 | ], 26 | expansions: [ 27 | TweetExpansion.authorId, 28 | ], 29 | ); 30 | 31 | return _getTweetsListWithAuthors(response); 32 | } 33 | 34 | List _getTweetsListWithAuthors(TwitterResponse, TweetMeta> response) { 35 | return response.data.map((tweet) { 36 | final UserData user = _getUserForTweet(response.includes?.users, tweet); 37 | 38 | // Extend tweet data with user data related to that tweet 39 | return Tweet( 40 | tweet.id, 41 | tweet.text, 42 | user.profileImageUrl?.replaceAll('normal', '400x400'), 43 | user.name, 44 | user.username, 45 | tweet.publicMetrics!.likeCount, 46 | tweet.publicMetrics!.retweetCount, 47 | tweet.publicMetrics!.replyCount, 48 | DateFormat.MMMd().format(tweet.createdAt!), 49 | ); 50 | }).toList(); 51 | } 52 | 53 | // From authorId inside the tweet find a matching user from the list of all unique users who posted those tweets 54 | UserData _getUserForTweet(List? users, TweetData tweet) => users!.singleWhere((user) => user.id == tweet.authorId); 55 | } 56 | -------------------------------------------------------------------------------- /lib/source_remote/impl/login_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/user.dart'; 2 | import 'package:flutter_dasher/domain/repository/login_repository.dart'; 3 | import 'package:flutter_dasher/source_remote/twitter/twitter_api_container.dart'; 4 | import 'package:twitter_api_v2/twitter_api_v2.dart' as v2; 5 | import 'package:twitter_oauth2_pkce/twitter_oauth2_pkce.dart'; 6 | 7 | class LoginRepositoryImpl implements LoginRepository { 8 | LoginRepositoryImpl(this.twitterOAuth2Client, this.twitterApiContainer); 9 | 10 | final TwitterOAuth2Client twitterOAuth2Client; 11 | final TwitterApiContainer twitterApiContainer; 12 | 13 | @override 14 | Future login() async { 15 | final response = await twitterOAuth2Client.executeAuthCodeFlowWithPKCE( 16 | scopes: Scope.values, 17 | ); 18 | 19 | final twitter = v2.TwitterApi(bearerToken: response.accessToken); 20 | 21 | twitterApiContainer.setTwitterApi(twitter); 22 | 23 | final userResponse = await twitter.usersService.lookupMe( 24 | userFields: [ 25 | v2.UserField.profileImageUrl, 26 | v2.UserField.description, 27 | v2.UserField.publicMetrics, 28 | ], 29 | ); 30 | 31 | return User( 32 | id: userResponse.data.id, 33 | name: userResponse.data.name, 34 | username: userResponse.data.username, 35 | imageUrl: userResponse.data.profileImageUrl?.replaceAll('normal', '400x400'), 36 | description: userResponse.data.description, 37 | followers: userResponse.data.publicMetrics?.followersCount, 38 | following: userResponse.data.publicMetrics?.followingCount, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/source_remote/impl/new_tweet_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/new_tweet.dart'; 2 | import 'package:flutter_dasher/domain/repository/new_tweet_repository.dart'; 3 | import 'package:flutter_dasher/source_remote/twitter/twitter_api_container.dart'; 4 | 5 | class NewTweetRepositoryImpl implements NewTweetRepository { 6 | NewTweetRepositoryImpl(this.twitterApiContainer); 7 | 8 | final TwitterApiContainer twitterApiContainer; 9 | 10 | @override 11 | Future postNewTweet(NewTweet newTweet) async { 12 | twitterApiContainer.getTwitterApi().tweetsService.createTweet(text: newTweet.tweetText); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/source_remote/impl/profile_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/tweet.dart'; 2 | import 'package:flutter_dasher/domain/data/user_data_holder.dart'; 3 | import 'package:flutter_dasher/domain/repository/profile_repository.dart'; 4 | import 'package:flutter_dasher/source_remote/twitter/twitter_api_container.dart'; 5 | import 'package:intl/intl.dart'; 6 | import 'package:twitter_api_v2/twitter_api_v2.dart'; 7 | 8 | class ProfileRepositoryImpl implements ProfileRepository { 9 | ProfileRepositoryImpl(this.twitterApiContainer, this.userDataHolder); 10 | 11 | final TwitterApiContainer twitterApiContainer; 12 | final UserDataHolder userDataHolder; 13 | 14 | @override 15 | Future> fetchProfileTweets() async { 16 | final response = await twitterApiContainer.getTwitterApi().tweetsService.lookupTweets( 17 | userId: userDataHolder.user!.id, 18 | tweetFields: [ 19 | TweetField.publicMetrics, 20 | TweetField.createdAt, 21 | ], 22 | userFields: [ 23 | UserField.createdAt, 24 | UserField.profileImageUrl, 25 | ], 26 | expansions: [ 27 | TweetExpansion.authorId, 28 | ], 29 | ); 30 | 31 | return response.data.map((tweet) { 32 | final UserData? user = response.includes?.users?.singleWhere((user) => user.id == tweet.authorId); 33 | 34 | return Tweet( 35 | tweet.id, 36 | tweet.text, 37 | user!.profileImageUrl?.replaceAll('normal', '400x400'), 38 | user.name, 39 | user.username, 40 | tweet.publicMetrics!.likeCount, 41 | tweet.publicMetrics!.retweetCount, 42 | tweet.publicMetrics!.replyCount, 43 | DateFormat.MMMd().format(tweet.createdAt!), 44 | ); 45 | }).toList(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/source_remote/twitter/twitter_api_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 2 | import 'package:twitter_api_v2/twitter_api_v2.dart'; 3 | 4 | part 'twitter_api_container.g.dart'; 5 | 6 | @Riverpod(keepAlive: true) 7 | TwitterApiContainer twitterApiContainer(TwitterApiContainerRef _) { 8 | return TwitterApiContainer(); 9 | } 10 | 11 | class TwitterApiContainer { 12 | TwitterApi? _twitterApi; 13 | 14 | void setTwitterApi(TwitterApi twitterApi) { 15 | _twitterApi = twitterApi; 16 | } 17 | 18 | TwitterApi getTwitterApi() { 19 | if (_twitterApi == null) throw Exception('Twitter was null you need to login first to obtain instance.'); 20 | return _twitterApi!; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/source_remote/twitter/twitter_api_container.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'twitter_api_container.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$twitterApiContainerHash() => 10 | r'907ec865bdc8c5a2ca83e71cdf8cba7fc83952a7'; 11 | 12 | /// See also [twitterApiContainer]. 13 | @ProviderFor(twitterApiContainer) 14 | final twitterApiContainerProvider = Provider.internal( 15 | twitterApiContainer, 16 | name: r'twitterApiContainerProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$twitterApiContainerHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef TwitterApiContainerRef = ProviderRef; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 27 | -------------------------------------------------------------------------------- /lib/source_remote/twitter/twitter_oauth_client.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/flavor/flavor_config.dart'; 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | import 'package:twitter_oauth2_pkce/twitter_oauth2_pkce.dart'; 4 | 5 | part 'twitter_oauth_client.g.dart'; 6 | 7 | @Riverpod(keepAlive: true) 8 | TwitterOAuth2Client twitterOAuthClient(TwitterOAuthClientRef ref) { 9 | return TwitterOAuth2Client( 10 | clientId: FlavorConfig.instance.values.clientId, 11 | clientSecret: FlavorConfig.instance.values.clientSecret, 12 | redirectUri: FlavorConfig.instance.values.redirectUri, 13 | customUriScheme: FlavorConfig.instance.values.customUriScheme, 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /lib/source_remote/twitter/twitter_oauth_client.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'twitter_oauth_client.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$twitterOAuthClientHash() => 10 | r'ba660264130d72743a3f7dcbdb8922e0d2595acc'; 11 | 12 | /// See also [twitterOAuthClient]. 13 | @ProviderFor(twitterOAuthClient) 14 | final twitterOAuthClientProvider = Provider.internal( 15 | twitterOAuthClient, 16 | name: r'twitterOAuthClientProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$twitterOAuthClientHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef TwitterOAuthClientRef = ProviderRef; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 27 | -------------------------------------------------------------------------------- /lib/ui/common/bits/request_provider/request_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter_dasher/ui/common/bits/request_provider/request_state.dart'; 5 | import 'package:loggy/loggy.dart'; 6 | 7 | abstract class RequestProvider with ChangeNotifier, NetworkLoggy { 8 | RequestProvider({RequestState initial = const RequestState.initial()}) : _requestState = initial; 9 | 10 | RequestState _requestState; 11 | 12 | RequestState get state => _requestState; 13 | 14 | bool isLoading() { 15 | return state.maybeMap(loading: (_) => true, orElse: () => false); 16 | } 17 | 18 | bool _mounted = true; 19 | 20 | set _state(RequestState newState) { 21 | if (newState == _requestState) { 22 | return; 23 | } 24 | 25 | _requestState = newState; 26 | 27 | if (_mounted) { 28 | notifyListeners(); 29 | } 30 | } 31 | 32 | Future executeRequest({ 33 | required ValueGetter> requestBuilder, 34 | Exception? Function(Exception)? errorHandler, 35 | }) async { 36 | try { 37 | _state = _requestState.maybeMap( 38 | success: (r) => RequestState.loading(resultMaybe: r.value), 39 | orElse: () => RequestState.loading(), 40 | ); 41 | 42 | final value = await requestBuilder(); 43 | _state = RequestState.success(value); 44 | } catch (error, st) { 45 | loggy.error(error); 46 | print(st); 47 | final exception = (error is Exception) ? error : Exception(); 48 | final stateException = errorHandler != null ? errorHandler(exception) : exception; 49 | if (stateException != null) { 50 | _state = RequestState.failure(stateException); 51 | } else { 52 | _state = const RequestState.initial(); 53 | } 54 | } 55 | } 56 | 57 | @override 58 | void dispose() { 59 | _mounted = false; 60 | super.dispose(); 61 | } 62 | 63 | void reset() => _state = const RequestState.initial(); 64 | } 65 | -------------------------------------------------------------------------------- /lib/ui/common/bits/request_provider/request_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'request_state.freezed.dart'; 4 | 5 | @freezed 6 | class RequestState with _$RequestState { 7 | const factory RequestState.initial() = RequestStateInitial; 8 | 9 | const factory RequestState.loading({Value? resultMaybe}) = RequestStateLoading; 10 | 11 | const factory RequestState.success(Value result) = RequestStateSuccess; 12 | 13 | const factory RequestState.failure(Error error) = RequestStateFailure; 14 | } 15 | 16 | extension IsLoading on RequestState { 17 | bool get isLoading => maybeMap(orElse: () => false, loading: (value) => true); 18 | 19 | Value? get value => maybeWhen(orElse: () => null, success: (result) => result); 20 | 21 | Exception? get error => maybeWhen(orElse: () => null, failure: (error) => error); 22 | } 23 | -------------------------------------------------------------------------------- /lib/ui/common/buttons/primary_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_dasher/ui/common/look/widget/look.dart'; 3 | 4 | class PrimaryButton extends StatelessWidget { 5 | const PrimaryButton({ 6 | Key? key, 7 | this.onPressed, 8 | this.elevation = 0, 9 | required this.child, 10 | }) : super(key: key); 11 | 12 | final double elevation; 13 | final VoidCallback? onPressed; 14 | final Widget child; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return ElevatedButton( 19 | style: ButtonStyle( 20 | elevation: MaterialStateProperty.all(elevation), 21 | foregroundColor: MaterialStateProperty.all(Look.of(context).color.onPrimary), 22 | backgroundColor: MaterialStateProperty.all(Look.of(context).color.secondary), 23 | shape: MaterialStateProperty.all(RoundedRectangleBorder( 24 | borderRadius: BorderRadius.circular(16.0), 25 | )), 26 | padding: MaterialStateProperty.all( 27 | const EdgeInsets.symmetric(vertical: 10), 28 | ), 29 | textStyle: MaterialStateProperty.all(Look.of(context).typography.button)), 30 | onPressed: onPressed, 31 | child: child, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/ui/common/buttons/primary_text_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_dasher/ui/common/look/widget/look.dart'; 3 | 4 | class PrimaryTextButton extends StatelessWidget { 5 | const PrimaryTextButton({ 6 | Key? key, 7 | this.onPressed, 8 | required this.child, 9 | }) : super(key: key); 10 | 11 | final VoidCallback? onPressed; 12 | final Widget child; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return TextButton( 17 | style: ButtonStyle( 18 | textStyle: MaterialStateProperty.all(Look.of(context).typography.lightButton.copyWith(color: Look.of(context).color.primary)), 19 | padding: MaterialStateProperty.all(const EdgeInsets.symmetric(vertical: 8)), 20 | ), 21 | onPressed: onPressed, 22 | child: child, 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/ui/common/buttons/primary_variant_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_dasher/ui/common/look/widget/look.dart'; 3 | 4 | class PrimaryVariantButton extends StatelessWidget { 5 | const PrimaryVariantButton({ 6 | Key? key, 7 | this.onPressed, 8 | this.elevation = 0, 9 | required this.child, 10 | }) : super(key: key); 11 | 12 | final double elevation; 13 | final VoidCallback? onPressed; 14 | final Widget child; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return ElevatedButton( 19 | style: ButtonStyle( 20 | elevation: MaterialStateProperty.all(elevation), 21 | foregroundColor: MaterialStateProperty.all(Look.of(context).color.onPrimary), 22 | backgroundColor: MaterialStateProperty.all(Look.of(context).color.secondary), 23 | shape: MaterialStateProperty.all(RoundedRectangleBorder( 24 | borderRadius: BorderRadius.circular(50.0), 25 | )), 26 | padding: MaterialStateProperty.all( 27 | const EdgeInsets.symmetric(vertical: 8, horizontal: 12), 28 | ), 29 | textStyle: MaterialStateProperty.all(Look.of(context).typography.lightButton)), 30 | onPressed: onPressed, 31 | child: child, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/ui/common/dasher_bottom_navigation_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_dasher/gen/assets.gen.dart'; 3 | import 'package:flutter_dasher/ui/common/look/widget/look.dart'; 4 | import 'package:flutter_svg/flutter_svg.dart'; 5 | 6 | class DasherBottomNavigationBar extends StatelessWidget { 7 | const DasherBottomNavigationBar({ 8 | Key? key, 9 | }) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Container( 14 | decoration: BoxDecoration( 15 | color: Colors.white, 16 | border: Border( 17 | top: BorderSide( 18 | color: Look.of(context).color.border, 19 | width: 0.5, 20 | ), 21 | ), 22 | ), 23 | child: BottomNavigationBar( 24 | backgroundColor: Look.of(context).color.background, 25 | showSelectedLabels: false, 26 | showUnselectedLabels: false, 27 | type: BottomNavigationBarType.fixed, 28 | items: [ 29 | BottomNavigationBarItem( 30 | icon: SvgPicture.asset(Assets.svg.navigationHome.path), 31 | label: 'Home', 32 | ), 33 | BottomNavigationBarItem( 34 | icon: SvgPicture.asset(Assets.svg.navigationSearch.path), 35 | label: 'Search', 36 | ), 37 | BottomNavigationBarItem( 38 | icon: SvgPicture.asset(Assets.svg.navigationBell.path), 39 | label: 'Notifications', 40 | ), 41 | BottomNavigationBarItem( 42 | icon: SvgPicture.asset(Assets.svg.navigationMail.path), 43 | label: 'Inbox', 44 | ), 45 | ], 46 | currentIndex: 0, 47 | selectedItemColor: Colors.red, 48 | onTap: null, 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/ui/common/dasher_new_tweet_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_dasher/gen/assets.gen.dart'; 3 | import 'package:flutter_dasher/ui/common/look/widget/look.dart'; 4 | import 'package:flutter_dasher/ui/new_tweet/new_tweet_screen.dart'; 5 | import 'package:flutter_svg/flutter_svg.dart'; 6 | 7 | class DasherNewTweetButton extends StatelessWidget { 8 | const DasherNewTweetButton({ 9 | Key? key, 10 | }) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return FloatingActionButton( 15 | onPressed: () => Navigator.of(context).push(NewTweetScreen.route()), 16 | backgroundColor: Look.of(context).color.secondary, 17 | child: SvgPicture.asset(Assets.svg.buttonNewTweet.path), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/ui/common/dasher_tweets_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_dasher/common/model/tweet.dart'; 3 | import 'package:flutter_dasher/ui/common/dasher_tweet.dart'; 4 | import 'package:flutter_dasher/ui/dashboard/presenter/feed_request_presenter.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | 7 | class DasherTweetsList extends ConsumerWidget { 8 | const DasherTweetsList({ 9 | Key? key, 10 | }) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | final _presenter = ref.watch(feedRequestPresenter); 15 | 16 | return Center( 17 | child: _presenter.state.maybeWhen( 18 | success: (feed) => _TweetsList( 19 | feed: feed, 20 | ), 21 | initial: () => const CircularProgressIndicator(), 22 | loading: (feed) { 23 | if (feed == null) { 24 | return const CircularProgressIndicator(); 25 | } else { 26 | return _TweetsList( 27 | feed: feed, 28 | ); 29 | } 30 | }, 31 | failure: (e) => Text('Error occurred $e'), 32 | orElse: () => const CircularProgressIndicator(), 33 | ), 34 | ); 35 | } 36 | } 37 | 38 | class _TweetsList extends ConsumerWidget { 39 | const _TweetsList({ 40 | Key? key, 41 | required this.feed, 42 | }) : super(key: key); 43 | 44 | final List feed; 45 | 46 | @override 47 | Widget build(BuildContext context, WidgetRef ref) { 48 | return RefreshIndicator( 49 | onRefresh: ref.read(feedRequestPresenter).fetchTweetsTimeline, 50 | child: ListView.builder( 51 | itemCount: feed.length, 52 | itemBuilder: (context, index) { 53 | return DasherTweet( 54 | avatarURL: feed[index].profileImageUrl, 55 | name: feed[index].name, 56 | usernameTag: feed[index].username, 57 | createdAt: feed[index].createdAt, 58 | tweetText: feed[index].text, 59 | commentsCount: feed[index].replyCount.toString(), 60 | retweetsCount: feed[index].retweetCount.toString(), 61 | likesCount: feed[index].likeCount.toString(), 62 | ); 63 | }, 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/ui/common/generic/generic_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../look/widget/look.dart'; 4 | 5 | /// Shows generic error widget, with possibility to add retry button below it 6 | /// if [onRetry] is not null retry will be active 7 | /// 8 | /// [message] should be readable error message that will be shown to the user 9 | class GenericError extends StatelessWidget { 10 | const GenericError(this.message, {this.onRetry, Key? key}) : super(key: key); 11 | 12 | factory GenericError.exception(Exception exception, BuildContext context, {VoidCallback? onRetry, Key? key}) { 13 | return GenericError('Unknown error', onRetry: onRetry, key: key); 14 | } 15 | 16 | final String message; 17 | final VoidCallback? onRetry; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Padding( 22 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8.0), 23 | child: Center( 24 | child: Column( 25 | mainAxisSize: MainAxisSize.min, 26 | mainAxisAlignment: MainAxisAlignment.center, 27 | children: [ 28 | Column( 29 | mainAxisSize: MainAxisSize.min, 30 | mainAxisAlignment: MainAxisAlignment.center, 31 | children: [ 32 | CircleAvatar( 33 | backgroundColor: Look.of(context).color.error.withOpacity(0.1), 34 | child: Icon( 35 | Icons.error_outline, 36 | color: Look.of(context).color.error, 37 | ), 38 | ), 39 | const SizedBox(width: 12), 40 | Text(message, textAlign: TextAlign.center, style: Look.of(context).typography.body), 41 | ], 42 | ), 43 | const SizedBox(height: 16), 44 | 45 | /// Show retry button below the widget if [onRetry] has been provided 46 | if (onRetry != null) _RetryButton(onRetry!), 47 | ], 48 | ), 49 | ), 50 | ); 51 | } 52 | } 53 | 54 | class _RetryButton extends StatelessWidget { 55 | const _RetryButton(this.onRetry, {Key? key}) : super(key: key); 56 | 57 | final VoidCallback onRetry; 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | return TextButton( 62 | onPressed: onRetry, 63 | child: const Text('Retry'), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/ui/common/look/look_data/look_data.dart: -------------------------------------------------------------------------------- 1 | import 'specific_look_data/color_look_data.dart'; 2 | import 'specific_look_data/motion_look_data.dart'; 3 | import 'specific_look_data/shape_look_data.dart'; 4 | import 'specific_look_data/typography_look_data.dart'; 5 | 6 | class LookData { 7 | const LookData({ 8 | required this.color, 9 | required this.motion, 10 | required this.shape, 11 | required this.typography, 12 | }); 13 | 14 | LookData.getDefault() 15 | : color = const ColorLookData.getDefaultWithUserSpecificColor(), 16 | motion = const MotionLookData.getDefault(), 17 | shape = ShapeLookData.getDefault(), 18 | typography = const TypographyLookData.getDefault(); 19 | 20 | final ColorLookData color; 21 | final MotionLookData motion; 22 | final ShapeLookData shape; 23 | final TypographyLookData typography; 24 | } 25 | -------------------------------------------------------------------------------- /lib/ui/common/look/look_data/specific_look_data/color_look_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | @immutable 4 | class ColorLookData { 5 | const ColorLookData({ 6 | required this.brightness, 7 | required this.primary, 8 | required this.primaryContainer, 9 | required this.primaryPressed, 10 | required this.primaryDisabled, 11 | required this.secondary, 12 | required this.secondaryContainer, 13 | required this.secondaryPressed, 14 | required this.secondaryDisabled, 15 | required this.tertiary, 16 | required this.tertiaryDisabled, 17 | required this.onPrimary, 18 | required this.onSecondary, 19 | required this.error, 20 | required this.neutral, 21 | required this.onError, 22 | required this.background, 23 | required this.onBackground, 24 | required this.surface, 25 | required this.onSurface, 26 | required this.white10p, 27 | required this.black10p, 28 | required this.overlay, 29 | required this.green, 30 | required this.gray, 31 | required this.symbolGray, 32 | required this.border, 33 | required this.header, 34 | required this.onHeader, 35 | required this.black, 36 | }); 37 | 38 | const ColorLookData.getDefaultWithUserSpecificColor([Color? primaryContainer]) // primaryContainer is assigned as user specific color 39 | : brightness = Brightness.light, 40 | primary = const Color(0xff1DA1F2), 41 | primaryContainer = primaryContainer ?? const Color(0xff005670), 42 | primaryPressed = const Color(0xff043E50), 43 | primaryDisabled = const Color(0xffc4d6dc), 44 | secondary = const Color(0xff4C9EEB), 45 | secondaryContainer = const Color(0xffAA198D), 46 | secondaryPressed = const Color(0xff7c0E66), 47 | secondaryDisabled = const Color(0xffe3cbde), 48 | tertiary = const Color(0xffe5eef1), 49 | tertiaryDisabled = const Color(0xffebebeb), 50 | neutral = const Color(0xff333333), 51 | background = Colors.white, 52 | surface = Colors.white, 53 | error = const Color(0xffFB3449), 54 | onError = Colors.white, 55 | onBackground = const Color(0xff141619), 56 | onSurface = const Color(0xff666666), 57 | onPrimary = Colors.white, 58 | onSecondary = Colors.white, 59 | onHeader = const Color(0xffDEDEDE), 60 | black10p = const Color(0x1A000000), 61 | black = const Color(0xFF000000), 62 | white10p = const Color(0x1AFFFFFF), 63 | overlay = const Color(0x8000374F), 64 | green = const Color(0xff00A03B), 65 | gray = const Color(0xffE7ECF0), 66 | symbolGray = const Color(0xff687684), 67 | border = const Color(0xffCED5DC), 68 | header = const Color(0xff1F1F1F); 69 | 70 | // Material color scheme 71 | final Brightness brightness; 72 | final Color primary; 73 | final Color primaryContainer; 74 | final Color secondary; 75 | final Color secondaryContainer; 76 | final Color onPrimary; 77 | final Color onSecondary; 78 | final Color error; 79 | final Color onError; 80 | final Color background; 81 | final Color onBackground; 82 | final Color surface; 83 | final Color onSurface; 84 | 85 | // Other colors 86 | final Color white10p; 87 | final Color black10p; 88 | final Color black; 89 | final Color overlay; 90 | final Color green; 91 | final Color gray; 92 | 93 | final Color neutral; 94 | 95 | // Other: Button colors 96 | final Color primaryPressed; 97 | final Color primaryDisabled; 98 | final Color secondaryPressed; 99 | final Color secondaryDisabled; 100 | final Color tertiary; 101 | final Color tertiaryDisabled; 102 | final Color symbolGray; 103 | final Color border; 104 | final Color header; 105 | final Color onHeader; 106 | } 107 | -------------------------------------------------------------------------------- /lib/ui/common/look/look_data/specific_look_data/motion_look_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | @immutable 4 | class MotionLookData { 5 | const MotionLookData({ 6 | required this.durationVeryFast, 7 | required this.durationFast, 8 | required this.durationNormal, 9 | required this.durationSlow, 10 | }); 11 | 12 | const MotionLookData.getDefault() 13 | : durationVeryFast = const Duration(milliseconds: 100), 14 | durationFast = const Duration(milliseconds: 200), 15 | durationNormal = const Duration(milliseconds: 300), 16 | durationSlow = const Duration(milliseconds: 500); 17 | 18 | final Duration durationVeryFast; 19 | final Duration durationFast; 20 | final Duration durationNormal; 21 | final Duration durationSlow; 22 | } 23 | -------------------------------------------------------------------------------- /lib/ui/common/look/look_data/specific_look_data/shape_look_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | @immutable 4 | class ShapeLookData { 5 | const ShapeLookData({ 6 | required this.cardBorderRadius, 7 | }); 8 | 9 | ShapeLookData.getDefault() : cardBorderRadius = BorderRadius.circular(24); 10 | 11 | // final ShapeBorder buttonShape; 12 | // final ShapeBorder circleButtonShape; 13 | final BorderRadius cardBorderRadius; 14 | } 15 | -------------------------------------------------------------------------------- /lib/ui/common/look/look_data/specific_look_data/typography_look_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | @immutable 4 | class TypographyLookData { 5 | const TypographyLookData({ 6 | required this.primaryFontFamily, 7 | required this.h1, 8 | required this.h2, 9 | required this.h3, 10 | required this.h4, 11 | required this.subtitle1, 12 | required this.subtitle2, 13 | required this.body, 14 | required this.body2, 15 | required this.button, 16 | required this.caption, 17 | required this.overline, 18 | required this.label, 19 | required this.tweetBody, 20 | required this.tweetBold, 21 | required this.symbolLabel, 22 | required this.headerLabel, 23 | required this.lightButton, 24 | }); 25 | 26 | const TypographyLookData.getDefault() 27 | : primaryFontFamily = 'Roboto', 28 | h1 = const TextStyle( 29 | fontWeight: FontWeight.w800, 30 | fontSize: 45, 31 | letterSpacing: -0.3, 32 | ), 33 | h2 = const TextStyle( 34 | fontSize: 32, 35 | letterSpacing: -0.75, 36 | ), 37 | h3 = const TextStyle( 38 | fontWeight: FontWeight.w800, 39 | fontSize: 22, 40 | letterSpacing: -0.3, 41 | ), 42 | h4 = const TextStyle( 43 | fontWeight: FontWeight.bold, 44 | fontSize: 18, 45 | ), 46 | subtitle1 = const TextStyle( 47 | fontWeight: FontWeight.w400, 48 | fontSize: 16, 49 | letterSpacing: -0.3, 50 | ), 51 | subtitle2 = const TextStyle( 52 | fontWeight: FontWeight.bold, 53 | fontSize: 15, 54 | ), 55 | body = const TextStyle( 56 | fontSize: 17, 57 | fontWeight: FontWeight.w400, 58 | ), 59 | body2 = const TextStyle( 60 | fontSize: 15, 61 | ), 62 | button = const TextStyle( 63 | fontWeight: FontWeight.w600, 64 | fontSize: 18, 65 | color: Colors.white, 66 | letterSpacing: -0.1, 67 | ), 68 | caption = const TextStyle( 69 | fontWeight: FontWeight.w400, 70 | fontSize: 19, 71 | letterSpacing: -0.5, 72 | ), 73 | overline = const TextStyle( 74 | fontSize: 12, 75 | fontWeight: FontWeight.w400, 76 | ), 77 | label = const TextStyle( 78 | fontSize: 16, 79 | letterSpacing: -0.3, 80 | fontWeight: FontWeight.w400, 81 | ), 82 | tweetBody = const TextStyle( 83 | fontSize: 16, 84 | fontWeight: FontWeight.w400, 85 | letterSpacing: -0.3, 86 | ), 87 | tweetBold = const TextStyle( 88 | fontSize: 16, 89 | fontWeight: FontWeight.w700, 90 | letterSpacing: -0.3, 91 | ), 92 | symbolLabel = const TextStyle( 93 | fontSize: 12, 94 | fontWeight: FontWeight.w400, 95 | letterSpacing: -0.3, 96 | ), 97 | headerLabel = const TextStyle( 98 | fontSize: 22, 99 | fontWeight: FontWeight.w700, 100 | letterSpacing: -0.9, 101 | ), 102 | lightButton = const TextStyle( 103 | fontSize: 17, 104 | fontWeight: FontWeight.w400, 105 | letterSpacing: -0.3, 106 | ); 107 | 108 | final String primaryFontFamily; 109 | 110 | final TextStyle h1; 111 | final TextStyle h2; 112 | final TextStyle h3; 113 | final TextStyle h4; 114 | 115 | final TextStyle subtitle1; 116 | final TextStyle subtitle2; 117 | 118 | final TextStyle body; 119 | final TextStyle body2; 120 | final TextStyle button; 121 | 122 | final TextStyle caption; 123 | final TextStyle overline; 124 | 125 | final TextStyle label; 126 | 127 | final TextStyle tweetBody; 128 | final TextStyle tweetBold; 129 | final TextStyle symbolLabel; 130 | final TextStyle headerLabel; 131 | final TextStyle lightButton; 132 | } 133 | -------------------------------------------------------------------------------- /lib/ui/common/look/mapping/theme_data_mapping/theme_data_mapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../look_data/look_data.dart'; 4 | 5 | /// We use Look to define style in our app, and ThemeData is used by Flutter framework. 6 | /// Here we try to map as many ThemeData fields from Look. 7 | class ThemeDataMapper { 8 | ThemeDataMapper._(); 9 | 10 | static ThemeData map(LookData lookData) { 11 | final defaultTheme = ThemeData.light(); 12 | 13 | return ThemeData( 14 | fontFamily: lookData.typography.primaryFontFamily, 15 | colorScheme: ColorScheme( 16 | primary: lookData.color.primary, 17 | primaryContainer: lookData.color.primaryContainer, 18 | secondary: lookData.color.secondary, 19 | onSecondary: lookData.color.onSecondary, 20 | secondaryContainer: lookData.color.secondaryContainer, 21 | surface: lookData.color.surface, 22 | background: lookData.color.background, 23 | error: lookData.color.error, 24 | onPrimary: lookData.color.onPrimary, 25 | onSurface: lookData.color.onSurface, 26 | onBackground: lookData.color.onBackground, 27 | onError: lookData.color.onError, 28 | brightness: lookData.color.brightness), 29 | primaryColor: lookData.color.primary, 30 | splashColor: lookData.color.secondary, 31 | errorColor: lookData.color.error, 32 | // disabledColor: lookData.color // waiting for designer to add color name 33 | backgroundColor: lookData.color.background, 34 | textTheme: defaultTheme.textTheme.copyWith( 35 | headline1: lookData.typography.h1, 36 | headline2: lookData.typography.h2, 37 | headline3: lookData.typography.h3, 38 | headline4: lookData.typography.h4, 39 | bodyText1: lookData.typography.body, 40 | bodyText2: lookData.typography.body2, 41 | caption: lookData.typography.caption, 42 | button: lookData.typography.button, 43 | subtitle1: lookData.typography.subtitle1, 44 | subtitle2: lookData.typography.subtitle2, 45 | overline: lookData.typography.overline), 46 | inputDecorationTheme: InputDecorationTheme( 47 | hintStyle: lookData.typography.body.copyWith(color: lookData.color.neutral), 48 | labelStyle: lookData.typography.body.copyWith(color: lookData.color.neutral), 49 | errorStyle: lookData.typography.caption.copyWith(color: lookData.color.error), 50 | focusColor: lookData.color.primary, 51 | ), 52 | brightness: lookData.color.brightness, 53 | cardColor: lookData.color.surface, 54 | scaffoldBackgroundColor: lookData.color.background, 55 | // cupertinoOverrideTheme: CupertinoThemeData(primaryColor: lookData.color.primary), // do we need this 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/ui/common/look/widget/look.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../look_data/look_data.dart'; 4 | 5 | /// Simple inherited widget that allows us to do Look.of(context) just like theme works 6 | class Look extends InheritedWidget { 7 | const Look({ 8 | Key? key, 9 | required this.lookData, 10 | required Widget child, 11 | }) : super(key: key, child: child); 12 | 13 | final LookData lookData; 14 | 15 | @override 16 | bool updateShouldNotify(Look oldWidget) { 17 | return lookData != oldWidget.lookData; 18 | } 19 | 20 | static LookData of(BuildContext context) { 21 | return context.dependOnInheritedWidgetOfExactType()!.lookData; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/ui/common/look/widget/look_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../look_data/look_data.dart'; 4 | import 'look.dart'; 5 | 6 | class LookBuilder extends StatelessWidget { 7 | const LookBuilder({ 8 | Key? key, 9 | required this.lookData, 10 | required this.builder, 11 | }) : super(key: key); 12 | 13 | final LookData lookData; 14 | final WidgetBuilder builder; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Look( 19 | lookData: lookData, 20 | child: Builder(builder: builder), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/ui/common/look/widget/look_subtree.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../look_data/look_data.dart'; 5 | import '../look_data/specific_look_data/color_look_data.dart'; 6 | import '../look_data/specific_look_data/motion_look_data.dart'; 7 | import '../look_data/specific_look_data/shape_look_data.dart'; 8 | import '../look_data/specific_look_data/typography_look_data.dart'; 9 | import 'look.dart'; 10 | import 'user_specific_color_provider.dart'; 11 | 12 | /// This is widget that uses [Look] but it expands it with [UserSpecificColorProvider] so it can be refreshed at runtime. 13 | /// 14 | /// If you don't have an use case where you need a runtime theme change, then you don't need to use this or 15 | /// [UserSpecificColorProvider] 16 | class LookSubtree extends ConsumerWidget { 17 | const LookSubtree({Key? key, required this.child}) : super(key: key); 18 | 19 | final Widget child; 20 | 21 | @override 22 | Widget build(BuildContext context, WidgetRef ref) { 23 | final Color color = ref.watch(userSpecificColorProvider); 24 | 25 | return Look( 26 | lookData: _createLookData(color), 27 | child: child, 28 | ); 29 | } 30 | 31 | LookData _createLookData(Color userSpecificColor) { 32 | return LookData( 33 | color: ColorLookData.getDefaultWithUserSpecificColor(userSpecificColor), 34 | motion: const MotionLookData.getDefault(), 35 | shape: ShapeLookData.getDefault(), 36 | typography: const TypographyLookData.getDefault(), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/ui/common/look/widget/user_specific_color_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import 'look_subtree.dart'; 5 | 6 | /// This is example of provider that can change theme and colors at runtime. If you don't have this case then you 7 | /// don't need to use this provider or [LookSubtree]. 8 | /// 9 | /// We use provider to set new primaryContainer color and refresh theme at runtime. 10 | class UserSpecificColorProvider extends StateNotifier { 11 | UserSpecificColorProvider() : super(Colors.grey.shade500); 12 | 13 | void setNewColor(Color newColor) { 14 | state = newColor; 15 | } 16 | } 17 | 18 | final userSpecificColorProvider = StateNotifierProvider((ref) { 19 | return UserSpecificColorProvider(); 20 | }); 21 | -------------------------------------------------------------------------------- /lib/ui/dashboard/dashboard_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_dasher/ui/common/dasher_bottom_navigation_bar.dart'; 4 | import 'package:flutter_dasher/ui/common/dasher_new_tweet_button.dart'; 5 | import 'package:flutter_dasher/ui/common/dasher_tweets_list.dart'; 6 | import 'package:flutter_dasher/ui/common/look/widget/look.dart'; 7 | import 'package:flutter_dasher/ui/dashboard/presenter/current_user_presenter.dart'; 8 | import 'package:flutter_dasher/ui/profile/profile_screen.dart'; 9 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 10 | 11 | class DashboardScreen extends StatelessWidget { 12 | const DashboardScreen({ 13 | Key? key, 14 | }) : super(key: key); 15 | 16 | static Route route() { 17 | return MaterialPageRoute( 18 | builder: (BuildContext context) { 19 | return const DashboardScreen(); 20 | }, 21 | ); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Scaffold( 27 | appBar: AppBar( 28 | automaticallyImplyLeading: false, 29 | title: Container( 30 | alignment: Alignment.centerLeft, 31 | margin: const EdgeInsets.only(right: 30.0, left: 4), 32 | child: const _ProfilePicture(), 33 | ), 34 | backgroundColor: Colors.white, 35 | elevation: 0, 36 | shape: Border( 37 | bottom: BorderSide( 38 | color: Look.of(context).color.border, 39 | width: 0.5, 40 | ), 41 | ), 42 | ), 43 | body: const DasherTweetsList(), 44 | floatingActionButton: const DasherNewTweetButton(), 45 | bottomNavigationBar: const DasherBottomNavigationBar(), 46 | ); 47 | } 48 | } 49 | 50 | class _ProfilePicture extends ConsumerWidget { 51 | const _ProfilePicture({ 52 | Key? key, 53 | }) : super(key: key); 54 | 55 | @override 56 | Widget build(BuildContext context, WidgetRef ref) { 57 | final imageUrl = ref.watch(currentUserPresenter).imageUrl; 58 | 59 | return GestureDetector( 60 | onTap: () => Navigator.of(context).push(ProfileScreen.route()), 61 | child: CircleAvatar( 62 | radius: 16.0, 63 | backgroundImage: CachedNetworkImageProvider(imageUrl!), 64 | backgroundColor: Colors.white, 65 | ), 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/ui/dashboard/presenter/current_user_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/user.dart'; 2 | import 'package:flutter_dasher/domain/data/user_data_holder.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | final currentUserPresenter = StateProvider.autoDispose( 6 | (ref) => ref.watch(userDataHolderProvider).user!, 7 | ); 8 | -------------------------------------------------------------------------------- /lib/ui/dashboard/presenter/feed_request_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/tweet.dart'; 2 | import 'package:flutter_dasher/domain/interactor/dashboard/fetch_feed_interactor.dart'; 3 | import 'package:flutter_dasher/ui/common/bits/request_provider/request_provider.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | final feedRequestPresenter = ChangeNotifierProvider.autoDispose( 7 | (ref) => FeedRequestPresenter(ref.read(fetchFeedInteractorProvider)), 8 | ); 9 | 10 | class FeedRequestPresenter extends RequestProvider> { 11 | FeedRequestPresenter(this._fetchFeedInteractor) { 12 | fetchTweetsTimeline(); 13 | } 14 | 15 | final FetchFeedInteractor _fetchFeedInteractor; 16 | 17 | Future fetchTweetsTimeline() { 18 | return executeRequest(requestBuilder: _fetchFeedInteractor.fetchFeedTimeline); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/ui/login/login_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_dasher/gen/assets.gen.dart'; 3 | import 'package:flutter_dasher/ui/common/buttons/primary_button.dart'; 4 | import 'package:flutter_dasher/ui/common/look/widget/look.dart'; 5 | import 'package:flutter_dasher/ui/dashboard/dashboard_screen.dart'; 6 | import 'package:flutter_dasher/ui/login/presenter/login_request_presenter.dart'; 7 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 8 | 9 | class LoginScreen extends HookConsumerWidget { 10 | const LoginScreen({ 11 | Key? key, 12 | }) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context, WidgetRef ref) { 16 | final _presenter = ref.watch(loginRequestPresenter); 17 | 18 | ref.listen(loginRequestPresenter, (_, presenter) { 19 | presenter.state.whenOrNull( 20 | success: (_) => Navigator.of(context).push(DashboardScreen.route()), 21 | ); 22 | }); 23 | 24 | return Scaffold( 25 | body: SafeArea( 26 | top: false, 27 | child: Column( 28 | mainAxisAlignment: MainAxisAlignment.start, 29 | crossAxisAlignment: CrossAxisAlignment.stretch, 30 | children: [ 31 | const _HeaderIllustration(), 32 | const SizedBox(height: 24), 33 | Text( 34 | 'Dasher', 35 | textAlign: TextAlign.center, 36 | style: Look.of(context).typography.h1.copyWith(color: Look.of(context).color.onBackground), 37 | ), 38 | const SizedBox(height: 34), 39 | Text( 40 | 'Please Log In to continue', 41 | textAlign: TextAlign.center, 42 | style: Look.of(context).typography.subtitle1.copyWith(color: Look.of(context).color.onBackground), 43 | ), 44 | const Spacer(), 45 | Padding( 46 | padding: const EdgeInsets.symmetric( 47 | horizontal: 22, 48 | vertical: 22, 49 | ), 50 | child: PrimaryButton( 51 | onPressed: _presenter.onLoginClicked, 52 | child: const Text('Login with Twitter'), 53 | ), 54 | ), 55 | ], 56 | ), 57 | ), 58 | ); 59 | } 60 | } 61 | 62 | class _HeaderIllustration extends StatelessWidget { 63 | const _HeaderIllustration({ 64 | Key? key, 65 | }) : super(key: key); 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | return Container( 70 | color: Look.of(context).color.primary, 71 | height: 300, 72 | child: Stack( 73 | alignment: Alignment.center, 74 | children: [ 75 | Positioned( 76 | bottom: 44, 77 | child: Assets.svg.logo.svg(), 78 | ), 79 | ], 80 | ), 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/ui/login/presenter/login_request_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/domain/interactor/login/login_interactor.dart'; 2 | import 'package:flutter_dasher/ui/common/bits/request_provider/request_provider.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | final loginRequestPresenter = ChangeNotifierProvider.autoDispose( 6 | (ref) => LoginRequestPresenter(ref.watch(loginInteractorProvider)), 7 | ); 8 | 9 | class LoginRequestPresenter extends RequestProvider { 10 | LoginRequestPresenter(this._loginInteractor); 11 | 12 | final LoginInteractor _loginInteractor; 13 | 14 | void onLoginClicked() { 15 | executeRequest(requestBuilder: () => _loginInteractor.login()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/ui/new_tweet/new_tweet_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_dasher/ui/common/buttons/primary_text_button.dart'; 5 | import 'package:flutter_dasher/ui/common/buttons/primary_variant_button.dart'; 6 | import 'package:flutter_dasher/ui/common/look/widget/look.dart'; 7 | import 'package:flutter_dasher/ui/dashboard/presenter/current_user_presenter.dart'; 8 | import 'package:flutter_dasher/ui/new_tweet/presenter/new_tweet_provider.dart'; 9 | import 'package:flutter_dasher/ui/new_tweet/presenter/new_tweet_request_provider.dart'; 10 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 11 | 12 | class NewTweetScreen extends ConsumerWidget { 13 | const NewTweetScreen({Key? key}) : super(key: key); 14 | 15 | static Route route() { 16 | return MaterialPageRoute( 17 | builder: (BuildContext context) { 18 | return const NewTweetScreen(); 19 | }, 20 | ); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context, WidgetRef ref) { 25 | final _presenter = ref.watch(newTweetPresenter); 26 | final _newTweetPresenter = ref.watch(newTweetRequestPresenter); 27 | final imageUrl = ref.watch(currentUserPresenter).imageUrl; 28 | 29 | ref.listen(newTweetRequestPresenter, (_, presenter) { 30 | presenter.state.whenOrNull( 31 | success: (_) { 32 | Future.delayed(const Duration(milliseconds: 800), () { 33 | Navigator.of(context).pop(); 34 | }); 35 | }, 36 | ); 37 | }); 38 | 39 | return AnnotatedRegion( 40 | value: SystemUiOverlayStyle.dark, 41 | child: Scaffold( 42 | body: SafeArea( 43 | child: SingleChildScrollView( 44 | child: Padding( 45 | padding: const EdgeInsets.symmetric(horizontal: 20.0), 46 | child: Column( 47 | children: [ 48 | Row( 49 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 50 | children: [ 51 | PrimaryTextButton( 52 | child: const Text('Cancel'), 53 | onPressed: () => Navigator.of(context).pop(), 54 | ), 55 | _newTweetPresenter.state.maybeWhen( 56 | orElse: () => PrimaryVariantButton( 57 | child: const Text('Tweet'), 58 | onPressed: () => _newTweetPresenter.postNewTweet(), 59 | ), 60 | success: (_) => Icon( 61 | Icons.check, 62 | size: 30, 63 | color: Look.of(context).color.primary, 64 | ), 65 | failure: (_) => Icon( 66 | Icons.error_outline, 67 | size: 30, 68 | color: Look.of(context).color.error, 69 | ), 70 | ), 71 | ], 72 | ), 73 | Row( 74 | crossAxisAlignment: CrossAxisAlignment.start, 75 | children: [ 76 | Padding( 77 | padding: const EdgeInsets.only(right: 14, top: 5), 78 | child: _buildProfilePicture(imageUrl), 79 | ), 80 | Flexible( 81 | child: TextFormField( 82 | maxLines: null, 83 | textInputAction: TextInputAction.go, 84 | style: Look.of(context).typography.caption, 85 | decoration: InputDecoration( 86 | border: InputBorder.none, 87 | hintText: "What's happening?", 88 | hintStyle: Look.of(context).typography.caption.copyWith(color: Look.of(context).color.symbolGray), 89 | ), 90 | onChanged: _presenter.onNewTweetChanged, 91 | ), 92 | ) 93 | ], 94 | ) 95 | ], 96 | ), 97 | ), 98 | ), 99 | ), 100 | ), 101 | ); 102 | } 103 | 104 | Widget _buildProfilePicture(String? imageUrl) { 105 | if (imageUrl != null) { 106 | return CircleAvatar( 107 | radius: 18.0, 108 | backgroundImage: CachedNetworkImageProvider(imageUrl), 109 | backgroundColor: Colors.white, 110 | ); 111 | } else { 112 | return const CircleAvatar( 113 | radius: 18.0, 114 | backgroundColor: Colors.grey, 115 | ); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/ui/new_tweet/presenter/new_tweet_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_dasher/common/model/new_tweet.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | final newTweetPresenter = ChangeNotifierProvider.autoDispose( 6 | (ref) => NewTweetPresenter(), 7 | ); 8 | 9 | class NewTweetPresenter extends ChangeNotifier { 10 | NewTweetPresenter({String tweetText = ''}) : _tweetText = tweetText; 11 | 12 | String _tweetText; 13 | String get tweetText => _tweetText; 14 | 15 | void onNewTweetChanged(String tweetText) { 16 | _tweetText = tweetText; 17 | } 18 | 19 | NewTweet buildNewTweetPost() { 20 | return NewTweet(tweetText); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/ui/new_tweet/presenter/new_tweet_request_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/domain/interactor/new_tweet/new_tweet_interactor.dart'; 2 | import 'package:flutter_dasher/ui/common/bits/request_provider/request_provider.dart'; 3 | import 'package:flutter_dasher/ui/new_tweet/presenter/new_tweet_provider.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | final newTweetRequestPresenter = ChangeNotifierProvider.autoDispose( 7 | (ref) => NewTweetRequestPresenter(ref.watch(newTweetInteractorProvider), ref), 8 | ); 9 | 10 | class NewTweetRequestPresenter extends RequestProvider { 11 | NewTweetRequestPresenter(this._newTweetInteractor, this._ref); 12 | 13 | final NewTweetInteractor _newTweetInteractor; 14 | final Ref _ref; 15 | 16 | Future postNewTweet() { 17 | return executeRequest(requestBuilder: () async { 18 | final presenter = _ref.read(newTweetPresenter); 19 | return await _newTweetInteractor.postNewTweet(presenter.buildNewTweetPost()); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/ui/profile/presenter/profile_request_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/common/model/tweet.dart'; 2 | import 'package:flutter_dasher/domain/interactor/profile/profile_tweets_interactor.dart'; 3 | import 'package:flutter_dasher/ui/common/bits/request_provider/request_provider.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | final profileRequestPresenter = ChangeNotifierProvider.autoDispose( 7 | (ref) => ProfileRequestPresenter(ref.watch(profileTweetsInteractorProvider)), 8 | ); 9 | 10 | class ProfileRequestPresenter extends RequestProvider> { 11 | ProfileRequestPresenter(this._profileTweetsInteractor) { 12 | fetchProfileTweets(); 13 | } 14 | 15 | final ProfileTweetsInteractor _profileTweetsInteractor; 16 | 17 | Future fetchProfileTweets() { 18 | return executeRequest(requestBuilder: _profileTweetsInteractor.fetchProfileTweets); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/ui/profile/profile_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_dasher/common/model/tweet.dart'; 4 | import 'package:flutter_dasher/ui/common/dasher_bottom_navigation_bar.dart'; 5 | import 'package:flutter_dasher/ui/common/dasher_new_tweet_button.dart'; 6 | import 'package:flutter_dasher/ui/common/dasher_tweet.dart'; 7 | import 'package:flutter_dasher/ui/dashboard/presenter/current_user_presenter.dart'; 8 | import 'package:flutter_dasher/ui/profile/presenter/profile_request_presenter.dart'; 9 | import 'package:flutter_dasher/ui/profile/widget/header_bar_component.dart'; 10 | import 'package:flutter_dasher/ui/profile/widget/profile_info_component.dart'; 11 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 12 | 13 | class ProfileScreen extends ConsumerWidget { 14 | const ProfileScreen({Key? key}) : super(key: key); 15 | 16 | static Route route() { 17 | return MaterialPageRoute( 18 | builder: (BuildContext context) { 19 | return const ProfileScreen(); 20 | }, 21 | ); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context, WidgetRef ref) { 26 | final user = ref.watch(currentUserPresenter); 27 | final _presenter = ref.watch(profileRequestPresenter); 28 | 29 | return AnnotatedRegion( 30 | value: SystemUiOverlayStyle.light, 31 | child: Scaffold( 32 | body: RefreshIndicator( 33 | onRefresh: ref.read(profileRequestPresenter).fetchProfileTweets, 34 | child: CustomScrollView( 35 | scrollDirection: Axis.vertical, 36 | slivers: [ 37 | SliverPersistentHeader( 38 | delegate: HeaderBar( 39 | expandedHeight: 138, 40 | shrinkHeight: 100, 41 | profileName: user.name, 42 | avatarURL: user.imageUrl, 43 | ), 44 | pinned: true, 45 | ), 46 | SliverToBoxAdapter( 47 | child: ProfileInfo( 48 | name: user.name, 49 | usernameTag: '@${user.username}', 50 | bio: user.description, 51 | following: user.following.toString(), 52 | followers: user.followers.toString(), 53 | ), 54 | ), 55 | _presenter.state.maybeWhen( 56 | orElse: () => const _LoadingIndicator(), 57 | success: (tweets) => _TweetsList( 58 | tweets: tweets, 59 | ), 60 | failure: (e) => SliverFillRemaining( 61 | child: Center( 62 | child: Text('Error occurred $e'), 63 | ), 64 | ), 65 | initial: () => const _LoadingIndicator(), 66 | loading: (tweets) { 67 | if (tweets == null) { 68 | return const _LoadingIndicator(); 69 | } else { 70 | return _TweetsList( 71 | tweets: tweets, 72 | ); 73 | } 74 | }, 75 | ), 76 | ], 77 | ), 78 | ), 79 | floatingActionButton: const DasherNewTweetButton(), 80 | bottomNavigationBar: const DasherBottomNavigationBar(), 81 | ), 82 | ); 83 | } 84 | } 85 | 86 | class _TweetsList extends StatelessWidget { 87 | const _TweetsList({ 88 | Key? key, 89 | required this.tweets, 90 | }) : super(key: key); 91 | 92 | final List tweets; 93 | 94 | @override 95 | Widget build(BuildContext context) { 96 | return SliverList( 97 | delegate: SliverChildBuilderDelegate( 98 | (BuildContext context, int index) { 99 | return DasherTweet( 100 | avatarURL: tweets[index].profileImageUrl, 101 | name: tweets[index].name, 102 | usernameTag: tweets[index].username, 103 | createdAt: tweets[index].createdAt, 104 | tweetText: tweets[index].text, 105 | commentsCount: tweets[index].replyCount.toString(), 106 | retweetsCount: tweets[index].retweetCount.toString(), 107 | likesCount: tweets[index].likeCount.toString(), 108 | ); 109 | }, 110 | childCount: tweets.length, // 1000 list items 111 | ), 112 | ); 113 | } 114 | } 115 | 116 | class _LoadingIndicator extends StatelessWidget { 117 | const _LoadingIndicator({ 118 | Key? key, 119 | }) : super(key: key); 120 | 121 | @override 122 | Widget build(BuildContext context) { 123 | return const SliverFillRemaining( 124 | child: Center( 125 | child: CircularProgressIndicator(), 126 | ), 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/ui/profile/widget/follow_counter_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_dasher/ui/common/look/widget/look.dart'; 3 | 4 | class FollowCounter extends StatelessWidget { 5 | const FollowCounter({ 6 | Key? key, 7 | required this.counter, 8 | required this.text, 9 | }) : super(key: key); 10 | 11 | final String counter; 12 | final String text; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Padding( 17 | padding: const EdgeInsets.only(right: 10.0), 18 | child: RichText( 19 | text: TextSpan( 20 | text: '$counter ', 21 | style: Look.of(context).typography.tweetBold.copyWith(color: Look.of(context).color.onBackground), 22 | children: [ 23 | TextSpan( 24 | text: text, 25 | style: Look.of(context).typography.tweetBody.copyWith(color: Look.of(context).color.symbolGray), 26 | ), 27 | ], 28 | ), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/ui/profile/widget/header_bar_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_dasher/ui/common/look/widget/look.dart'; 4 | 5 | class HeaderBar extends SliverPersistentHeaderDelegate { 6 | HeaderBar({ 7 | required this.expandedHeight, 8 | required this.shrinkHeight, 9 | required this.profileName, 10 | required this.avatarURL, 11 | }); 12 | 13 | final double expandedHeight; 14 | final double shrinkHeight; 15 | final String profileName; 16 | final String? avatarURL; 17 | 18 | // Distance in points from maximum expanded header to minimum shrunk header 19 | double get _distanceToShrink => expandedHeight - shrinkHeight; 20 | 21 | @override 22 | double get maxExtent => expandedHeight; 23 | 24 | @override 25 | double get minExtent => shrinkHeight; 26 | 27 | @override 28 | bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => true; 29 | 30 | @override 31 | Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { 32 | return Stack( 33 | fit: StackFit.expand, 34 | clipBehavior: Clip.none, 35 | alignment: Alignment.center, 36 | children: [ 37 | Container( 38 | decoration: BoxDecoration( 39 | color: Look.of(context).color.header, 40 | ), 41 | ), 42 | Positioned( 43 | top: _calculateHeaderTextTopPosition(shrinkOffset), 44 | child: Text( 45 | profileName, 46 | style: Look.of(context).typography.headerLabel.copyWith(color: Look.of(context).color.onHeader), 47 | ), 48 | ), 49 | Positioned( 50 | top: _calculateAvatarTopPosition(shrinkOffset), 51 | left: 20, 52 | child: Opacity( 53 | opacity: _calculateAvatarOpacity(shrinkOffset), 54 | child: Container( 55 | decoration: BoxDecoration( 56 | color: Look.of(context).color.onSurface, 57 | image: DecorationImage( 58 | image: CachedNetworkImageProvider(avatarURL!), 59 | fit: BoxFit.contain, 60 | ), 61 | shape: BoxShape.circle, 62 | border: Border.all( 63 | color: Look.of(context).color.background, 64 | width: 4, 65 | ), 66 | ), 67 | child: SizedBox( 68 | height: expandedHeight, 69 | width: 70, 70 | ), 71 | ), 72 | ), 73 | ), 74 | Positioned( 75 | top: 42, 76 | left: 14, 77 | child: ElevatedButton( 78 | onPressed: () => Navigator.pop(context), 79 | style: ElevatedButton.styleFrom( 80 | minimumSize: Size.zero, 81 | shape: const CircleBorder(), 82 | padding: const EdgeInsets.all(8), 83 | primary: Look.of(context).color.black, // <-- Button color 84 | ), 85 | child: Icon( 86 | Icons.arrow_back_ios_new_rounded, 87 | color: Look.of(context).color.background, 88 | size: 18, 89 | ), 90 | ), 91 | ), 92 | ], 93 | ); 94 | } 95 | 96 | double _calculateHeaderTextTopPosition(double shrinkOffset) { 97 | // Value from 1 to 0, when header is maximum extended value is 1, on shrunk header value is 0 98 | final double revertedProgressToShrink = 1 - shrinkOffset / _distanceToShrink; 99 | 100 | // Offset from center for text on extended header 101 | const double textExtendedOffset = 10; 102 | 103 | // Animate text top position from extended to shrunk, when header is shrunk text stays always in the middle of header 104 | if (shrinkOffset < _distanceToShrink) { 105 | return expandedHeight / 2 - shrinkOffset / 2 - textExtendedOffset * revertedProgressToShrink; 106 | } else { 107 | return shrinkHeight / 2; 108 | } 109 | } 110 | 111 | double _calculateAvatarTopPosition(double shrinkOffset) { 112 | // Correct top distance of avatar when header is shrunk 113 | final double avatarPositionOnShrunk = shrinkHeight / 2 - 20; 114 | 115 | // Animate avatar top position from extended to shrunk, when the header is shrunk avatar stays always on the header bottom line 116 | if (shrinkOffset < _distanceToShrink) { 117 | return expandedHeight / 2 - shrinkOffset; 118 | } else { 119 | return avatarPositionOnShrunk; 120 | } 121 | } 122 | 123 | double _calculateAvatarOpacity(double shrinkOffset) { 124 | // Value from 0 to 1, when header is maximum extended value is 0, on shrunk header value is 1 125 | final double progressToShrink = shrinkOffset / _distanceToShrink; 126 | 127 | // Multiplier how much faster will animation finish from extended header to shrunk 128 | const double progressSpeed = 2; 129 | 130 | // Because of progressSpeed, the animation will finish faster in case of 2 will finish half way of header collapse 131 | if (progressSpeed * shrinkOffset < _distanceToShrink) { 132 | return 1 - progressSpeed * progressToShrink; 133 | } else { 134 | return 0; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /lib/ui/profile/widget/profile_info_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_dasher/ui/common/look/widget/look.dart'; 3 | import 'package:flutter_dasher/ui/profile/widget/follow_counter_component.dart'; 4 | 5 | class ProfileInfo extends StatelessWidget { 6 | const ProfileInfo({ 7 | Key? key, 8 | required this.name, 9 | required this.usernameTag, 10 | this.bio, 11 | required this.following, 12 | required this.followers, 13 | }) : super(key: key); 14 | 15 | final String name; 16 | final String usernameTag; 17 | final String? bio; 18 | final String following; 19 | final String followers; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Padding( 24 | padding: const EdgeInsets.only( 25 | left: 20.0, 26 | top: 55, 27 | right: 20, 28 | bottom: 0, 29 | ), 30 | child: Column( 31 | crossAxisAlignment: CrossAxisAlignment.start, 32 | children: [ 33 | Text( 34 | name, 35 | style: Look.of(context).typography.h3.copyWith(color: Look.of(context).color.onBackground), 36 | ), 37 | const SizedBox(height: 4), 38 | Text( 39 | usernameTag, 40 | style: Look.of(context).typography.tweetBody.copyWith(color: Look.of(context).color.symbolGray), 41 | ), 42 | const SizedBox(height: 15), 43 | if (bio != null) 44 | Padding( 45 | padding: const EdgeInsets.only(bottom: 38.0), 46 | child: Text( 47 | bio!, 48 | style: Look.of(context).typography.tweetBody.copyWith(color: Look.of(context).color.onBackground), 49 | ), 50 | ), 51 | Row( 52 | children: [ 53 | FollowCounter(counter: following, text: 'Following'), 54 | FollowCounter(counter: followers, text: 'Followers'), 55 | ], 56 | ), 57 | const SizedBox(height: 20), 58 | ], 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/ui/routing/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/ui/routing/routes.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | 4 | final router = GoRouter( 5 | initialLocation: '/login', 6 | routes: $appRoutes, 7 | ); 8 | -------------------------------------------------------------------------------- /lib/ui/routing/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dasher/ui/dashboard/dashboard_screen.dart'; 2 | import 'package:flutter_dasher/ui/login/login_screen.dart'; 3 | import 'package:flutter_dasher/ui/profile/profile_screen.dart'; 4 | import 'package:go_router/go_router.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | part 'routes.g.dart'; 8 | 9 | @TypedGoRoute( 10 | path: '/', 11 | routes: [ 12 | TypedGoRoute( 13 | path: 'profile', 14 | ), 15 | ], 16 | ) 17 | class DashboardScreenRoute extends GoRouteData { 18 | @override 19 | Widget build(BuildContext context, GoRouterState state) { 20 | return const DashboardScreen(); 21 | } 22 | } 23 | 24 | class ProfileScreenRoute extends GoRouteData { 25 | @override 26 | Widget build(BuildContext context, GoRouterState state) { 27 | return const ProfileScreen(); 28 | } 29 | } 30 | 31 | 32 | @TypedGoRoute( 33 | path: '/login', 34 | ) 35 | class LoginScreenRoute extends GoRouteData { 36 | @override 37 | Widget build(BuildContext context, GoRouterState state) { 38 | return const LoginScreen(); 39 | } 40 | } -------------------------------------------------------------------------------- /lib/ui/routing/routes.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'routes.dart'; 4 | 5 | // ************************************************************************** 6 | // GoRouterGenerator 7 | // ************************************************************************** 8 | 9 | List get $appRoutes => [ 10 | $dashboardScreenRoute, 11 | $loginScreenRoute, 12 | ]; 13 | 14 | RouteBase get $dashboardScreenRoute => GoRouteData.$route( 15 | path: '/', 16 | factory: $DashboardScreenRouteExtension._fromState, 17 | routes: [ 18 | GoRouteData.$route( 19 | path: 'profile', 20 | factory: $ProfileScreenRouteExtension._fromState, 21 | ), 22 | ], 23 | ); 24 | 25 | extension $DashboardScreenRouteExtension on DashboardScreenRoute { 26 | static DashboardScreenRoute _fromState(GoRouterState state) => 27 | DashboardScreenRoute(); 28 | 29 | String get location => GoRouteData.$location( 30 | '/', 31 | ); 32 | 33 | void go(BuildContext context) => context.go(location); 34 | 35 | Future push(BuildContext context) => context.push(location); 36 | 37 | void pushReplacement(BuildContext context) => 38 | context.pushReplacement(location); 39 | 40 | void replace(BuildContext context) => context.replace(location); 41 | } 42 | 43 | extension $ProfileScreenRouteExtension on ProfileScreenRoute { 44 | static ProfileScreenRoute _fromState(GoRouterState state) => 45 | ProfileScreenRoute(); 46 | 47 | String get location => GoRouteData.$location( 48 | '/profile', 49 | ); 50 | 51 | void go(BuildContext context) => context.go(location); 52 | 53 | Future push(BuildContext context) => context.push(location); 54 | 55 | void pushReplacement(BuildContext context) => 56 | context.pushReplacement(location); 57 | 58 | void replace(BuildContext context) => context.replace(location); 59 | } 60 | 61 | RouteBase get $loginScreenRoute => GoRouteData.$route( 62 | path: '/login', 63 | factory: $LoginScreenRouteExtension._fromState, 64 | ); 65 | 66 | extension $LoginScreenRouteExtension on LoginScreenRoute { 67 | static LoginScreenRoute _fromState(GoRouterState state) => LoginScreenRoute(); 68 | 69 | String get location => GoRouteData.$location( 70 | '/login', 71 | ); 72 | 73 | void go(BuildContext context) => context.go(location); 74 | 75 | Future push(BuildContext context) => context.push(location); 76 | 77 | void pushReplacement(BuildContext context) => 78 | context.pushReplacement(location); 79 | 80 | void replace(BuildContext context) => context.replace(location); 81 | } 82 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_dasher 2 | description: A new Flutter onboarding project. 3 | publish_to: 'none' 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: ">=2.17.1 <3.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | cupertino_icons: ^1.0.2 13 | flutter_hooks: ^0.20.4 14 | flutter_inappwebview: ^6.0.0 15 | flutter_svg: ^2.0.9 16 | riverpod: ^2.4.9 17 | riverpod_annotation: ^2.3.3 18 | dio: ^5.4.0 19 | flutter_secure_storage: ^9.0.0 20 | go_router: ^13.0.1 21 | japx: ^2.0.4 22 | loggy: ^2.0.1+1 23 | hooks_riverpod: ^2.4.9 24 | flutter_launcher_icons: ^0.13.1 25 | flutter_native_splash: ^2.3.9 26 | flutter_gen_runner: ^5.4.0 27 | flutter_loggy: ^2.0.1 28 | freezed_annotation: ^2.0.3 29 | cached_network_image: ^3.3.1 30 | twitter_oauth2_pkce: ^1.0.2 31 | twitter_api_v2: ^4.9.4 32 | intl: ^0.19.0 33 | json_annotation: ^4.8.1 34 | 35 | dev_dependencies: 36 | flutter_test: 37 | sdk: flutter 38 | flutter_lints: ^3.0.1 39 | flutter_gen: ^5.4.0 40 | build_runner: ^2.4.8 41 | freezed: ^2.4.6 42 | go_router_builder: ^2.4.1 43 | json_serializable: ^6.7.1 44 | custom_lint: ^0.5.8 45 | riverpod_generator: ^2.3.9 46 | riverpod_lint: ^2.3.7 47 | 48 | flutter_gen: 49 | integrations: 50 | flutter_svg: true 51 | 52 | flutter: 53 | uses-material-design: true 54 | assets: 55 | - assets/png/ 56 | - assets/svg/ 57 | 58 | flutter_icons: 59 | image_path: 'assets/app_icons/app_icon.png' 60 | android: true 61 | ios: true 62 | remove_alpha_ios: true 63 | 64 | flutter_native_splash: 65 | color: "#1DA1F2" 66 | image: assets/images/splash_center_logo.png -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | --------------------------------------------------------------------------------