├── .firebaserc
├── .github
├── images
│ └── time-tracker-screenshots.png
└── workflows
│ └── tests.yaml
├── .gitignore
├── .metadata
├── .vscode
└── launch.json
├── LICENSE.md
├── README.md
├── analysis_options.yaml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── starter_architecture_flutter_firebase
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
└── time-tracking.svg
├── firebase.json
├── ios
├── .gitignore
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ └── Release.xcconfig
├── Podfile
├── Podfile.lock
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.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
│ └── 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
├── lib
├── main.dart
└── src
│ ├── app.dart
│ ├── common_widgets
│ ├── action_text_button.dart
│ ├── async_value_widget.dart
│ ├── avatar.dart
│ ├── custom_text_button.dart
│ ├── date_time_picker.dart
│ ├── empty_content.dart
│ ├── empty_placeholder_widget.dart
│ ├── error_message_widget.dart
│ ├── input_dropdown.dart
│ ├── list_items_builder.dart
│ ├── primary_button.dart
│ ├── responsive_center.dart
│ ├── responsive_scrollable_card.dart
│ └── segmented_control.dart
│ ├── constants
│ ├── app_sizes.dart
│ ├── breakpoints.dart
│ ├── keys.dart
│ └── strings.dart
│ ├── features
│ ├── authentication
│ │ ├── data
│ │ │ ├── firebase_auth_repository.dart
│ │ │ └── firebase_auth_repository.g.dart
│ │ ├── domain
│ │ │ └── app_user.dart
│ │ └── presentation
│ │ │ ├── auth_providers.dart
│ │ │ ├── auth_providers.g.dart
│ │ │ ├── custom_profile_screen.dart
│ │ │ └── custom_sign_in_screen.dart
│ ├── entries
│ │ ├── application
│ │ │ ├── entries_service.dart
│ │ │ └── entries_service.g.dart
│ │ ├── data
│ │ │ ├── entries_repository.dart
│ │ │ └── entries_repository.g.dart
│ │ ├── domain
│ │ │ ├── daily_jobs_details.dart
│ │ │ ├── entries_list_tile_model.dart
│ │ │ ├── entry.dart
│ │ │ └── entry_job.dart
│ │ └── presentation
│ │ │ ├── entries_screen.dart
│ │ │ └── entry_screen
│ │ │ ├── entry_screen.dart
│ │ │ ├── entry_screen_controller.dart
│ │ │ └── entry_screen_controller.g.dart
│ ├── jobs
│ │ ├── data
│ │ │ ├── jobs_repository.dart
│ │ │ └── jobs_repository.g.dart
│ │ ├── domain
│ │ │ └── job.dart
│ │ └── presentation
│ │ │ ├── edit_job_screen
│ │ │ ├── edit_job_screen.dart
│ │ │ ├── edit_job_screen_controller.dart
│ │ │ ├── edit_job_screen_controller.g.dart
│ │ │ └── job_submit_exception.dart
│ │ │ ├── job_entries_screen
│ │ │ ├── entry_list_item.dart
│ │ │ ├── job_entries_list.dart
│ │ │ ├── job_entries_list_controller.dart
│ │ │ ├── job_entries_list_controller.g.dart
│ │ │ └── job_entries_screen.dart
│ │ │ └── jobs_screen
│ │ │ ├── jobs_screen.dart
│ │ │ ├── jobs_screen_controller.dart
│ │ │ └── jobs_screen_controller.g.dart
│ └── onboarding
│ │ ├── data
│ │ ├── onboarding_repository.dart
│ │ └── onboarding_repository.g.dart
│ │ └── presentation
│ │ ├── onboarding_controller.dart
│ │ ├── onboarding_controller.g.dart
│ │ └── onboarding_screen.dart
│ ├── localization
│ └── string_hardcoded.dart
│ ├── routing
│ ├── app_router.dart
│ ├── app_router.g.dart
│ ├── app_startup.dart
│ ├── app_startup.g.dart
│ ├── go_router_delegate_listener.dart
│ ├── go_router_refresh_stream.dart
│ ├── not_found_screen.dart
│ └── scaffold_with_nested_navigation.dart
│ └── utils
│ ├── alert_dialogs.dart
│ ├── async_value_ui.dart
│ ├── format.dart
│ ├── shared_preferences_provider.dart
│ ├── shared_preferences_provider.g.dart
│ ├── show_alert_dialog.dart
│ └── show_exception_alert_dialog.dart
├── macos
├── .gitignore
├── Flutter
│ ├── Flutter-Debug.xcconfig
│ ├── Flutter-Release.xcconfig
│ └── GeneratedPluginRegistrant.swift
├── Podfile
├── Podfile.lock
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── Runner
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ └── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── app_icon_1024.png
│ │ │ ├── app_icon_128.png
│ │ │ ├── app_icon_16.png
│ │ │ ├── app_icon_256.png
│ │ │ ├── app_icon_32.png
│ │ │ ├── app_icon_512.png
│ │ │ └── app_icon_64.png
│ ├── Base.lproj
│ │ └── MainMenu.xib
│ ├── Configs
│ │ ├── AppInfo.xcconfig
│ │ ├── Debug.xcconfig
│ │ ├── Release.xcconfig
│ │ └── Warnings.xcconfig
│ ├── DebugProfile.entitlements
│ ├── Info.plist
│ ├── MainFlutterWindow.swift
│ └── Release.entitlements
└── RunnerTests
│ └── RunnerTests.swift
├── pubspec.lock
├── pubspec.yaml
├── test
└── src
│ ├── features
│ └── jobs
│ │ └── domain
│ │ └── job_test.dart
│ └── mocks.dart
├── update-android-project.sh
└── web
├── favicon.png
├── flutter_bootstrap.js
├── icons
├── Icon-192.png
├── Icon-512.png
├── Icon-maskable-192.png
└── Icon-maskable-512.png
├── index.html
└── manifest.json
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "starter-architecture-flutter"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.github/images/time-tracker-screenshots.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/.github/images/time-tracker-screenshots.png
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: Run Tests
2 | on: [push, workflow_dispatch]
3 | jobs:
4 | drive:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v1
8 | - name: Write firebase_options.dart
9 | run: echo '{{ secrets.FIREBASE_OPTIONS }}' > lib/firebase_options.dart
10 | - name: Write GoogleService-info.plist
11 | run: echo '{{ secrets.GOOGLE_SERVICE_INFO_PLIST }}' > ios/Runner/GoogleService-Info.plist
12 | - name: Write google-services.json
13 | run: echo '{{ secrets.GOOGLE_SERVICES_JSON }}' > android/app/google-services.json
14 | - uses: subosito/flutter-action@v2.8.0
15 | - name: Run Flutter tests
16 | run: flutter test
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | #*.lock
4 | *.log
5 | *.pyc
6 | *.swp
7 | .DS_Store
8 | .atom/
9 | .buildlog/
10 | .history
11 | .svn/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # Visual Studio Code related
20 | #.vscode/
21 | .vscode/settings.json
22 |
23 | # Flutter repo-specific
24 | /bin/cache/
25 | /bin/mingit/
26 | /dev/benchmarks/mega_gallery/
27 | /dev/bots/.recipe_deps
28 | /dev/bots/android_tools/
29 | /dev/docs/doc/
30 | /dev/docs/flutter.docs.zip
31 | /dev/docs/lib/
32 | /dev/docs/pubspec.yaml
33 | /packages/flutter/coverage/
34 | version
35 |
36 | # Flutter/Dart/Pub related
37 | **/doc/api/
38 | .dart_tool/
39 | .flutter-plugins
40 | .packages
41 | .pub-cache/
42 | .pub/
43 | build/
44 | flutter_*.png
45 | linked_*.ds
46 | unlinked.ds
47 | unlinked_spec.ds
48 | .flutter-plugins-dependencies
49 |
50 | # Android related
51 | **/android/**/gradle-wrapper.jar
52 | **/android/.gradle
53 | **/android/captures/
54 | **/android/gradlew
55 | **/android/gradlew.bat
56 | **/android/local.properties
57 | **/android/**/GeneratedPluginRegistrant.java
58 | **/android/key.properties
59 | *.jks
60 |
61 | # iOS/XCode related
62 | **/ios/**/*.mode1v3
63 | **/ios/**/*.mode2v3
64 | **/ios/**/*.moved-aside
65 | **/ios/**/*.pbxuser
66 | **/ios/**/*.perspectivev3
67 | **/ios/**/*sync/
68 | **/ios/**/.sconsign.dblite
69 | **/ios/**/.tags*
70 | **/ios/**/.vagrant/
71 | **/ios/**/DerivedData/
72 | **/ios/**/Icon?
73 | **/ios/**/Pods/
74 | **/ios/**/.symlinks/
75 | **/ios/**/profile
76 | **/ios/**/xcuserdata
77 | **/ios/.generated/
78 | **/ios/Flutter/App.framework
79 | **/ios/Flutter/Flutter.framework
80 | **/ios/Flutter/Generated.xcconfig
81 | **/ios/Flutter/app.flx
82 | **/ios/Flutter/app.zip
83 | **/ios/Flutter/flutter_assets/
84 | **/ios/ServiceDefinitions.json
85 | **/ios/Runner/GeneratedPluginRegistrant.*
86 | **/ios/Flutter/flutter_export_environment.sh
87 | **/ios/Flutter/.last_build_id
88 |
89 |
90 | # Exceptions to above rules.
91 | !**/ios/**/default.mode1v3
92 | !**/ios/**/default.mode2v3
93 | !**/ios/**/default.pbxuser
94 | !**/ios/**/default.perspectivev3
95 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
96 |
97 | # Firebase configuration files
98 | lib/firebase_options.dart
99 | ios/Runner/GoogleService-Info.plist
100 | ios/firebase_app_id_file.json
101 | macos/Runner/GoogleService-Info.plist
102 | macos/firebase_app_id_file.json
103 | android/app/google-services.json
104 |
105 | # Firebase hosting
106 | .firebase/
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: "4cf269e36de2573851eaef3c763994f8f9be494d"
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: 4cf269e36de2573851eaef3c763994f8f9be494d
17 | base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d
18 | - platform: android
19 | create_revision: 4cf269e36de2573851eaef3c763994f8f9be494d
20 | base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d
21 |
22 | # User provided section
23 |
24 | # List of Local paths (relative to this file) that should be
25 | # ignored by the migrate tool.
26 | #
27 | # Files that are not part of the templates will be ignored by default.
28 | unmanaged_files:
29 | - 'lib/main.dart'
30 | - 'ios/Runner.xcodeproj/project.pbxproj'
31 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Run",
9 | "request": "launch",
10 | "type": "dart"
11 | },
12 | {
13 | "name": "Run (profile mode)",
14 | "request": "launch",
15 | "type": "dart",
16 | "flutterMode": "profile"
17 | },
18 | {
19 | "name": "Run (release mode)",
20 | "request": "launch",
21 | "type": "dart",
22 | "flutterMode": "release"
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 Andrea Bizzotto [bizz84@gmail.com](mailto:bizz84@gmail.com)
2 |
3 | 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:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | 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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Time Tracking app with Flutter & Firebase
2 |
3 | A time tracking application built with Flutter & Firebase:
4 |
5 | 
6 |
7 | This is intended as a **reference app** based on my [Riverpod Architecture](https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/).
8 |
9 | > **Note**: this project used to be called "Started Architecture for Flutter & Firebase" (based on this [old article](https://codewithandrea.com/videos/starter-architecture-flutter-firebase/)). As of January 2023, it follows my updated [Riverpod Architecture](https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/), using the latest packages.
10 |
11 | ## Flutter web preview
12 |
13 | A Flutter web preview of the app is available here:
14 |
15 | - [Time Tracker | Flutter web demo](https://starter-architecture-flutter.web.app)
16 |
17 | ## Features
18 |
19 | - **Simple onboarding page**
20 | - **Full authentication flow** (using email & password)
21 | - **Jobs**: users can view, create, edit, and delete their own private jobs (each job has a name and hourly rate)
22 | - **Entries**: for each job, user can view, create, edit, and delete the corresponding entries (an entry is a task with a start and end time, with an optional comment)
23 | - **A report page** that shows a daily breakdown of all jobs, hours worked and pay, along with the totals.
24 |
25 | All the data is persisted with Firestore and is kept in sync across multiple devices.
26 |
27 | ## Roadmap
28 |
29 | - [ ] Add missing tests
30 | - [x] Stateful Nested Navigation (available since GoRouter 7.1)
31 | - [ ] Use controllers / notifiers consistently across the app (some code still needs to be updated)
32 | - [ ] Add localization
33 | - [ ] Use the new Firebase UI packages where useful
34 | - [ ] Responsive UI
35 |
36 | > This is a tentative roadmap. There is no ETA for any of the points above. This is a low priority project and I don't have much time to maintain it.
37 |
38 | ## Relevant Articles
39 |
40 | The app is based on my Flutter Riverpod architecture, which is explained in detail here:
41 |
42 | - [Flutter App Architecture with Riverpod: An Introduction](https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/)
43 | - [Flutter Project Structure: Feature-first or Layer-first?](https://codewithandrea.com/articles/flutter-project-structure/)
44 | - [Flutter App Architecture: The Repository Pattern](https://codewithandrea.com/articles/flutter-repository-pattern/)
45 | - [How to Build a Robust Flutter App Initialization Flow with Riverpod](https://codewithandrea.com/articles/robust-app-initialization-riverpod/)
46 |
47 | More more info on Riverpod, read this:
48 |
49 | - [Flutter Riverpod 2.0: The Ultimate Guide](https://codewithandrea.com/articles/flutter-state-management-riverpod/)
50 |
51 | ## Packages in use
52 |
53 | These are the main packages used in the app:
54 |
55 | - [Flutter Riverpod](https://pub.dev/packages/flutter_riverpod) for data caching, dependency injection, and more
56 | - [Riverpod Generator](https://pub.dev/packages/riverpod_generator) and [Riverpod Lint](https://pub.dev/packages/riverpod_lint) for the latest Riverpod APIs
57 | - [GoRouter](https://pub.dev/packages/go_router) for navigation
58 | - [Firebase Auth](https://pub.dev/packages/firebase_auth) and [Firebase UI Auth](https://pub.dev/packages/firebase_ui_auth) for authentication
59 | - [Cloud Firestore](https://pub.dev/packages/cloud_firestore) as a realtime database
60 | - [Firebase UI for Firestore](https://pub.dev/packages/firebase_ui_firestore) for the `FirestoreListView` widget with pagination support
61 | - [RxDart](https://pub.dev/packages/rxdart) for combining multiple Firestore collections as needed
62 | - [Intl](https://pub.dev/packages/intl) for currency, date, time formatting
63 | - [Mocktail](https://pub.dev/packages/mocktail) for testing
64 | - [Equatable](https://pub.dev/packages/equatable) to reduce boilerplate code in model classes
65 |
66 | See the [pubspec.yaml](pubspec.yaml) file for the complete list.
67 |
68 | ## Running the project with Firebase
69 |
70 | To use this project with Firebase, follow these steps:
71 |
72 | - Create a new project with the Firebase console
73 | - Enable Firebase Authentication, along with the Email/Password Authentication Sign-in provider in the Firebase Console (Authentication > Sign-in method > Email/Password > Edit > Enable > Save)
74 | - Enable Cloud Firestore
75 |
76 | Then, follow one of the two approaches below. 👇
77 |
78 | ### 1. Using the CLI
79 |
80 | Make sure you have the Firebase CLI and [FlutterFire CLI](https://pub.dev/packages/flutterfire_cli) installed.
81 |
82 | Then run this on the terminal from the root of this project:
83 |
84 | - Run `firebase login` so you have access to the Firebase project you have created
85 | - Run `flutterfire configure` and follow all the steps
86 |
87 | For more info, follow this guide:
88 |
89 | - [How to add Firebase to a Flutter app with FlutterFire CLI](https://codewithandrea.com/articles/flutter-firebase-flutterfire-cli/)
90 |
91 | ### 2. Manual way (not recommended)
92 |
93 | If you don't want to use FlutterFire CLI, follow these steps instead:
94 |
95 | - Register separate iOS, Android, and web apps in the Firebase project settings.
96 | - On Android, use `com.example.starter_architecture_flutter_firebase` as the package name.
97 | - then, [download and copy](https://firebase.google.com/docs/flutter/setup#configure_an_android_app) `google-services.json` into `android/app`.
98 | - On iOS, use `com.example.starterArchitectureFlutterFirebase` as the bundle ID.
99 | - then, [download and copy](https://firebase.google.com/docs/flutter/setup#configure_an_ios_app) `GoogleService-Info.plist` into `iOS/Runner`, and add it to the Runner target in Xcode.
100 |
101 | That's it. Have fun!
102 |
103 | ## [License: MIT](LICENSE.md)
104 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | analyzer:
13 | plugins:
14 | - custom_lint
15 |
16 | linter:
17 | # The lint rules applied to this project can be customized in the
18 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
19 | # included above or to enable additional rules. A list of all available lints
20 | # and their documentation is published at
21 | # https://dart-lang.github.io/linter/lints/index.html.
22 | #
23 | # Instead of disabling a lint rule for the entire project in the
24 | # section below, it can also be suppressed for a single line of code
25 | # or a specific dart file by using the `// ignore: name_of_lint` and
26 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
27 | # producing the lint.
28 | rules:
29 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
30 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
31 |
32 | # Additional information about this file can be found at
33 | # https://dart.dev/guides/language/analysis-options
34 |
--------------------------------------------------------------------------------
/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/to/reference-keystore
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 | app/.cxx
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "com.android.application"
3 | // START: FlutterFire Configuration
4 | id 'com.google.gms.google-services'
5 | // END: FlutterFire Configuration
6 | id "kotlin-android"
7 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
8 | id "dev.flutter.flutter-gradle-plugin"
9 | }
10 |
11 | android {
12 | namespace = "com.example.starter_architecture_flutter_firebase"
13 | compileSdk = flutter.compileSdkVersion
14 | ndkVersion = "27.0.12077973"
15 |
16 | compileOptions {
17 | sourceCompatibility = JavaVersion.VERSION_17
18 | targetCompatibility = JavaVersion.VERSION_17
19 | }
20 |
21 | kotlinOptions {
22 | jvmTarget = JavaVersion.VERSION_17
23 | }
24 |
25 | defaultConfig {
26 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
27 | applicationId = "com.example.starter_architecture_flutter_firebase"
28 | // You can update the following values to match your application needs.
29 | // For more information, see: https://flutter.dev/to/review-gradle-config.
30 | minSdk = 24
31 | targetSdk = 34
32 | versionCode = flutter.versionCode
33 | versionName = flutter.versionName
34 | }
35 |
36 | buildTypes {
37 | release {
38 | // TODO: Add your own signing config for the release build.
39 | // Signing with the debug keys for now, so `flutter run --release` works.
40 | signingConfig = signingConfigs.debug
41 | }
42 | }
43 | }
44 |
45 | flutter {
46 | source = "../.."
47 | }
48 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
15 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
33 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/starter_architecture_flutter_firebase/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.starter_architecture_flutter_firebase
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity()
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | }
7 |
8 | rootProject.buildDir = "../build"
9 | subprojects {
10 | project.buildDir = "${rootProject.buildDir}/${project.name}"
11 | }
12 | subprojects {
13 | project.evaluationDependsOn(":app")
14 | }
15 |
16 | tasks.register("clean", Delete) {
17 | delete rootProject.buildDir
18 | }
19 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
6 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | def flutterSdkPath = {
3 | def properties = new Properties()
4 | file("local.properties").withInputStream { properties.load(it) }
5 | def flutterSdkPath = properties.getProperty("flutter.sdk")
6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
7 | return flutterSdkPath
8 | }()
9 |
10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
11 |
12 | repositories {
13 | google()
14 | mavenCentral()
15 | gradlePluginPortal()
16 | }
17 | }
18 |
19 | plugins {
20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0"
21 | id "com.android.application" version "8.7.2" apply false
22 | // START: FlutterFire Configuration
23 | id "com.google.gms.google-services" version "4.3.15" apply false
24 | // END: FlutterFire Configuration
25 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false
26 | }
27 |
28 | include ":app"
29 |
--------------------------------------------------------------------------------
/assets/time-tracking.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {"hosting":{"public":"build/web","ignore":["firebase.json","**/.*","**/node_modules/**"],"rewrites":[{"source":"**","destination":"/index.html"}]},"flutter":{"platforms":{"android":{"default":{"projectId":"starter-architecture-flutter","appId":"1:204483935261:android:6bf22efec4b84563779af4","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"starter-architecture-flutter","appId":"1:204483935261:ios:df913fb4eeda0a29779af4","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"starter-architecture-flutter","appId":"1:204483935261:ios:df913fb4eeda0a29779af4","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"starter-architecture-flutter","configurations":{"android":"1:204483935261:android:6bf22efec4b84563779af4","ios":"1:204483935261:ios:df913fb4eeda0a29779af4","macos":"1:204483935261:ios:df913fb4eeda0a29779af4","web":"1:204483935261:web:2eff4a630625a401779af4"}}}}}}
--------------------------------------------------------------------------------
/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 | 12.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, '13.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 | pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '11.10.0'
32 | use_frameworks!
33 | use_modular_headers!
34 |
35 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
36 | end
37 |
38 | post_install do |installer|
39 | # Ensure pods also use the minimum deployment target set above
40 | # https://stackoverflow.com/a/64385584/436422
41 | puts 'Determining pod project minimum deployment target'
42 |
43 | pods_project = installer.pods_project
44 | deployment_target_key = 'IPHONEOS_DEPLOYMENT_TARGET'
45 | deployment_targets = pods_project.build_configurations.map{ |config| config.build_settings[deployment_target_key] }
46 | minimum_deployment_target = deployment_targets.min_by{ |version| Gem::Version.new(version) }
47 |
48 | puts 'Minimal deployment target is ' + minimum_deployment_target
49 | puts 'Setting each pod deployment target to ' + minimum_deployment_target
50 |
51 | installer.pods_project.targets.each do |target|
52 | flutter_additional_ios_build_settings(target)
53 | target.build_configurations.each do |config|
54 | #config.build_settings['ENABLE_BITCODE'] = 'NO'
55 | config.build_settings[deployment_target_key] = minimum_deployment_target
56 | # https://stackoverflow.com/a/77142190
57 | xcconfig_path = config.base_configuration_reference.real_path
58 | xcconfig = File.read(xcconfig_path)
59 | xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR")
60 | File.open(xcconfig_path, "w") { |file| file << xcconfig_mod }
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/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/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/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 | @main
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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bizz84/starter_architecture_flutter_firebase/821e21295c2af7292143a9190897b01b574397e8/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 |
--------------------------------------------------------------------------------
/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 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Starter Architecture Flutter Firebase
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | starter_architecture_flutter_firebase
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(FLUTTER_BUILD_NUMBER)
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | UIViewControllerBasedStatusBarAppearance
45 |
46 | CADisableMinimumFrameDurationOnPhone
47 |
48 | UIApplicationSupportsIndirectInputEvents
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_core/firebase_core.dart';
2 | import 'package:flutter/foundation.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_riverpod/flutter_riverpod.dart';
5 | import 'package:starter_architecture_flutter_firebase/firebase_options.dart';
6 | import 'package:starter_architecture_flutter_firebase/src/app.dart';
7 | import 'package:starter_architecture_flutter_firebase/src/localization/string_hardcoded.dart';
8 | // ignore:depend_on_referenced_packages
9 | import 'package:flutter_web_plugins/url_strategy.dart';
10 |
11 | Future main() async {
12 | WidgetsFlutterBinding.ensureInitialized();
13 | // turn off the # in the URLs on the web
14 | usePathUrlStrategy();
15 | // * Register error handlers. For more info, see:
16 | // * https://docs.flutter.dev/testing/errors
17 | registerErrorHandlers();
18 | // * Initialize Firebase
19 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
20 | // * Entry point of the app
21 | runApp(const ProviderScope(
22 | child: MyApp(),
23 | ));
24 | }
25 |
26 | void registerErrorHandlers() {
27 | // * Show some error UI if any uncaught exception happens
28 | FlutterError.onError = (FlutterErrorDetails details) {
29 | FlutterError.presentError(details);
30 | debugPrint(details.toString());
31 | };
32 | // * Handle errors from the underlying platform/OS
33 | PlatformDispatcher.instance.onError = (Object error, StackTrace stack) {
34 | debugPrint(error.toString());
35 | return true;
36 | };
37 | // * Show some error UI when any widget in the app fails to build
38 | ErrorWidget.builder = (FlutterErrorDetails details) {
39 | return Scaffold(
40 | appBar: AppBar(
41 | backgroundColor: Colors.red,
42 | title: Text('An error occurred'.hardcoded),
43 | ),
44 | body: Center(child: Text(details.toString())),
45 | );
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/lib/src/app.dart:
--------------------------------------------------------------------------------
1 | import 'dart:developer';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_riverpod/flutter_riverpod.dart';
5 | import 'package:force_update_helper/force_update_helper.dart';
6 | import 'package:starter_architecture_flutter_firebase/src/routing/app_router.dart';
7 | import 'package:starter_architecture_flutter_firebase/src/routing/app_startup.dart';
8 | import 'package:starter_architecture_flutter_firebase/src/routing/go_router_delegate_listener.dart';
9 | import 'package:starter_architecture_flutter_firebase/src/utils/alert_dialogs.dart';
10 | import 'package:url_launcher/url_launcher.dart';
11 |
12 | class MyApp extends ConsumerWidget {
13 | const MyApp({super.key});
14 |
15 | static const primaryColor = Colors.indigo;
16 |
17 | @override
18 | Widget build(BuildContext context, WidgetRef ref) {
19 | final goRouter = ref.watch(goRouterProvider);
20 | return MaterialApp.router(
21 | routerConfig: goRouter,
22 | builder: (_, child) {
23 | // * Important: Use AppStartupWidget to wrap ForceUpdateWidget otherwise you will get this error:
24 | // * Navigator operation requested with a context that does not include a Navigator.
25 | // * The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.
26 | return AppStartupWidget(
27 | onLoaded: (_) => ForceUpdateWidget(
28 | navigatorKey: goRouter.routerDelegate.navigatorKey,
29 | forceUpdateClient: ForceUpdateClient(
30 | // * Real apps should fetch this from an API endpoint or via
31 | // * Firebase Remote Config
32 | fetchRequiredVersion: () => Future.value('2.0.0'),
33 | // * Example ID from this app: https://fluttertips.dev/
34 | // * To avoid mistakes, store the ID as an environment variable and
35 | // * read it with String.fromEnvironment
36 | iosAppStoreId: '6482293361',
37 | ),
38 | allowCancel: false,
39 | showForceUpdateAlert: (context, allowCancel) => showAlertDialog(
40 | context: context,
41 | title: 'App Update Required',
42 | content: 'Please update to continue using the app.',
43 | cancelActionText: allowCancel ? 'Later' : null,
44 | defaultActionText: 'Update Now',
45 | ),
46 | showStoreListing: (storeUrl) async {
47 | if (await canLaunchUrl(storeUrl)) {
48 | await launchUrl(
49 | storeUrl,
50 | // * Open app store app directly (or fallback to browser)
51 | mode: LaunchMode.externalApplication,
52 | );
53 | } else {
54 | log('Cannot launch URL: $storeUrl');
55 | }
56 | },
57 | onException: (e, st) {
58 | log(e.toString());
59 | },
60 | child: GoRouterDelegateListener(child: child!),
61 | ),
62 | );
63 | },
64 | theme: ThemeData(
65 | colorSchemeSeed: primaryColor,
66 | unselectedWidgetColor: Colors.grey,
67 | appBarTheme: const AppBarTheme(
68 | backgroundColor: primaryColor,
69 | foregroundColor: Colors.white,
70 | elevation: 2.0,
71 | centerTitle: true,
72 | ),
73 | scaffoldBackgroundColor: Colors.grey[200],
74 | dividerColor: Colors.grey[400],
75 | elevatedButtonTheme: ElevatedButtonThemeData(
76 | style: ElevatedButton.styleFrom(
77 | backgroundColor: primaryColor,
78 | foregroundColor: Colors.white,
79 | ),
80 | ),
81 | outlinedButtonTheme: OutlinedButtonThemeData(
82 | style: OutlinedButton.styleFrom(
83 | backgroundColor: primaryColor,
84 | foregroundColor: Colors.white,
85 | ),
86 | ),
87 | floatingActionButtonTheme: const FloatingActionButtonThemeData(
88 | backgroundColor: primaryColor,
89 | ),
90 | ),
91 | debugShowCheckedModeBanner: false,
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/action_text_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart';
3 |
4 | /// Text button to be used as an [AppBar] action
5 | class ActionTextButton extends StatelessWidget {
6 | const ActionTextButton({super.key, required this.text, this.onPressed});
7 | final String text;
8 | final VoidCallback? onPressed;
9 | @override
10 | Widget build(BuildContext context) {
11 | return Padding(
12 | padding: const EdgeInsets.symmetric(horizontal: Sizes.p16),
13 | child: TextButton(
14 | onPressed: onPressed,
15 | child: Text(text,
16 | style: Theme.of(context)
17 | .textTheme
18 | .titleLarge!
19 | .copyWith(color: Colors.white)),
20 | ),
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/async_value_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:starter_architecture_flutter_firebase/src/common_widgets/error_message_widget.dart';
4 |
5 | class AsyncValueWidget extends StatelessWidget {
6 | const AsyncValueWidget({super.key, required this.value, required this.data});
7 | final AsyncValue value;
8 | final Widget Function(T) data;
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return value.when(
13 | data: data,
14 | error: (e, st) => Center(child: ErrorMessageWidget(e.toString())),
15 | loading: () => const Center(child: CircularProgressIndicator()),
16 | );
17 | }
18 | }
19 |
20 | class ScaffoldAsyncValueWidget extends StatelessWidget {
21 | const ScaffoldAsyncValueWidget(
22 | {super.key, required this.value, required this.data});
23 | final AsyncValue value;
24 | final Widget Function(T) data;
25 |
26 | @override
27 | Widget build(BuildContext context) {
28 | return value.when(
29 | data: data,
30 | error: (e, st) => Scaffold(
31 | appBar: AppBar(),
32 | body: Center(child: ErrorMessageWidget(e.toString())),
33 | ),
34 | loading: () => Scaffold(
35 | appBar: AppBar(),
36 | body: const Center(child: CircularProgressIndicator()),
37 | ),
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/avatar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class Avatar extends StatelessWidget {
4 | const Avatar({
5 | super.key,
6 | this.photoUrl,
7 | required this.radius,
8 | this.borderColor,
9 | this.borderWidth,
10 | });
11 | final String? photoUrl;
12 | final double radius;
13 | final Color? borderColor;
14 | final double? borderWidth;
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return Container(
19 | decoration: _borderDecoration(),
20 | child: CircleAvatar(
21 | radius: radius,
22 | backgroundColor: Colors.black12,
23 | backgroundImage: photoUrl != null ? NetworkImage(photoUrl!) : null,
24 | child: photoUrl == null ? Icon(Icons.camera_alt, size: radius) : null,
25 | ),
26 | );
27 | }
28 |
29 | Decoration? _borderDecoration() {
30 | if (borderColor != null && borderWidth != null) {
31 | return BoxDecoration(
32 | shape: BoxShape.circle,
33 | border: Border.all(
34 | color: borderColor!,
35 | width: borderWidth!,
36 | ),
37 | );
38 | }
39 | return null;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/custom_text_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart';
3 |
4 | /// Custom text button with a fixed height
5 | class CustomTextButton extends StatelessWidget {
6 | const CustomTextButton(
7 | {super.key, required this.text, this.style, this.onPressed});
8 | final String text;
9 | final TextStyle? style;
10 | final VoidCallback? onPressed;
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return SizedBox(
15 | height: Sizes.p48,
16 | child: TextButton(
17 | onPressed: onPressed,
18 | child: Text(
19 | text,
20 | style: style,
21 | textAlign: TextAlign.center,
22 | ),
23 | ),
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/date_time_picker.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:starter_architecture_flutter_firebase/src/common_widgets/input_dropdown.dart';
5 | import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart';
6 | import 'package:starter_architecture_flutter_firebase/src/utils/format.dart';
7 |
8 | class DateTimePicker extends StatelessWidget {
9 | const DateTimePicker({
10 | super.key,
11 | required this.labelText,
12 | required this.selectedDate,
13 | required this.selectedTime,
14 | this.onSelectedDate,
15 | this.onSelectedTime,
16 | });
17 |
18 | final String labelText;
19 | final DateTime selectedDate;
20 | final TimeOfDay selectedTime;
21 | final ValueChanged? onSelectedDate;
22 | final ValueChanged? onSelectedTime;
23 |
24 | Future _selectDate(BuildContext context) async {
25 | final pickedDate = await showDatePicker(
26 | context: context,
27 | initialDate: selectedDate,
28 | firstDate: DateTime(2019, 1),
29 | lastDate: DateTime(2100),
30 | );
31 | if (pickedDate != null && pickedDate != selectedDate) {
32 | onSelectedDate?.call(pickedDate);
33 | }
34 | }
35 |
36 | Future _selectTime(BuildContext context) async {
37 | final pickedTime =
38 | await showTimePicker(context: context, initialTime: selectedTime);
39 | if (pickedTime != null && pickedTime != selectedTime) {
40 | onSelectedTime?.call(pickedTime);
41 | }
42 | }
43 |
44 | @override
45 | Widget build(BuildContext context) {
46 | final valueStyle = Theme.of(context).textTheme.titleLarge!;
47 | return Row(
48 | crossAxisAlignment: CrossAxisAlignment.end,
49 | children: [
50 | Expanded(
51 | flex: 5,
52 | child: InputDropdown(
53 | labelText: labelText,
54 | valueText: Format.date(selectedDate),
55 | valueStyle: valueStyle,
56 | onPressed: () => _selectDate(context),
57 | ),
58 | ),
59 | gapW12,
60 | Expanded(
61 | flex: 4,
62 | child: InputDropdown(
63 | valueText: selectedTime.format(context),
64 | valueStyle: valueStyle,
65 | onPressed: () => _selectTime(context),
66 | ),
67 | ),
68 | ],
69 | );
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/empty_content.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class EmptyContent extends StatelessWidget {
4 | const EmptyContent({
5 | super.key,
6 | this.title = 'Nothing here',
7 | this.message = 'Add a new item to get started',
8 | });
9 | final String title;
10 | final String message;
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return Center(
15 | child: Column(
16 | mainAxisAlignment: MainAxisAlignment.center,
17 | children: [
18 | Text(
19 | title,
20 | style: const TextStyle(fontSize: 32.0, color: Colors.black54),
21 | ),
22 | Text(
23 | message,
24 | style: const TextStyle(fontSize: 16.0, color: Colors.black54),
25 | ),
26 | ],
27 | ),
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/empty_placeholder_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:go_router/go_router.dart';
4 | import 'package:starter_architecture_flutter_firebase/src/common_widgets/primary_button.dart';
5 | import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart';
6 | import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart';
7 | import 'package:starter_architecture_flutter_firebase/src/routing/app_router.dart';
8 |
9 | /// Placeholder widget showing a message and CTA to go back to the home screen.
10 | class EmptyPlaceholderWidget extends ConsumerWidget {
11 | const EmptyPlaceholderWidget({super.key, required this.message});
12 | final String message;
13 |
14 | @override
15 | Widget build(BuildContext context, WidgetRef ref) {
16 | return Padding(
17 | padding: const EdgeInsets.all(Sizes.p16),
18 | child: Center(
19 | child: Column(
20 | mainAxisSize: MainAxisSize.min,
21 | crossAxisAlignment: CrossAxisAlignment.center,
22 | children: [
23 | Text(
24 | message,
25 | style: Theme.of(context).textTheme.headlineMedium,
26 | textAlign: TextAlign.center,
27 | ),
28 | gapH32,
29 | PrimaryButton(
30 | onPressed: () {
31 | final isLoggedIn =
32 | ref.watch(authRepositoryProvider).currentUser != null;
33 | context.goNamed(
34 | isLoggedIn ? AppRoute.jobs.name : AppRoute.signIn.name);
35 | },
36 | text: 'Go Home',
37 | )
38 | ],
39 | ),
40 | ),
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/error_message_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class ErrorMessageWidget extends StatelessWidget {
4 | const ErrorMessageWidget(this.errorMessage, {super.key});
5 | final String errorMessage;
6 | @override
7 | Widget build(BuildContext context) {
8 | return Text(
9 | errorMessage,
10 | style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.red),
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/input_dropdown.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class InputDropdown extends StatelessWidget {
4 | const InputDropdown({
5 | super.key,
6 | this.labelText,
7 | required this.valueText,
8 | required this.valueStyle,
9 | this.onPressed,
10 | });
11 |
12 | final String? labelText;
13 | final String valueText;
14 | final TextStyle valueStyle;
15 | final VoidCallback? onPressed;
16 |
17 | @override
18 | Widget build(BuildContext context) {
19 | return InkWell(
20 | onTap: onPressed,
21 | child: InputDecorator(
22 | decoration: InputDecoration(
23 | labelText: labelText,
24 | ),
25 | baseStyle: valueStyle,
26 | child: Row(
27 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
28 | mainAxisSize: MainAxisSize.min,
29 | children: [
30 | Text(valueText, style: valueStyle),
31 | Icon(Icons.arrow_drop_down,
32 | color: Theme.of(context).brightness == Brightness.light
33 | ? Colors.grey.shade700
34 | : Colors.white70),
35 | ],
36 | ),
37 | ),
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/list_items_builder.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:starter_architecture_flutter_firebase/src/common_widgets/empty_content.dart';
4 |
5 | typedef ItemWidgetBuilder = Widget Function(BuildContext context, T item);
6 |
7 | class ListItemsBuilder extends StatelessWidget {
8 | const ListItemsBuilder({
9 | super.key,
10 | required this.data,
11 | required this.itemBuilder,
12 | });
13 | final AsyncValue> data;
14 | final ItemWidgetBuilder itemBuilder;
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return data.when(
19 | data: (items) => items.isNotEmpty
20 | ? ListView.separated(
21 | itemCount: items.length + 2,
22 | separatorBuilder: (context, index) => const Divider(height: 0.5),
23 | itemBuilder: (context, index) {
24 | if (index == 0 || index == items.length + 1) {
25 | return const SizedBox.shrink();
26 | }
27 | return itemBuilder(context, items[index - 1]);
28 | },
29 | )
30 | : const EmptyContent(),
31 | loading: () => const Center(child: CircularProgressIndicator()),
32 | error: (_, __) => const EmptyContent(
33 | title: 'Something went wrong',
34 | message: 'Can\'t load items right now',
35 | ),
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/primary_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart';
3 |
4 | /// Primary button based on [ElevatedButton].
5 | /// Useful for CTAs in the app.
6 | /// @param text - text to display on the button.
7 | /// @param isLoading - if true, a loading indicator will be displayed instead of
8 | /// the text.
9 | /// @param onPressed - callback to be called when the button is pressed.
10 | class PrimaryButton extends StatelessWidget {
11 | const PrimaryButton(
12 | {super.key, required this.text, this.isLoading = false, this.onPressed});
13 | final String text;
14 | final bool isLoading;
15 | final VoidCallback? onPressed;
16 | @override
17 | Widget build(BuildContext context) {
18 | return SizedBox(
19 | height: Sizes.p48,
20 | child: ElevatedButton(
21 | onPressed: onPressed,
22 | child: isLoading
23 | ? const CircularProgressIndicator()
24 | : Text(
25 | text,
26 | textAlign: TextAlign.center,
27 | style: Theme.of(context)
28 | .textTheme
29 | .titleLarge!
30 | .copyWith(color: Colors.white),
31 | ),
32 | ),
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/responsive_center.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:starter_architecture_flutter_firebase/src/constants/breakpoints.dart';
3 |
4 | /// Reusable widget for showing a child with a maximum content width constraint.
5 | /// If available width is larger than the maximum width, the child will be
6 | /// centered.
7 | /// If available width is smaller than the maximum width, the child use all the
8 | /// available width.
9 | class ResponsiveCenter extends StatelessWidget {
10 | const ResponsiveCenter({
11 | super.key,
12 | this.maxContentWidth = Breakpoint.desktop,
13 | this.padding = EdgeInsets.zero,
14 | required this.child,
15 | });
16 | final double maxContentWidth;
17 | final EdgeInsetsGeometry padding;
18 | final Widget child;
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | // Use Center as it has *unconstrained* width (loose constraints)
23 | return Center(
24 | // together with SizedBox to specify the max width (tight constraints)
25 | // See this thread for more info:
26 | // https://twitter.com/biz84/status/1445400059894542337
27 | child: SizedBox(
28 | width: maxContentWidth,
29 | child: Padding(
30 | padding: padding,
31 | child: child,
32 | ),
33 | ),
34 | );
35 | }
36 | }
37 |
38 | /// Sliver-equivalent of [ResponsiveCenter].
39 | class ResponsiveSliverCenter extends StatelessWidget {
40 | const ResponsiveSliverCenter({
41 | super.key,
42 | this.maxContentWidth = Breakpoint.desktop,
43 | this.padding = EdgeInsets.zero,
44 | required this.child,
45 | });
46 | final double maxContentWidth;
47 | final EdgeInsetsGeometry padding;
48 | final Widget child;
49 | @override
50 | Widget build(BuildContext context) {
51 | return SliverToBoxAdapter(
52 | child: ResponsiveCenter(
53 | maxContentWidth: maxContentWidth,
54 | padding: padding,
55 | child: child,
56 | ),
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/responsive_scrollable_card.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:starter_architecture_flutter_firebase/src/common_widgets/responsive_center.dart';
3 | import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart';
4 | import 'package:starter_architecture_flutter_firebase/src/constants/breakpoints.dart';
5 |
6 | /// Scrollable widget that shows a responsive card with a given child widget.
7 | /// Useful for displaying forms and other widgets that need to be scrollable.
8 | class ResponsiveScrollableCard extends StatelessWidget {
9 | const ResponsiveScrollableCard({super.key, required this.child});
10 | final Widget child;
11 | @override
12 | Widget build(BuildContext context) {
13 | return SingleChildScrollView(
14 | child: ResponsiveCenter(
15 | maxContentWidth: Breakpoint.tablet,
16 | child: Padding(
17 | padding: const EdgeInsets.all(Sizes.p16),
18 | child: Card(
19 | child: Padding(
20 | padding: const EdgeInsets.all(Sizes.p16),
21 | child: child,
22 | ),
23 | ),
24 | ),
25 | ),
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/src/common_widgets/segmented_control.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 |
3 | class SegmentedControl extends StatelessWidget {
4 | const SegmentedControl({
5 | super.key,
6 | required this.header,
7 | required this.value,
8 | required this.children,
9 | required this.onValueChanged,
10 | });
11 | final Widget header;
12 | final T value;
13 | final Map children;
14 | final ValueChanged onValueChanged;
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return Column(
19 | crossAxisAlignment: CrossAxisAlignment.start,
20 | children: [
21 | Padding(
22 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
23 | child: header,
24 | ),
25 | SizedBox(
26 | width: double.infinity,
27 | child: CupertinoSegmentedControl(
28 | children: children,
29 | groupValue: value,
30 | onValueChanged: onValueChanged,
31 | ),
32 | ),
33 | ],
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/src/constants/app_sizes.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | /// Constant sizes to be used in the app (paddings, gaps, rounded corners etc.)
4 | class Sizes {
5 | static const p4 = 4.0;
6 | static const p8 = 8.0;
7 | static const p12 = 12.0;
8 | static const p16 = 16.0;
9 | static const p20 = 20.0;
10 | static const p24 = 24.0;
11 | static const p32 = 32.0;
12 | static const p48 = 48.0;
13 | static const p64 = 64.0;
14 | }
15 |
16 | /// Constant gap widths
17 | const gapW4 = SizedBox(width: Sizes.p4);
18 | const gapW8 = SizedBox(width: Sizes.p8);
19 | const gapW12 = SizedBox(width: Sizes.p12);
20 | const gapW16 = SizedBox(width: Sizes.p16);
21 | const gapW20 = SizedBox(width: Sizes.p20);
22 | const gapW24 = SizedBox(width: Sizes.p24);
23 | const gapW32 = SizedBox(width: Sizes.p32);
24 | const gapW48 = SizedBox(width: Sizes.p48);
25 | const gapW64 = SizedBox(width: Sizes.p64);
26 |
27 | /// Constant gap heights
28 | const gapH4 = SizedBox(height: Sizes.p4);
29 | const gapH8 = SizedBox(height: Sizes.p8);
30 | const gapH12 = SizedBox(height: Sizes.p12);
31 | const gapH16 = SizedBox(height: Sizes.p16);
32 | const gapH20 = SizedBox(height: Sizes.p20);
33 | const gapH24 = SizedBox(height: Sizes.p24);
34 | const gapH32 = SizedBox(height: Sizes.p32);
35 | const gapH48 = SizedBox(height: Sizes.p48);
36 | const gapH64 = SizedBox(height: Sizes.p64);
37 |
--------------------------------------------------------------------------------
/lib/src/constants/breakpoints.dart:
--------------------------------------------------------------------------------
1 | /// Layout breakpoints used in the app.
2 | class Breakpoint {
3 | static const double desktop = 900;
4 | static const double tablet = 600;
5 | }
6 |
--------------------------------------------------------------------------------
/lib/src/constants/keys.dart:
--------------------------------------------------------------------------------
1 | class Keys {
2 | static const String emailPassword = 'email-password';
3 | static const String anonymous = 'anonymous';
4 | static const String tabBar = 'tabBar';
5 | static const String jobsTab = 'jobsTab';
6 | static const String entriesTab = 'entriesTab';
7 | static const String accountTab = 'accountTab';
8 | static const String logout = 'logout';
9 | static const String alertDefault = 'alertDefault';
10 | static const String alertCancel = 'alertCancel';
11 | }
12 |
--------------------------------------------------------------------------------
/lib/src/constants/strings.dart:
--------------------------------------------------------------------------------
1 | class Strings {
2 | // Generic strings
3 | static const String ok = 'OK';
4 | static const String cancel = 'Cancel';
5 |
6 | // Logout
7 | static const String logout = 'Logout';
8 | static const String logoutAreYouSure =
9 | 'Are you sure that you want to logout?';
10 | static const String logoutFailed = 'Logout failed';
11 |
12 | // Sign In Page
13 | static const String signIn = 'Sign in';
14 | static const String signInWithEmailPassword = 'Sign in with email & password';
15 | static const String goAnonymous = 'Go anonymous';
16 | static const String or = 'or';
17 | static const String signInFailed = 'Sign in failed';
18 |
19 | // Home page
20 | static const String homePage = 'Home Page';
21 |
22 | // Jobs page
23 | static const String jobs = 'Jobs';
24 |
25 | // Entries page
26 | static const String entries = 'Entries';
27 |
28 | // Account page
29 | static const String account = 'Account';
30 | static const String accountPage = 'Account Page';
31 | }
32 |
--------------------------------------------------------------------------------
/lib/src/features/authentication/data/firebase_auth_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_auth/firebase_auth.dart';
2 | import 'package:riverpod/riverpod.dart';
3 | import 'package:riverpod_annotation/riverpod_annotation.dart';
4 |
5 | part 'firebase_auth_repository.g.dart';
6 |
7 | class AuthRepository {
8 | AuthRepository(this._auth);
9 | final FirebaseAuth _auth;
10 |
11 | Stream authStateChanges() => _auth.authStateChanges();
12 | User? get currentUser => _auth.currentUser;
13 |
14 | Future signInAnonymously() {
15 | return _auth.signInAnonymously();
16 | }
17 | }
18 |
19 | @Riverpod(keepAlive: true)
20 | FirebaseAuth firebaseAuth(Ref ref) {
21 | return FirebaseAuth.instance;
22 | }
23 |
24 | @Riverpod(keepAlive: true)
25 | AuthRepository authRepository(Ref ref) {
26 | return AuthRepository(ref.watch(firebaseAuthProvider));
27 | }
28 |
29 | @riverpod
30 | Stream authStateChanges(Ref ref) {
31 | return ref.watch(authRepositoryProvider).authStateChanges();
32 | }
33 |
--------------------------------------------------------------------------------
/lib/src/features/authentication/data/firebase_auth_repository.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'firebase_auth_repository.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$firebaseAuthHash() => r'cb440927c3ab863427fd4b052a8ccba4c024c863';
10 |
11 | /// See also [firebaseAuth].
12 | @ProviderFor(firebaseAuth)
13 | final firebaseAuthProvider = Provider.internal(
14 | firebaseAuth,
15 | name: r'firebaseAuthProvider',
16 | debugGetCreateSourceHash:
17 | const bool.fromEnvironment('dart.vm.product') ? null : _$firebaseAuthHash,
18 | dependencies: null,
19 | allTransitiveDependencies: null,
20 | );
21 |
22 | @Deprecated('Will be removed in 3.0. Use Ref instead')
23 | // ignore: unused_element
24 | typedef FirebaseAuthRef = ProviderRef;
25 | String _$authRepositoryHash() => r'0e32dee9e183c43ec14a6b58d74d26deb3950cbc';
26 |
27 | /// See also [authRepository].
28 | @ProviderFor(authRepository)
29 | final authRepositoryProvider = Provider.internal(
30 | authRepository,
31 | name: r'authRepositoryProvider',
32 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
33 | ? null
34 | : _$authRepositoryHash,
35 | dependencies: null,
36 | allTransitiveDependencies: null,
37 | );
38 |
39 | @Deprecated('Will be removed in 3.0. Use Ref instead')
40 | // ignore: unused_element
41 | typedef AuthRepositoryRef = ProviderRef;
42 | String _$authStateChangesHash() => r'7bdb56f405df8ffc5554e0128ec15d474f011ec9';
43 |
44 | /// See also [authStateChanges].
45 | @ProviderFor(authStateChanges)
46 | final authStateChangesProvider = AutoDisposeStreamProvider.internal(
47 | authStateChanges,
48 | name: r'authStateChangesProvider',
49 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
50 | ? null
51 | : _$authStateChangesHash,
52 | dependencies: null,
53 | allTransitiveDependencies: null,
54 | );
55 |
56 | @Deprecated('Will be removed in 3.0. Use Ref instead')
57 | // ignore: unused_element
58 | typedef AuthStateChangesRef = AutoDisposeStreamProviderRef;
59 | // ignore_for_file: type=lint
60 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
61 |
--------------------------------------------------------------------------------
/lib/src/features/authentication/domain/app_user.dart:
--------------------------------------------------------------------------------
1 | /// Type defining a user ID from Firebase.
2 | typedef UserID = String;
3 |
4 | /// Simple class representing the user UID and email.
5 | class AppUser {
6 | const AppUser({
7 | required this.uid,
8 | required this.email,
9 | });
10 | final String uid;
11 | final String email;
12 |
13 | @override
14 | bool operator ==(Object other) {
15 | if (identical(this, other)) return true;
16 |
17 | return other is AppUser && other.uid == uid && other.email == email;
18 | }
19 |
20 | @override
21 | int get hashCode => uid.hashCode ^ email.hashCode;
22 |
23 | @override
24 | String toString() => 'AppUser(uid: $uid, email: $email)';
25 | }
26 |
--------------------------------------------------------------------------------
/lib/src/features/authentication/presentation/auth_providers.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_auth/firebase_auth.dart'
2 | hide EmailAuthProvider, AuthProvider;
3 | import 'package:firebase_ui_auth/firebase_ui_auth.dart';
4 | import 'package:riverpod/riverpod.dart';
5 | import 'package:riverpod_annotation/riverpod_annotation.dart';
6 |
7 | part 'auth_providers.g.dart';
8 |
9 | @Riverpod(keepAlive: true)
10 | List> authProviders(
11 | Ref ref) {
12 | return [
13 | EmailAuthProvider(),
14 | ];
15 | }
16 |
--------------------------------------------------------------------------------
/lib/src/features/authentication/presentation/auth_providers.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'auth_providers.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$authProvidersHash() => r'8a83535c31539dac72f21c3f27b7d7fb77161e5f';
10 |
11 | /// See also [authProviders].
12 | @ProviderFor(authProviders)
13 | final authProvidersProvider =
14 | Provider>>.internal(
15 | authProviders,
16 | name: r'authProvidersProvider',
17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
18 | ? null
19 | : _$authProvidersHash,
20 | dependencies: null,
21 | allTransitiveDependencies: null,
22 | );
23 |
24 | @Deprecated('Will be removed in 3.0. Use Ref instead')
25 | // ignore: unused_element
26 | typedef AuthProvidersRef
27 | = ProviderRef>>;
28 | // ignore_for_file: type=lint
29 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
30 |
--------------------------------------------------------------------------------
/lib/src/features/authentication/presentation/custom_profile_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_ui_auth/firebase_ui_auth.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_riverpod/flutter_riverpod.dart';
4 | import 'package:starter_architecture_flutter_firebase/src/features/authentication/presentation/auth_providers.dart';
5 |
6 | class CustomProfileScreen extends ConsumerWidget {
7 | const CustomProfileScreen({super.key});
8 |
9 | @override
10 | Widget build(BuildContext context, WidgetRef ref) {
11 | final authProviders = ref.watch(authProvidersProvider);
12 | return ProfileScreen(
13 | appBar: AppBar(
14 | title: const Text('Profile'),
15 | ),
16 | providers: authProviders,
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/src/features/authentication/presentation/custom_sign_in_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_ui_auth/firebase_ui_auth.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_riverpod/flutter_riverpod.dart';
4 | import 'package:starter_architecture_flutter_firebase/src/constants/app_sizes.dart';
5 | import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart';
6 |
7 | import 'auth_providers.dart';
8 |
9 | class CustomSignInScreen extends ConsumerWidget {
10 | const CustomSignInScreen({super.key});
11 |
12 | @override
13 | Widget build(BuildContext context, WidgetRef ref) {
14 | final authProviders = ref.watch(authProvidersProvider);
15 | return Scaffold(
16 | appBar: AppBar(
17 | title: const Text('Sign in'),
18 | ),
19 | body: SignInScreen(
20 | providers: authProviders,
21 | footerBuilder: (context, action) => const SignInAnonymouslyFooter(),
22 | ),
23 | );
24 | }
25 | }
26 |
27 | class SignInAnonymouslyFooter extends ConsumerWidget {
28 | const SignInAnonymouslyFooter({super.key});
29 |
30 | @override
31 | Widget build(BuildContext context, WidgetRef ref) {
32 | return Column(
33 | children: [
34 | gapH8,
35 | const Row(
36 | children: [
37 | Expanded(child: Divider()),
38 | Padding(
39 | padding: EdgeInsets.symmetric(horizontal: Sizes.p8),
40 | child: Text('or'),
41 | ),
42 | Expanded(child: Divider()),
43 | ],
44 | ),
45 | TextButton(
46 | onPressed: () => ref.read(firebaseAuthProvider).signInAnonymously(),
47 | child: const Text('Sign in anonymously'),
48 | ),
49 | ],
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/lib/src/features/entries/application/entries_service.dart:
--------------------------------------------------------------------------------
1 | import 'package:riverpod/riverpod.dart';
2 | import 'package:riverpod_annotation/riverpod_annotation.dart';
3 | import 'package:rxdart/rxdart.dart';
4 | import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart';
5 | import 'package:starter_architecture_flutter_firebase/src/features/authentication/domain/app_user.dart';
6 | import 'package:starter_architecture_flutter_firebase/src/features/entries/data/entries_repository.dart';
7 | import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/daily_jobs_details.dart';
8 | import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entries_list_tile_model.dart';
9 | import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry_job.dart';
10 | import 'package:starter_architecture_flutter_firebase/src/features/jobs/data/jobs_repository.dart';
11 | import 'package:starter_architecture_flutter_firebase/src/utils/format.dart';
12 | import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry.dart';
13 | import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart';
14 |
15 | part 'entries_service.g.dart';
16 |
17 | // TODO: Clean up this code a bit more
18 | class EntriesService {
19 | EntriesService(
20 | {required this.jobsRepository, required this.entriesRepository});
21 | final JobsRepository jobsRepository;
22 | final EntriesRepository entriesRepository;
23 |
24 | /// combine List, List into List
25 | Stream> _allEntriesStream(UserID uid) =>
26 | CombineLatestStream.combine2(
27 | entriesRepository.watchEntries(uid: uid),
28 | jobsRepository.watchJobs(uid: uid),
29 | _entriesJobsCombiner,
30 | );
31 |
32 | static List _entriesJobsCombiner(
33 | List entries, List jobs) {
34 | return entries.map((entry) {
35 | final job = jobs.firstWhere((job) => job.id == entry.jobId);
36 | return EntryJob(entry, job);
37 | }).toList();
38 | }
39 |
40 | /// Output stream
41 | Stream> entriesTileModelStream(UserID uid) =>
42 | _allEntriesStream(uid).map(_createModels);
43 |
44 | static List _createModels(List allEntries) {
45 | if (allEntries.isEmpty) {
46 | return [];
47 | }
48 | final allDailyJobsDetails = DailyJobsDetails.all(allEntries);
49 |
50 | // total duration across all jobs
51 | final totalDuration = allDailyJobsDetails
52 | .map((dateJobsDuration) => dateJobsDuration.duration)
53 | .reduce((value, element) => value + element);
54 |
55 | // total pay across all jobs
56 | final totalPay = allDailyJobsDetails
57 | .map((dateJobsDuration) => dateJobsDuration.pay)
58 | .reduce((value, element) => value + element);
59 |
60 | return [
61 | EntriesListTileModel(
62 | leadingText: 'All Entries',
63 | middleText: Format.currency(totalPay),
64 | trailingText: Format.hours(totalDuration),
65 | ),
66 | for (DailyJobsDetails dailyJobsDetails in allDailyJobsDetails) ...[
67 | EntriesListTileModel(
68 | isHeader: true,
69 | leadingText: Format.date(dailyJobsDetails.date),
70 | middleText: Format.currency(dailyJobsDetails.pay),
71 | trailingText: Format.hours(dailyJobsDetails.duration),
72 | ),
73 | for (JobDetails jobDuration in dailyJobsDetails.jobsDetails)
74 | EntriesListTileModel(
75 | leadingText: jobDuration.name,
76 | middleText: Format.currency(jobDuration.pay),
77 | trailingText: Format.hours(jobDuration.durationInHours),
78 | ),
79 | ]
80 | ];
81 | }
82 | }
83 |
84 | @riverpod
85 | EntriesService entriesService(Ref ref) {
86 | return EntriesService(
87 | jobsRepository: ref.watch(jobsRepositoryProvider),
88 | entriesRepository: ref.watch(entriesRepositoryProvider),
89 | );
90 | }
91 |
92 | @riverpod
93 | Stream> entriesTileModelStream(
94 | Ref ref) {
95 | final user = ref.watch(firebaseAuthProvider).currentUser;
96 | if (user == null) {
97 | throw AssertionError('User can\'t be null when fetching entries');
98 | }
99 | final entriesService = ref.watch(entriesServiceProvider);
100 | return entriesService.entriesTileModelStream(user.uid);
101 | }
102 |
--------------------------------------------------------------------------------
/lib/src/features/entries/application/entries_service.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'entries_service.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$entriesServiceHash() => r'106c29e519ac1706956f952263745337399caba9';
10 |
11 | /// See also [entriesService].
12 | @ProviderFor(entriesService)
13 | final entriesServiceProvider = AutoDisposeProvider.internal(
14 | entriesService,
15 | name: r'entriesServiceProvider',
16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
17 | ? null
18 | : _$entriesServiceHash,
19 | dependencies: null,
20 | allTransitiveDependencies: null,
21 | );
22 |
23 | @Deprecated('Will be removed in 3.0. Use Ref instead')
24 | // ignore: unused_element
25 | typedef EntriesServiceRef = AutoDisposeProviderRef;
26 | String _$entriesTileModelStreamHash() =>
27 | r'e8f3184f1b1db43eb92198669492a36d3ee03356';
28 |
29 | /// See also [entriesTileModelStream].
30 | @ProviderFor(entriesTileModelStream)
31 | final entriesTileModelStreamProvider =
32 | AutoDisposeStreamProvider>.internal(
33 | entriesTileModelStream,
34 | name: r'entriesTileModelStreamProvider',
35 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
36 | ? null
37 | : _$entriesTileModelStreamHash,
38 | dependencies: null,
39 | allTransitiveDependencies: null,
40 | );
41 |
42 | @Deprecated('Will be removed in 3.0. Use Ref instead')
43 | // ignore: unused_element
44 | typedef EntriesTileModelStreamRef
45 | = AutoDisposeStreamProviderRef>;
46 | // ignore_for_file: type=lint
47 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
48 |
--------------------------------------------------------------------------------
/lib/src/features/entries/data/entries_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:cloud_firestore/cloud_firestore.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:riverpod_annotation/riverpod_annotation.dart';
4 | import 'package:starter_architecture_flutter_firebase/src/features/authentication/data/firebase_auth_repository.dart';
5 | import 'package:starter_architecture_flutter_firebase/src/features/authentication/domain/app_user.dart';
6 | import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry.dart';
7 | import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart';
8 |
9 | part 'entries_repository.g.dart';
10 |
11 | class EntriesRepository {
12 | const EntriesRepository(this._firestore);
13 | final FirebaseFirestore _firestore;
14 |
15 | static String entryPath(String uid, String entryId) =>
16 | 'users/$uid/entries/$entryId';
17 | static String entriesPath(String uid) => 'users/$uid/entries';
18 |
19 | // create
20 | Future addEntry({
21 | required UserID uid,
22 | required JobID jobId,
23 | required DateTime start,
24 | required DateTime end,
25 | required String comment,
26 | }) =>
27 | _firestore.collection(entriesPath(uid)).add({
28 | 'jobId': jobId,
29 | 'start': start.millisecondsSinceEpoch,
30 | 'end': end.millisecondsSinceEpoch,
31 | 'comment': comment,
32 | });
33 |
34 | // update
35 | Future updateEntry({
36 | required UserID uid,
37 | required Entry entry,
38 | }) =>
39 | _firestore.doc(entryPath(uid, entry.id)).update(entry.toMap());
40 |
41 | // delete
42 | Future deleteEntry({required UserID uid, required EntryID entryId}) =>
43 | _firestore.doc(entryPath(uid, entryId)).delete();
44 |
45 | // read
46 | Stream> watchEntries({required UserID uid, JobID? jobId}) =>
47 | queryEntries(uid: uid, jobId: jobId)
48 | .snapshots()
49 | .map((snapshot) => snapshot.docs.map((doc) => doc.data()).toList());
50 |
51 | Query queryEntries({required UserID uid, JobID? jobId}) {
52 | Query query =
53 | _firestore.collection(entriesPath(uid)).withConverter(
54 | fromFirestore: (snapshot, _) =>
55 | Entry.fromMap(snapshot.data()!, snapshot.id),
56 | toFirestore: (entry, _) => entry.toMap(),
57 | );
58 | if (jobId != null) {
59 | query = query.where('jobId', isEqualTo: jobId);
60 | }
61 | return query;
62 | }
63 | }
64 |
65 | @riverpod
66 | EntriesRepository entriesRepository(Ref ref) {
67 | return EntriesRepository(FirebaseFirestore.instance);
68 | }
69 |
70 | @riverpod
71 | Query jobEntriesQuery(Ref ref, String jobId) {
72 | final user = ref.watch(firebaseAuthProvider).currentUser;
73 | if (user == null) {
74 | throw AssertionError('User can\'t be null when fetching jobs');
75 | }
76 | final repository = ref.watch(entriesRepositoryProvider);
77 | return repository.queryEntries(uid: user.uid, jobId: jobId);
78 | }
79 |
--------------------------------------------------------------------------------
/lib/src/features/entries/domain/daily_jobs_details.dart:
--------------------------------------------------------------------------------
1 | import 'package:starter_architecture_flutter_firebase/src/features/entries/domain/entry_job.dart';
2 |
3 | /// Temporary model class to store the time tracked and pay for a job
4 | class JobDetails {
5 | JobDetails({
6 | required this.name,
7 | required this.durationInHours,
8 | required this.pay,
9 | });
10 | final String name;
11 | double durationInHours;
12 | double pay;
13 | }
14 |
15 | /// Groups together all jobs/entries on a given day
16 | class DailyJobsDetails {
17 | DailyJobsDetails({required this.date, required this.jobsDetails});
18 | final DateTime date;
19 | final List jobsDetails;
20 |
21 | double get pay => jobsDetails
22 | .map((jobDuration) => jobDuration.pay)
23 | .reduce((value, element) => value + element);
24 |
25 | double get duration => jobsDetails
26 | .map((jobDuration) => jobDuration.durationInHours)
27 | .reduce((value, element) => value + element);
28 |
29 | /// splits all entries into separate groups by date
30 | static Map> _entriesByDate(List entries) {
31 | final Map> map = {};
32 | for (final entryJob in entries) {
33 | final entryDayStart = DateTime(entryJob.entry.start.year,
34 | entryJob.entry.start.month, entryJob.entry.start.day);
35 | if (map[entryDayStart] == null) {
36 | map[entryDayStart] = [entryJob];
37 | } else {
38 | map[entryDayStart]!.add(entryJob);
39 | }
40 | }
41 | return map;
42 | }
43 |
44 | /// maps an unordered list of EntryJob into a list of DailyJobsDetails with date information
45 | static List all(List entries) {
46 | final byDate = _entriesByDate(entries);
47 | final List list = [];
48 | for (final pair in byDate.entries) {
49 | final date = pair.key;
50 | final entriesByDate = pair.value;
51 | final byJob = _jobsDetails(entriesByDate);
52 | list.add(DailyJobsDetails(date: date, jobsDetails: byJob));
53 | }
54 | return list.toList();
55 | }
56 |
57 | /// groups entries by job
58 | static List _jobsDetails(List entries) {
59 | final Map jobDuration = {};
60 | for (final entryJob in entries) {
61 | final entry = entryJob.entry;
62 | final pay = entry.durationInHours * entryJob.job.ratePerHour;
63 | if (jobDuration[entry.jobId] == null) {
64 | jobDuration[entry.jobId] = JobDetails(
65 | name: entryJob.job.name,
66 | durationInHours: entry.durationInHours,
67 | pay: pay,
68 | );
69 | } else {
70 | jobDuration[entry.jobId]!.pay += pay;
71 | jobDuration[entry.jobId]!.durationInHours += entry.durationInHours;
72 | }
73 | }
74 | return jobDuration.values.toList();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/lib/src/features/entries/domain/entries_list_tile_model.dart:
--------------------------------------------------------------------------------
1 | class EntriesListTileModel {
2 | const EntriesListTileModel({
3 | required this.leadingText,
4 | required this.trailingText,
5 | this.middleText,
6 | this.isHeader = false,
7 | });
8 | final String leadingText;
9 | final String trailingText;
10 | final String? middleText;
11 | final bool isHeader;
12 | }
13 |
--------------------------------------------------------------------------------
/lib/src/features/entries/domain/entry.dart:
--------------------------------------------------------------------------------
1 | import 'package:equatable/equatable.dart';
2 | import 'package:starter_architecture_flutter_firebase/src/features/jobs/domain/job.dart';
3 |
4 | typedef EntryID = String;
5 |
6 | class Entry extends Equatable {
7 | const Entry({
8 | required this.id,
9 | required this.jobId,
10 | required this.start,
11 | required this.end,
12 | required this.comment,
13 | });
14 | final EntryID id;
15 | final JobID jobId;
16 | final DateTime start;
17 | final DateTime end;
18 | final String comment;
19 |
20 | @override
21 | List