├── .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 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.run/Production [release].run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.run/Production.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.run/Staging [profile].run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.run/Staging [release].run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.run/Staging.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/__brick__/.run/Staging.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
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 |
5 |
--------------------------------------------------------------------------------
/assets/svg/logo.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/assets/svg/navigation_bell.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/svg/navigation_home.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/svg/navigation_mail.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/svg/navigation_search.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/svg/tweet_comment.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/svg/tweet_like.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/svg/tweet_retweet.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/svg/tweet_share.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------